Repository: RetroAchievements/RAIntegration
Pull request #1102 (2024-05-21 20:44:34)

honor Enabled Leaderboards setting
https://discord.com/channels/310192285306454017/353363923333808128/1242463346125574225


Patch:
diff --git a/rcheevos b/rcheevos
index 21b87cf8..47b0bcdd 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit 21b87cf86c5bf35f19788292b86019d2716e40d1
+Subproject commit 47b0bcdd9b3df44a28d8ab0a34a76d2929bf3bab
diff --git a/src/RA_ImageFactory.h b/src/RA_ImageFactory.h
index 19ad1e0d..8dc4acc1 100644
--- a/src/RA_ImageFactory.h
+++ b/src/RA_ImageFactory.h
@@ -5,11 +5,9 @@
 //////////////////////////////////////////////////////////////////////////
 //	Image Factory
 
-_EXTERN_C
 void DrawImage(_In_ HDC hDC, _In_ HBITMAP hBitmap, _In_ int nX, _In_ int nY, _In_ int nW, _In_ int nH);
 void DrawImageTiled(_In_ HDC hDC, _In_ HBITMAP hBitmap, _Inout_ RECT& rcSource, _In_ const RECT& rcDest);
 void DrawImageStretched(_In_ HDC hDC, _In_ HBITMAP hBitmap,
                         _In_ const RECT& rcSource, _In_ const RECT& rcDest);
-_END_EXTERN_C
 
 #endif // !RA_IMAGEFACTORY_H
diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp
index 89dc5f81..b3b74c5d 100644
--- a/src/services/AchievementRuntime.cpp
+++ b/src/services/AchievementRuntime.cpp
@@ -319,8 +319,34 @@ static ra::services::Http::Response HandleOfflineRequest(const ra::services::Htt
         ra::StringPrintf("{\"Success\":false,\"Error\":\"No offline implementation for %s\"}", sApi));
 }
 
+void AchievementRuntime::AsyncServerCall(const rc_api_request_t* pRequest,
+                                         AsyncServerCallCallback fCallback, void* pCallbackData) const
+{
+    struct callback_pair_t
+    {
+        AsyncServerCallCallback fCallback;
+        void* pCallbackData;
+    };
+
+    auto* pCallbackPair = static_cast<callback_pair_t*>(malloc(sizeof(callback_pair_t)));
+    Expects(pCallbackPair != nullptr);
+    pCallbackPair->fCallback = fCallback;
+    pCallbackPair->pCallbackData = pCallbackData;
+
+    ServerCallAsync(pRequest,
+        [](const rc_api_server_response_t* server_response, void* callback_data)
+        {
+            Expects(server_response != nullptr);
+            Expects(callback_data != nullptr);
+            auto* pCallbackPair = static_cast<callback_pair_t*>(callback_data);
+            pCallbackPair->fCallback(*server_response, pCallbackPair->pCallbackData);
+            free(pCallbackPair);
+        },
+        pCallbackPair, nullptr);
+}
+
 void AchievementRuntime::ServerCallAsync(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback,
-                                     void* pCallbackData, rc_client_t*)
+                                         void* pCallbackData, rc_client_t*)
 {
     ra::services::Http::Request httpRequest(pRequest->url);
     httpRequest.SetPostData(pRequest->post_data);
@@ -1736,7 +1762,8 @@ static void HandleLeaderboardStartedEvent(const rc_client_leaderboard_t& pLeader
 
     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted) !=
-        ra::ui::viewmodels::PopupLocation::None)
+        ra::ui::viewmodels::PopupLocation::None &&
+        pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
     {
         ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lb.wav");
         auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
@@ -1757,7 +1784,8 @@ static void HandleLeaderboardFailedEvent(const rc_client_leaderboard_t& pLeaderb
 
     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled) !=
-        ra::ui::viewmodels::PopupLocation::None)
+        ra::ui::viewmodels::PopupLocation::None &&
+        pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
     {
         ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\lbcancel.wav");
         auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
@@ -1770,7 +1798,8 @@ static void ShowSimplifiedScoreboard(const rc_client_leaderboard_t& pLeaderboard
 {
     auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) ==
-        ra::ui::viewmodels::PopupLocation::None)
+        ra::ui::viewmodels::PopupLocation::None ||
+        !pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
     {
         return;
     }
@@ -1799,6 +1828,10 @@ static void HandleLeaderboardSubmittedEvent(const rc_client_leaderboard_t& pLead
         return;
     }
 
+    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+    if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
+        return;
+
     std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmPopup(new ra::ui::viewmodels::PopupMessageViewModel);
     vmPopup->SetDescription(ra::Widen(pLeaderboard.title));
     std::wstring sTitle = L"Leaderboard Submitted";
@@ -1845,7 +1878,6 @@ static void HandleLeaderboardSubmittedEvent(const rc_client_leaderboard_t& pLead
 
     if (bSubmit)
     {
-        const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
         if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
         {
             vmPopup->SetErrorDetail(L"Submission requires Hardcore mode");
@@ -1892,7 +1924,8 @@ static void HandleLeaderboardTrackerShowEvent(const rc_client_leaderboard_tracke
 {
     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) !=
-        ra::ui::viewmodels::PopupLocation::None)
+        ra::ui::viewmodels::PopupLocation::None &&
+        pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
     {
         auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
         auto& pScoreTracker = pOverlayManager.AddScoreTracker(pTracker.id);
@@ -1911,7 +1944,8 @@ static void HandleLeaderboardScoreboardEvent(const rc_client_leaderboard_scorebo
 {
     auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) ==
-        ra::ui::viewmodels::PopupLocation::None)
+        ra::ui::viewmodels::PopupLocation::None ||
+        !pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
     {
         return;
     }
@@ -2011,6 +2045,10 @@ static void HandleServerError(const rc_client_server_error_t& pServerError)
 
     if (strcmp(pServerError.api, "submit_lboard_entry") == 0)
     {
+        const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+        if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards))
+            return;
+
         const auto nLeaderboardId = pServerError.related_id;
         const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
         const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(nLeaderboardId);
diff --git a/src/services/AchievementRuntime.hh b/src/services/AchievementRuntime.hh
index 9a58cb21..d1dbe61b 100644
--- a/src/services/AchievementRuntime.hh
+++ b/src/services/AchievementRuntime.hh
@@ -149,6 +149,12 @@ public:
     /// </summary>
     void SetPaused(bool bValue) noexcept { m_bPaused = bValue; }
 
+    typedef void (*AsyncServerCallCallback)(const rc_api_server_response_t& pResponse, void* pCallbackData);
+    /// <summary>
+    /// Makes an asynchronous rc_api server call
+    /// </summary>
+    void AsyncServerCall(const rc_api_request_t* pRequest, AsyncServerCallCallback fCallback, void* pCallbackData) const;
+
     class Synchronizer
     {
     public:
diff --git a/src/ui/viewmodels/IntegrationMenuViewModel.cpp b/src/ui/viewmodels/IntegrationMenuViewModel.cpp
index f3c463f2..ca830908 100644
--- a/src/ui/viewmodels/IntegrationMenuViewModel.cpp
+++ b/src/ui/viewmodels/IntegrationMenuViewModel.cpp
@@ -16,6 +16,7 @@
 #include "ui/viewmodels/GameChecksumViewModel.hh"
 #include "ui/viewmodels/LoginViewModel.hh"
 #include "ui/viewmodels/MessageBoxViewModel.hh"
+#include "ui/viewmodels/OverlayManager.hh"
 #include "ui/viewmodels/OverlaySettingsViewModel.hh"
 #include "ui/viewmodels/UnknownGameViewModel.hh"
 #include "ui/viewmodels/WindowManager.hh"
@@ -248,13 +249,11 @@ void IntegrationMenuViewModel::ToggleLeaderboards()
 
     if (!bLeaderboardsActive)
     {
-        // TODO: disallow disabling leaderboards
-        //pGameContext.DeactivateLeaderboards();
+        ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().ClearLeaderboardPopups();
         ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Leaderboards are now disabled.");
     }
     else
     {
-        //pGameContext.ActivateLeaderboards();
         ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Leaderboards are now enabled.");
     }
 
diff --git a/src/ui/viewmodels/MemoryViewerViewModel.cpp b/src/ui/viewmodels/MemoryViewerViewModel.cpp
index b44ca9e8..aeb8a075 100644
--- a/src/ui/viewmodels/MemoryViewerViewModel.cpp
+++ b/src/ui/viewmodels/MemoryViewerViewModel.cpp
@@ -658,6 +658,45 @@ void MemoryViewerViewModel::RetreatCursorPage()
     }
 }
 
+bool MemoryViewerViewModel::IncreaseCurrentValue(uint32_t nModifier)
+{
+    if (m_bReadOnly)
+        return false;
+
+    const auto nAddress = GetAddress();
+    const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
+    auto nMem = pEmulatorContext.ReadMemory(GetAddress(), GetSize());
+    auto const nMaxValue = ra::data::MemSizeMax(GetSize());
+
+    if (nMem >= nMaxValue)
+        return false;
+    if ((nMaxValue - nMem) < nModifier)
+        nModifier = (nMaxValue - nMem);
+    nMem += nModifier;
+
+    pEmulatorContext.WriteMemory(nAddress, GetSize(), nMem);
+    return true;
+}
+
+bool MemoryViewerViewModel::DecreaseCurrentValue(uint32_t nModifier)
+{
+    if (m_bReadOnly)
+        return false;
+
+    auto const nAddress = GetAddress();
+    const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
+    auto nMem = pEmulatorContext.ReadMemory(GetAddress(), GetSize());
+
+    if (nMem == 0)
+        return false;
+    if (nMem < nModifier)
+        nModifier = nMem;
+    nMem -= nModifier;
+
+    pEmulatorContext.WriteMemory(nAddress, GetSize(), nMem);
+    return true;
+}
+
 void MemoryViewerViewModel::OnActiveGameChanged()
 {
     m_nNeedsRedraw |= REDRAW_ADDRESSES;
diff --git a/src/ui/viewmodels/MemoryViewerViewModel.hh b/src/ui/viewmodels/MemoryViewerViewModel.hh
index f295118c..98a13c03 100644
--- a/src/ui/viewmodels/MemoryViewerViewModel.hh
+++ b/src/ui/viewmodels/MemoryViewerViewModel.hh
@@ -175,6 +175,9 @@ public:
     void AdvanceCursorPage();
     void RetreatCursorPage();
 
+    bool IncreaseCurrentValue(uint32_t nModifier);
+    bool DecreaseCurrentValue(uint32_t nModifier);
+
 protected:
     void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
 
diff --git a/src/ui/viewmodels/OverlayManager.cpp b/src/ui/viewmodels/OverlayManager.cpp
index ee763983..60c3d150 100644
--- a/src/ui/viewmodels/OverlayManager.cpp
+++ b/src/ui/viewmodels/OverlayManager.cpp
@@ -405,6 +405,21 @@ void OverlayManager::ClearPopups()
     RequestRender();
 }
 
+void OverlayManager::ClearLeaderboardPopups()
+{
+    {
+        std::lock_guard<std::mutex> pGuard(m_pPopupQueueMutex);
+
+        for (auto& pScoreboard : m_vScoreboards)
+            pScoreboard.SetDestroyPending();
+
+        for (auto& pTracker : m_vScoreTrackers)
+            pTracker->SetDestroyPending();
+    }
+
+    RequestRender();
+}
+
 ra::ui::Position OverlayManager::GetRenderLocation(const ra::ui::viewmodels::PopupViewModelBase& vmPopup,
     int nX, int nY, const ra::ui::drawing::ISurface& pSurface, const PopupLocations& pPopupLocations)
 {
diff --git a/src/ui/viewmodels/OverlayManager.hh b/src/ui/viewmodels/OverlayManager.hh
index c0ee2a04..b1323069 100644
--- a/src/ui/viewmodels/OverlayManager.hh
+++ b/src/ui/viewmodels/OverlayManager.hh
@@ -288,6 +288,11 @@ public:
     /// </summary>
     virtual void ClearPopups();
 
+    /// <summary>
+    /// Clear leaderboard popups.
+    /// </summary>
+    void ClearLeaderboardPopups();
+
     /// <summary>
     /// Sets the function to call when there's something to be rendered.
     /// </summary>
diff --git a/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp b/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
index b49d972e..7229e2ad 100644
--- a/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
+++ b/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
@@ -8,11 +8,12 @@
 #include "data\context\SessionTracker.hh"
 #include "data\context\UserContext.hh"
 
-#include "services\Http.hh"
+#include "services\AchievementRuntime.hh"
 #include "services\IConfiguration.hh"
 #include "services\ILocalStorage.hh"
 #include "services\IThreadPool.hh"
 
+#include <rcheevos\include\rc_api_info.h>
 #include <rcheevos\include\rc_api_runtime.h>
 
 namespace ra {
@@ -67,18 +68,14 @@ void OverlayRecentGamesPageViewModel::Refresh()
         ra::services::ServiceLocator::GetMutable<ra::services::IThreadPool>().RunAsync(
             [this, vPendingGames = std::move(vPendingGames)]()
         {
-            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();
-
+            bool bAnyMissing = false;
             auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
             for (const auto nGameId : vPendingGames)
             {
                 auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId));
                 if (pData != nullptr)
                 {
+                    // found patchdata. extract title and badge
                     std::string sContents = "{\"Success\":true,\"PatchData\":";
                     std::string sLine;
                     while (pData->GetLine(sLine))
@@ -96,33 +93,38 @@ void OverlayRecentGamesPageViewModel::Refresh()
                 }
                 else
                 {
-                    api_params.game_id = nGameId;
-
-                    rc_api_request_t request;
-                    if (rc_api_init_fetch_game_data_request(&request, &api_params) != RC_OK)
-                        continue;
+                    bAnyMissing = true;
+                }
+            }
 
-                    ra::services::Http::Request httpRequest(request.url);
-                    httpRequest.SetContentType(request.content_type);
-                    httpRequest.SetPostData(request.post_data);
+            if (bAnyMissing)
+            {
+                rc_api_fetch_game_titles_request_t api_params;
+                memset(&api_params, 0, sizeof(api_params));
+                api_params.game_ids = &vPendingGames.at(0);
+                api_params.num_game_ids = gsl::narrow_cast<uint32_t>(vPendingGames.size());
 
-                    httpRequest.CallAsync([this, nGameId](const ra::services::Http::Response& pResponse)
-                    {
-                        rc_api_fetch_game_data_response_t response{};
-                        rc_api_server_response_t server_response;
+                rc_api_request_t request;
+                if (rc_api_init_fetch_game_titles_request(&request, &api_params) != RC_OK)
+                    return;
 
-                        memset(&server_response, 0, sizeof(server_response));
-                        server_response.body = pResponse.Content().c_str();
-                        server_response.body_length = pResponse.Content().length();
-                        server_response.http_status_code = ra::etoi(pResponse.StatusCode());
+                ra::services::ServiceLocator::Get<ra::services::AchievementRuntime>().AsyncServerCall(
+                    &request, [](const rc_api_server_response_t& server_response, void* pCallbackData) {
+                        rc_api_fetch_game_titles_response_t response{};
 
-                        if (rc_api_process_fetch_game_data_server_response(&response, &server_response) == RC_OK &&
+                        if (rc_api_process_fetch_game_titles_server_response(&response, &server_response) == RC_OK &&
                             response.response.succeeded)
                         {
-                            UpdateGameEntry(nGameId, ra::Widen(response.title), response.image_name);
+                            auto* pThis = static_cast<OverlayRecentGamesPageViewModel*>(pCallbackData);
+                            Expects(pThis != nullptr);
+
+                            for (uint32_t i = 0; i < response.num_entries; i++)
+                            {
+                                pThis->UpdateGameEntry(response.entries[i].id, ra::Widen(response.entries[i].title),
+                                                       response.entries[i].image_name);
+                            }
                         }
-                    });
-                }
+                    }, this);
             }
         });
     }
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 5d3253c6..6ca6b2e1 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -140,6 +140,14 @@ void TriggerConditionViewModel::SerializeAppend(std::string& sBuffer) const
                 sBuffer.push_back('%');
                 break;
 
+            case TriggerOperatorType::Add:
+                sBuffer.push_back('+');
+                break;
+
+            case TriggerOperatorType::Subtract:
+                sBuffer.push_back('-');
+                break;
+
             default:
                 assert(!"Unknown comparison");
                 break;
@@ -337,7 +345,6 @@ static std::wstring FormatTypedValue(rc_typed_value_t& pValue, TriggerOperandTyp
         default:
             rc_typed_value_convert(&pValue, RC_VALUE_TYPE_UNSIGNED);
             return ra::Widen(ra::ByteAddressToString(pValue.value.u32));
-            break;
     }
 }
 
@@ -520,6 +527,8 @@ static constexpr bool IsModifyingOperator(TriggerOperatorType nType)
         case TriggerOperatorType::BitwiseAnd:
         case TriggerOperatorType::BitwiseXor:
         case TriggerOperatorType::Modulus:
+        case TriggerOperatorType::Add:
+        case TriggerOperatorType::Subtract:
             return true;
 
         default:
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index 4e0052ff..dac7af23 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -57,7 +57,9 @@ enum class TriggerOperatorType : uint8_t
     Divide = RC_OPERATOR_DIV,
     BitwiseAnd = RC_OPERATOR_AND,
     BitwiseXor = RC_OPERATOR_XOR,
-    Modulus = RC_OPERATOR_MOD
+    Modulus = RC_OPERATOR_MOD,
+    Add = RC_OPERATOR_ADD,
+    Subtract = RC_OPERATOR_SUB
 };
 
 class TriggerConditionViewModel : public ViewModelBase
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index a5e8e583..ae9eab7c 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -85,6 +85,8 @@ TriggerViewModel::TriggerViewModel() noexcept
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Multiply), L"*");
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Divide), L"/");
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Modulus), L"%");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Add), L"+");
+    m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::Subtract), L"-");
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::BitwiseAnd), L"&");
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::BitwiseXor), L"^");
 }
diff --git a/src/ui/win32/bindings/MemoryViewerControlBinding.cpp b/src/ui/win32/bindings/MemoryViewerControlBinding.cpp
index 9cf8128d..052f13f1 100644
--- a/src/ui/win32/bindings/MemoryViewerControlBinding.cpp
+++ b/src/ui/win32/bindings/MemoryViewerControlBinding.cpp
@@ -139,6 +139,8 @@ bool MemoryViewerControlBinding::OnKeyDown(UINT nChar)
 
     if (!m_pViewModel.IsAddressFixed())
         bHandled = HandleNavigation(nChar);
+    if (!bHandled)
+        bHandled = HandleShortcut(nChar);
 
     m_bSuppressMemoryViewerInvalidate = false;
 
@@ -236,6 +238,36 @@ bool MemoryViewerControlBinding::HandleNavigation(UINT nChar)
     }
 }
 
+bool MemoryViewerControlBinding::HandleShortcut(UINT nChar)
+{
+    const bool bShiftHeld = (GetKeyState(VK_SHIFT) < 0);
+    const bool bControlHeld = (GetKeyState(VK_CONTROL) < 0);
+
+    switch (nChar)
+    {
+        // Increment/Decrement value Shortcuts
+        case VK_ADD:
+        case VK_SUBTRACT:
+        {
+            auto nModifier = 1;
+
+            // Increase/decrease by 1 or 2 on the high and lower nibble depending key pressed
+            if (bControlHeld)
+                nModifier *= 2;
+            if (bShiftHeld)
+                nModifier *= 16;
+
+            if (nChar == VK_ADD)
+                return m_pViewModel.IncreaseCurrentValue(nModifier);
+            else
+                return m_pViewModel.DecreaseCurrentValue(nModifier);
+        }
+
+        default:
+            return false;
+    }
+}
+
 bool MemoryViewerControlBinding::OnEditInput(UINT c)
 {
     // multiple properties may change while typing, we'll do a single Invalidate after we're done
diff --git a/src/ui/win32/bindings/MemoryViewerControlBinding.hh b/src/ui/win32/bindings/MemoryViewerControlBinding.hh
index 627a0ba5..84f8ea8d 100644
--- a/src/ui/win32/bindings/MemoryViewerControlBinding.hh
+++ b/src/ui/win32/bindings/MemoryViewerControlBinding.hh
@@ -63,6 +63,7 @@ protected:
 
 private:
     bool HandleNavigation(UINT nChar);
+    bool HandleShortcut(UINT nChar);
     bool m_bSuppressMemoryViewerInvalidate = false;
 
     ra::ui::viewmodels::MemoryViewerViewModel& m_pViewModel;


Test patch:
diff --git a/tests/services/AchievementRuntime_Tests.cpp b/tests/services/AchievementRuntime_Tests.cpp
index 9c8b05e4..adc151e8 100644
--- a/tests/services/AchievementRuntime_Tests.cpp
+++ b/tests/services/AchievementRuntime_Tests.cpp
@@ -2336,6 +2336,7 @@ public:
         AchievementRuntimeHarness runtime;
         auto *pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted,
                                                    ra::ui::viewmodels::PopupLocation::BottomLeft);
 
@@ -2361,6 +2362,7 @@ public:
         AchievementRuntimeHarness runtime;
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted,
                                                    ra::ui::viewmodels::PopupLocation::None);
 
@@ -2374,11 +2376,31 @@ public:
         Assert::IsNull(pPopup);
     }
 
+    TEST_METHOD(TestHandleLeaderboardStartedEventDisabled)
+    {
+        AchievementRuntimeHarness runtime;
+        auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
+        runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
+        runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted,
+                                                   ra::ui::viewmodels::PopupLocation::BottomLeft);
+
+        rc_client_event_t event;
+        memset(&event, 0, sizeof(event));
+        event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED;
+        event.leaderboard = &pLbd4->public_;
+        runtime.RaiseEvent(event);
+
+        auto* pPopup = runtime.mockOverlayManager.GetMessage(1);
+        Assert::IsNull(pPopup);
+    }
+
     TEST_METHOD(TestHandleLeaderboardFailedEvent)
     {
         AchievementRuntimeHarness runtime;
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled,
                                                    ra::ui::viewmodels::PopupLocation::BottomLeft);
 
@@ -2404,6 +2426,7 @@ public:
         AchievementRuntimeHarness runtime;
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled,
                                                    ra::ui::viewmodels::PopupLocation::None);
 
@@ -2417,6 +2440,25 @@ public:
         Assert::IsNull(pPopup);
     }
 
+    TEST_METHOD(TestHandleLeaderboardFailedEventDisabled)
+    {
+        AchievementRuntimeHarness runtime;
+        auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
+        runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
+        runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled,
+                                                   ra::ui::viewmodels::PopupLocation::BottomLeft);
+
+        rc_client_event_t event;
+        memset(&event, 0, sizeof(event));
+        event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED;
+        event.leaderboard = &pLbd4->public_;
+        runtime.RaiseEvent(event);
+
+        auto* pPopup = runtime.mockOverlayManager.GetMessage(1);
+        Assert::IsNull(pPopup);
+    }
+
     TEST_METHOD(TestHandleLeaderboardSubmittedEvent)
     {
         AchievementRuntimeHarness runtime;
@@ -2424,6 +2466,7 @@ public:
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2465,6 +2508,7 @@ public:
         auto* pLbd4 = runtime.MockLocalLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2495,6 +2539,7 @@ public:
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2525,6 +2570,7 @@ public:
         pLbd4->public_.tracker_value = "1:23.45";
         auto* vmLbd4 = runtime.WrapLeaderboard(pLbd4);
         vmLbd4->SetDescription(L"Modified Description");
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2556,6 +2602,7 @@ public:
         auto* vmLbd4 = runtime.WrapLeaderboard(pLbd4);
         vmLbd4->SetDescription(L"Modified Description");
         vmLbd4->UpdateLocalCheckpoint();
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2586,6 +2633,7 @@ public:
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
         runtime.mockEmulatorContext.MockMemoryModified(true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2615,6 +2663,7 @@ public:
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2645,6 +2694,7 @@ public:
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
         runtime.mockEmulatorContext.MockMemoryInsecure(true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2675,6 +2725,7 @@ public:
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         pLbd4->public_.tracker_value = "1:23.45";
         runtime.WrapLeaderboard(pLbd4);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2701,6 +2752,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
 
@@ -2725,6 +2777,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
                                                    ra::ui::viewmodels::PopupLocation::None);
 
@@ -2743,10 +2796,34 @@ public:
         Assert::IsNull(pPopup);
     }
 
+    TEST_METHOD(TestHandleLeaderboardTrackerShowEventDisabled)
+    {
+        AchievementRuntimeHarness runtime;
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
+        runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
+                                                   ra::ui::viewmodels::PopupLocation::BottomRight);
+
+        rc_client_leaderboard_tracker_t tracker;
+        memset(&tracker, 0, sizeof(tracker));
+        snprintf(tracker.display, sizeof(tracker.display), "1:23.45");
+        tracker.id = 1;
+
+        rc_client_event_t event;
+        memset(&event, 0, sizeof(event));
+        event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW;
+        event.leaderboard_tracker = &tracker;
+        runtime.RaiseEvent(event);
+
+        auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1);
+        Assert::IsNull(pPopup);
+    }
+
     TEST_METHOD(TestHandleLeaderboardTrackerUpdateEvent)
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
         runtime.mockOverlayManager.AddScoreTracker(1U).SetDisplayText(L"XXX");
@@ -2772,6 +2849,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
                                                    ra::ui::viewmodels::PopupLocation::None);
 
@@ -2790,10 +2868,34 @@ public:
         Assert::IsNull(pPopup);
     }
 
+    TEST_METHOD(TestHandleLeaderboardTrackerUpdateEventDisabled)
+    {
+        AchievementRuntimeHarness runtime;
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
+        runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
+                                                   ra::ui::viewmodels::PopupLocation::BottomRight);
+
+        rc_client_leaderboard_tracker_t tracker;
+        memset(&tracker, 0, sizeof(tracker));
+        snprintf(tracker.display, sizeof(tracker.display), "1:23.45");
+        tracker.id = 1;
+
+        rc_client_event_t event;
+        memset(&event, 0, sizeof(event));
+        event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE;
+        event.leaderboard_tracker = &tracker;
+        runtime.RaiseEvent(event);
+
+        auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1);
+        Assert::IsNull(pPopup);
+    }
+
     TEST_METHOD(TestHandleLeaderboardTrackerHideEvent)
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
         runtime.mockOverlayManager.AddScoreTracker(1U).SetDisplayText(L"XXX");
@@ -2817,6 +2919,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
@@ -2883,6 +2986,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::None);
@@ -2918,11 +3022,53 @@ public:
         auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(4U);
         Assert::IsNull(pScoreboard);
     }
+    
+    TEST_METHOD(TestHandleLeaderboardScoreboardEventDisabled)
+    {
+        AchievementRuntimeHarness runtime;
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
+        auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
+        runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
+                                                   ra::ui::viewmodels::PopupLocation::BottomRight);
+
+        rc_client_leaderboard_scoreboard_entry_t entries[3];
+        memset(entries, 0, sizeof(entries));
+        rc_client_leaderboard_scoreboard_t scoreboard;
+        memset(&scoreboard, 0, sizeof(scoreboard));
+        scoreboard.leaderboard_id = 4U;
+        scoreboard.new_rank = 2;
+        snprintf(scoreboard.best_score, sizeof(scoreboard.best_score), "1:20.00");
+        snprintf(scoreboard.submitted_score, sizeof(scoreboard.submitted_score), "1:23.45");
+        scoreboard.num_entries = 3;
+        scoreboard.num_top_entries = 3;
+        scoreboard.top_entries = entries;
+        entries[0].rank = 1;
+        snprintf(entries[0].score, sizeof(entries[0].score), "1:19.55");
+        entries[0].username = "Bob";
+        entries[1].rank = 2;
+        snprintf(entries[1].score, sizeof(entries[1].score), "1:20.00");
+        entries[1].username = "User_";
+        entries[2].rank = 3;
+        snprintf(entries[2].score, sizeof(entries[2].score), "1:20.12");
+        entries[2].username = "Tara";
+
+        rc_client_event_t event;
+        memset(&event, 0, sizeof(event));
+        event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD;
+        event.leaderboard = &pLbd4->public_;
+        event.leaderboard_scoreboard = &scoreboard;
+        runtime.RaiseEvent(event);
+
+        auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(4U);
+        Assert::IsNull(pScoreboard);
+    }
 
     TEST_METHOD(TestHandleLeaderboardScoreboardEventPlayerNotInTopEntries)
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard,
                                                    ra::ui::viewmodels::PopupLocation::BottomRight);
@@ -3029,6 +3175,7 @@ public:
     {
         AchievementRuntimeHarness runtime;
         runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         auto* pLeaderboard = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001");
         runtime.WrapLeaderboard(pLeaderboard);
 
diff --git a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp
index 3593c792..8809f23b 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\MockOverlayManager.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
 #include "tests\mocks\MockUserContext.hh"
@@ -379,6 +380,7 @@ public:
     TEST_METHOD(TestToggleLeaderboardsEnabled)
     {
         IntegrationMenuViewModelHarness menu;
+        ra::ui::viewmodels::mocks::MockOverlayManager overlayManager;
         menu.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, false);
         bool bMenuRebuilt = false;
         menu.mockEmulatorContext.SetRebuildMenuFunction([&bMenuRebuilt]() { bMenuRebuilt = true;  });
@@ -401,6 +403,7 @@ public:
     TEST_METHOD(TestToggleLeaderboardsDisabled)
     {
         IntegrationMenuViewModelHarness menu;
+        ra::ui::viewmodels::mocks::MockOverlayManager overlayManager;
         menu.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true);
         bool bMenuRebuilt = false;
         menu.mockEmulatorContext.SetRebuildMenuFunction([&bMenuRebuilt]() { bMenuRebuilt = true;  });
diff --git a/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp b/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp
index b900ee33..bfb7d980 100644
--- a/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/MemoryViewerViewModel_Tests.cpp
@@ -1918,6 +1918,168 @@ public:
         Assert::IsTrue(viewer.NeedsRedraw());
         viewer.MockRender();
     }
+
+    TEST_METHOD(TestIncreaseCurrentValueByOne)
+    {
+        MemoryViewerViewModelHarness viewer;
+        viewer.InitializeMemory(256);
+
+        // ignore if readonly
+        Assert::IsTrue(viewer.IsReadOnly());
+        Assert::IsFalse(viewer.IncreaseCurrentValue(1));
+
+        viewer.SetReadOnly(false);
+        Assert::IsFalse(viewer.IsReadOnly());
+
+        viewer.SetAddress(0x0U);
+        Assert::AreEqual({ 0U }, viewer.GetAddress());
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 1 from 0x0 should result to 0X1
+        Assert::IsTrue(viewer.IncreaseCurrentValue(1));
+        Assert::AreEqual({ 0x1U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 1 from 0xFE should result to 0XFF
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0XFEU);
+        Assert::IsTrue(viewer.IncreaseCurrentValue(1));
+        Assert::AreEqual({ 0xFFU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 1 from 0xFF should not change the value
+        Assert::IsFalse(viewer.IncreaseCurrentValue(1));
+        Assert::AreEqual({ 0xFFU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 1 from 0x0 should result to 0X0001
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0X0U);
+        Assert::IsTrue(viewer.IncreaseCurrentValue(1));
+        Assert::AreEqual({ 0x1U }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+
+        // Increase by 1 from 0xFFFF should not change the value
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0XFFFFU);
+        Assert::IsFalse(viewer.IncreaseCurrentValue(1));
+        Assert::AreEqual({ 0xFFFFU }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+    }
+
+    TEST_METHOD(TestIncreaseCurrentValueBySixTeen)
+    {
+        MemoryViewerViewModelHarness viewer;
+        viewer.InitializeMemory(256);
+
+        // ignore if readonly
+        Assert::IsTrue(viewer.IsReadOnly());
+        Assert::IsFalse(viewer.IncreaseCurrentValue(16));
+
+        viewer.SetReadOnly(false);
+        Assert::IsFalse(viewer.IsReadOnly());
+
+        viewer.SetAddress(0x0U);
+        Assert::AreEqual({ 0U }, viewer.GetAddress());
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 16 from 0x0 should result to 0X10
+        Assert::IsTrue(viewer.IncreaseCurrentValue(16));
+        Assert::AreEqual({ 0x10U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 16 from 0xFE should result to 0XFF
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0XFEU);
+        Assert::IsTrue(viewer.IncreaseCurrentValue(16));
+        Assert::AreEqual({ 0xFFU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 16 from 0xFF should not change the value
+        Assert::IsFalse(viewer.IncreaseCurrentValue(16));
+        Assert::AreEqual({ 0xFFU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Increase by 16 from 0x0 should result to 0X0010
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0X0U);
+        Assert::IsTrue(viewer.IncreaseCurrentValue(16));
+        Assert::AreEqual({ 0x10U }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+
+        // Increase by 16 from 0xFFFF should not change the value
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0XFFFFU);
+        Assert::IsFalse(viewer.IncreaseCurrentValue(16));
+        Assert::AreEqual({ 0xFFFFU }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+    }
+
+    TEST_METHOD(TestDecreaseCurrentValueByOne)
+    {
+        MemoryViewerViewModelHarness viewer;
+        viewer.InitializeMemory(256);
+
+        // ignore if readonly
+        Assert::IsTrue(viewer.IsReadOnly());
+        Assert::IsFalse(viewer.DecreaseCurrentValue(1));
+
+        viewer.SetReadOnly(false);
+        Assert::IsFalse(viewer.IsReadOnly());
+
+        viewer.SetAddress(0x0U);
+        Assert::AreEqual({ 0U }, viewer.GetAddress());
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 1 from 0x0 should not change the value
+        Assert::IsFalse(viewer.DecreaseCurrentValue(1));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 1 from 0xFF should result to 0XFE
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0XFFU);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(1));
+        Assert::AreEqual({ 0xFEU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 1 from 0x1 should result to 0X0
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0X1U);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(1));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 1 from 0x0 should not change the value
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0X0U);
+        Assert::IsFalse(viewer.DecreaseCurrentValue(1));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+
+        // Decrease by 1 from 0xFFFF should result to 0XFFFE
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0XFFFFU);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(1));
+        Assert::AreEqual({ 0xFFFEU }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+    }
+
+    TEST_METHOD(TestDecreaseCurrentValueBySixteen)
+    {
+        MemoryViewerViewModelHarness viewer;
+        viewer.InitializeMemory(256);
+
+        // ignore if readonly
+        Assert::IsTrue(viewer.IsReadOnly());
+        Assert::IsFalse(viewer.DecreaseCurrentValue(16));
+
+        viewer.SetReadOnly(false);
+        Assert::IsFalse(viewer.IsReadOnly());
+
+        viewer.SetAddress(0x0U);
+        Assert::AreEqual({ 0U }, viewer.GetAddress());
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 16 from 0x0 should not change the value
+        Assert::IsFalse(viewer.DecreaseCurrentValue(16));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 16 from 0xFF should result to 0XEF
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0XFFU);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(16));
+        Assert::AreEqual({ 0xEFU }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 16 from 0x1 should result to 0X0
+        viewer.mockEmulatorContext.WriteMemoryByte(0x0U, 0X1U);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(16));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemoryByte(0U));
+
+        // Decrease by 16 from 0x0 should not change the value
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0X0U);
+        Assert::IsFalse(viewer.DecreaseCurrentValue(16));
+        Assert::AreEqual({ 0x0U }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+
+        // Decrease by 16 from 0xFFFF should result to 0XFFEF
+        viewer.mockEmulatorContext.WriteMemory(0x0U, MemSize::SixteenBit, 0XFFFFU);
+        Assert::IsTrue(viewer.DecreaseCurrentValue(16));
+        Assert::AreEqual({ 0xFFEFU }, viewer.mockEmulatorContext.ReadMemory(0U, MemSize::SixteenBit));
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
index 66cfa328..3b71423e 100644
--- a/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayRecentGamesPageViewModel_Tests.cpp
@@ -2,6 +2,7 @@
 
 #include "ui\viewmodels\OverlayRecentGamesPageViewModel.hh"
 
+#include "tests\mocks\MockAchievementRuntime.hh"
 #include "tests\mocks\MockConfiguration.hh"
 #include "tests\mocks\MockHttpRequester.hh"
 #include "tests\mocks\MockImageRepository.hh"
@@ -94,20 +95,15 @@ public:
     TEST_METHOD(TestRefreshOneGameDataFetched)
     {
         OverlayRecentGamesPageViewModelHarness gamesPage;
+        ra::services::mocks::MockAchievementRuntime achievementRuntime;
         gamesPage.mockSessions.MockSession(3U, 1234567890U, std::chrono::seconds(5000));
         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\":\"\""
-                                                "}}");
+                                                "{\"Success\":true,\"Response\":["
+                                                    "{\"ID\":3,\"Title\":\"Game Name\",\"ImageIcon\":\"/Images/BADGE.png\"}"
+                                                "]}");
         });
 
         gamesPage.Refresh();
diff --git a/tests/ui/viewmodels/TriggerConditionAsserts.hh b/tests/ui/viewmodels/TriggerConditionAsserts.hh
index be538551..9d7f942a 100644
--- a/tests/ui/viewmodels/TriggerConditionAsserts.hh
+++ b/tests/ui/viewmodels/TriggerConditionAsserts.hh
@@ -105,6 +105,10 @@ std::wstring ToString<ra::ui::viewmodels::TriggerOperatorType>(
             return L"BitwiseAnd";
         case ra::ui::viewmodels::TriggerOperatorType::Modulus:
             return L"Modulus";
+        case ra::ui::viewmodels::TriggerOperatorType::Add:
+            return L"Add";
+        case ra::ui::viewmodels::TriggerOperatorType::Subtract:
+            return L"Subtract";
         default:
             return std::to_wstring(static_cast<int>(nConditionType));
     }
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index 75d9ec96..70175b64 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -193,6 +193,14 @@ public:
         vmCondition.SetOperator(TriggerOperatorType::Modulus);
         Assert::AreEqual(TriggerOperatorType::Modulus, vmCondition.GetOperator());
         Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Add);
+        Assert::AreEqual(TriggerOperatorType::Add, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Subtract);
+        Assert::AreEqual(TriggerOperatorType::Subtract, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
     }
 
     TEST_METHOD(TestHasTargetSize)
@@ -491,6 +499,9 @@ public:
         ParseAndRegenerate("A:0xH1234"); // none
         ParseAndRegenerate("A:0xH1234*5"); // multiply
         ParseAndRegenerate("A:0xH1234/5"); // divide
+        ParseAndRegenerate("A:0xH1234%5"); // modulus
+        ParseAndRegenerate("A:0xH1234+5"); // addition
+        ParseAndRegenerate("A:0xH1234-5"); // subtraction
         ParseAndRegenerate("A:0xH1234&5"); // bitwise and
     }
 
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 85a127e3..0f112a88 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -214,7 +214,7 @@ public:
         Assert::AreEqual((int)MemSize::MBF32LE, vmTrigger.OperandSizes().GetItemAt(23)->GetId());
         Assert::AreEqual(std::wstring(L"MBF32 LE"), vmTrigger.OperandSizes().GetItemAt(23)->GetLabel());
 
-        Assert::AreEqual({ 12U }, vmTrigger.OperatorTypes().Count());
+        Assert::AreEqual({ 14U }, vmTrigger.OperatorTypes().Count());
         Assert::AreEqual((int)TriggerOperatorType::Equals, vmTrigger.OperatorTypes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L"="), vmTrigger.OperatorTypes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)TriggerOperatorType::LessThan, vmTrigger.OperatorTypes().GetItemAt(1)->GetId());
@@ -235,10 +235,14 @@ public:
         Assert::AreEqual(std::wstring(L"/"), vmTrigger.OperatorTypes().GetItemAt(8)->GetLabel());
         Assert::AreEqual((int)TriggerOperatorType::Modulus, vmTrigger.OperatorTypes().GetItemAt(9)->GetId());
         Assert::AreEqual(std::wstring(L"%"), vmTrigger.OperatorTypes().GetItemAt(9)->GetLabel());
-        Assert::AreEqual((int)TriggerOperatorType::BitwiseAnd, vmTrigger.OperatorTypes().GetItemAt(10)->GetId());
-        Assert::AreEqual(std::wstring(L"&"), vmTrigger.OperatorTypes().GetItemAt(10)->GetLabel());
-        Assert::AreEqual((int)TriggerOperatorType::BitwiseXor, vmTrigger.OperatorTypes().GetItemAt(11)->GetId());
-        Assert::AreEqual(std::wstring(L"^"), vmTrigger.OperatorTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::Add, vmTrigger.OperatorTypes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"+"), vmTrigger.OperatorTypes().GetItemAt(10)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::Subtract, vmTrigger.OperatorTypes().GetItemAt(11)->GetId());
+        Assert::AreEqual(std::wstring(L"-"), vmTrigger.OperatorTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::BitwiseAnd, vmTrigger.OperatorTypes().GetItemAt(12)->GetId());
+        Assert::AreEqual(std::wstring(L"&"), vmTrigger.OperatorTypes().GetItemAt(12)->GetLabel());
+        Assert::AreEqual((int)TriggerOperatorType::BitwiseXor, vmTrigger.OperatorTypes().GetItemAt(13)->GetId());
+        Assert::AreEqual(std::wstring(L"^"), vmTrigger.OperatorTypes().GetItemAt(13)->GetLabel());
     }
 
     TEST_METHOD(TestParseAndRegenerateCoreOnly)