Repository: RetroAchievements/RAIntegration
Pull request #1085 (2024-04-24 01:20:30)

Add +/- shortcuts on the memory viewer
Enhanced user experience by providing a way to increment or decrement a value with +/- directly in the memory view.


Patch:
diff --git a/rcheevos b/rcheevos
index bfa0d313..47b0bcdd 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit bfa0d3138865ae2a649a0beadfa4206408a14690
+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/RA_Resource.h b/src/RA_Resource.h
index 032dadbe..643da9bd 100644
--- a/src/RA_Resource.h
+++ b/src/RA_Resource.h
@@ -147,6 +147,7 @@
 #define IDC_RA_MEMVIEW_32BITBE          1242
 #define IDC_RA_LBL_TYPE                 1243
 #define IDC_RA_TYPE                     1244
+#define IDC_RA_RESULTS_IMPORT           1245
 
 
 #define IDD_RA_MEMORY                   1501
@@ -215,7 +216,7 @@
 #ifndef APSTUDIO_READONLY_SYMBOLS
 #define _APS_NEXT_RESOURCE_VALUE        122
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1243
+#define _APS_NEXT_CONTROL_VALUE         1246
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 465aa6f2..965c010a 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -51,7 +51,8 @@ BEGIN
     PUSHBUTTON      ">>",IDC_RA_RESULTS_FORWARD,58,78,20,14
     PUSHBUTTON      "E&xclude Selected",IDC_RA_RESULTS_REMOVE,8,95,70,14
     PUSHBUTTON      "Book&mark Selected",IDC_RA_RESULTS_BOOKMARK,8,111,70,14
-    PUSHBUTTON      "&Export",IDC_RA_RESULTS_EXPORT,8,127,70,14
+    PUSHBUTTON      "&Export",IDC_RA_RESULTS_EXPORT,8,127,35,14
+    PUSHBUTTON      "&Import",IDC_RA_RESULTS_IMPORT,43,127,35,14
     CONTROL         "",IDC_RA_RESULTS,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_OWNERDATA | LVS_NOCOLUMNHEADER | WS_BORDER | WS_VSCROLL | WS_TABSTOP,82,55,242,86
     GROUPBOX        "Code Notes",IDC_RA_GBX_NOTES,4,145,324,55
     LTEXT           "&Address:",IDC_STATIC,8,155,32,9
diff --git a/src/data/Types.hh b/src/data/Types.hh
index d343bf31..ce087919 100644
--- a/src/data/Types.hh
+++ b/src/data/Types.hh
@@ -86,6 +86,23 @@ enum class ValueFormat : uint8_t
 const char* ValueFormatToString(ValueFormat nFormat) noexcept;
 ValueFormat ValueFormatFromString(const std::string& sFormat) noexcept;
 
+constexpr bool MemSizeIsFloat(MemSize nSize)
+{
+    switch (nSize)
+    {
+        case MemSize::Float:
+        case MemSize::FloatBigEndian:
+        case MemSize::MBF32:
+        case MemSize::MBF32LE:
+        case MemSize::Double32:
+        case MemSize::Double32BigEndian:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
 constexpr unsigned int MemSizeBits(MemSize nSize)
 {
     switch (nSize)
@@ -96,6 +113,8 @@ constexpr unsigned int MemSizeBits(MemSize nSize)
         case MemSize::FloatBigEndian:
         case MemSize::MBF32:
         case MemSize::MBF32LE:
+        case MemSize::Double32:
+        case MemSize::Double32BigEndian:
             return 32;
 
         case MemSize::EightBit:
diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp
index 89dc5f81..bbed3ec4 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);
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/services/AchievementRuntimeExports.cpp b/src/services/AchievementRuntimeExports.cpp
index ddcd3e14..f9cd4cf7 100644
--- a/src/services/AchievementRuntimeExports.cpp
+++ b/src/services/AchievementRuntimeExports.cpp
@@ -129,16 +129,15 @@ public:
         return pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
     }
 
-    static void set_unofficial_enabled(int value)
+    static void set_unofficial_enabled(int) noexcept
     {
-        auto& pClient = ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>();
-        rc_client_set_unofficial_enabled(pClient.GetClient(), value);
+        // do nothing. unofficial achievements should always be available when using the toolkit.
     }
 
-    static int get_unofficial_enabled()
+    static int get_unofficial_enabled() noexcept
     {
-        const auto& pClient = ra::services::ServiceLocator::Get<ra::services::AchievementRuntime>();
-        return rc_client_get_unofficial_enabled(pClient.GetClient());
+        // unofficial achievements should always be available when using the toolkit.
+        return true;
     }
 
     static void set_encore_mode_enabled(int value)
diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp
index c78f626c..fb745fe9 100644
--- a/src/services/SearchResults.cpp
+++ b/src/services/SearchResults.cpp
@@ -576,6 +576,7 @@ protected:
         return srResults.m_vBlocks.emplace_back(nAddress, nSize, nMaxAddresses);
     }
 
+public:
     void AddBlocks(SearchResults& srNew, std::vector<ra::ByteAddress>& vMatches,
         std::vector<unsigned char>& vMemory, ra::ByteAddress nPreviousBlockFirstAddress, unsigned int nPadding) const
     {
@@ -1754,78 +1755,63 @@ ra::ByteAddress MemBlock::GetMatchingAddress(gsl::index nIndex) const noexcept
 
 _CONSTANT_VAR MAX_BLOCK_SIZE = 256U * 1024; // 256K
 
-void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchType nType)
+static impl::SearchImpl* GetSearchImpl(SearchType nType) noexcept
 {
-    m_nType = nType;
-
-    const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
-    const auto nTotalMemorySize = pEmulatorContext.TotalMemorySize();
-    if (nAddress > nTotalMemorySize)
-        nAddress = 0;
-    if (nBytes + nAddress > nTotalMemorySize)
-        nBytes = nTotalMemorySize - nAddress;
-
     switch (nType)
     {
         case SearchType::FourBit:
-            m_pImpl = &ra::services::impl::s_pFourBitSearchImpl;
-            break;
+            return &ra::services::impl::s_pFourBitSearchImpl;
         default:
         case SearchType::EightBit:
-            m_pImpl = &ra::services::impl::s_pEightBitSearchImpl;
-            break;
+            return &ra::services::impl::s_pEightBitSearchImpl;
         case SearchType::SixteenBit:
-            m_pImpl = &ra::services::impl::s_pSixteenBitSearchImpl;
-            break;
+            return &ra::services::impl::s_pSixteenBitSearchImpl;
         case SearchType::TwentyFourBit:
-            m_pImpl = &ra::services::impl::s_pTwentyFourBitSearchImpl;
-            break;
+            return &ra::services::impl::s_pTwentyFourBitSearchImpl;
         case SearchType::ThirtyTwoBit:
-            m_pImpl = &ra::services::impl::s_pThirtyTwoBitSearchImpl;
-            break;
+            return &ra::services::impl::s_pThirtyTwoBitSearchImpl;
         case SearchType::SixteenBitAligned:
-            m_pImpl = &ra::services::impl::s_pSixteenBitAlignedSearchImpl;
-            break;
+            return &ra::services::impl::s_pSixteenBitAlignedSearchImpl;
         case SearchType::ThirtyTwoBitAligned:
-            m_pImpl = &ra::services::impl::s_pThirtyTwoBitAlignedSearchImpl;
-            break;
+            return &ra::services::impl::s_pThirtyTwoBitAlignedSearchImpl;
         case SearchType::SixteenBitBigEndian:
-            m_pImpl = &ra::services::impl::s_pSixteenBitBigEndianSearchImpl;
-            break;
+            return &ra::services::impl::s_pSixteenBitBigEndianSearchImpl;
         case SearchType::ThirtyTwoBitBigEndian:
-            m_pImpl = &ra::services::impl::s_pThirtyTwoBitBigEndianSearchImpl;
-            break;
+            return &ra::services::impl::s_pThirtyTwoBitBigEndianSearchImpl;
         case SearchType::SixteenBitBigEndianAligned:
-            m_pImpl = &ra::services::impl::s_pSixteenBitBigEndianAlignedSearchImpl;
-            break;
+            return &ra::services::impl::s_pSixteenBitBigEndianAlignedSearchImpl;
         case SearchType::ThirtyTwoBitBigEndianAligned:
-            m_pImpl = &ra::services::impl::s_pThirtyTwoBitBigEndianAlignedSearchImpl;
-            break;
+            return &ra::services::impl::s_pThirtyTwoBitBigEndianAlignedSearchImpl;
         case SearchType::BitCount:
-            m_pImpl = &ra::services::impl::s_pBitCountSearchImpl;
-            break;
+            return &ra::services::impl::s_pBitCountSearchImpl;
         case SearchType::AsciiText:
-            m_pImpl = &ra::services::impl::s_pAsciiTextSearchImpl;
-            break;
+            return &ra::services::impl::s_pAsciiTextSearchImpl;
         case SearchType::Float:
-            m_pImpl = &ra::services::impl::s_pFloatSearchImpl;
-            break;
+            return &ra::services::impl::s_pFloatSearchImpl;
         case SearchType::FloatBigEndian:
-            m_pImpl = &ra::services::impl::s_pFloatBESearchImpl;
-            break;
+            return &ra::services::impl::s_pFloatBESearchImpl;
         case SearchType::Double32:
-            m_pImpl = &ra::services::impl::s_pDouble32SearchImpl;
-            break;
+            return &ra::services::impl::s_pDouble32SearchImpl;
         case SearchType::Double32BigEndian:
-            m_pImpl = &ra::services::impl::s_pDouble32BESearchImpl;
-            break;
+            return &ra::services::impl::s_pDouble32BESearchImpl;
         case SearchType::MBF32:
-            m_pImpl = &ra::services::impl::s_pMBF32SearchImpl;
-            break;
+            return &ra::services::impl::s_pMBF32SearchImpl;
         case SearchType::MBF32LE:
-            m_pImpl = &ra::services::impl::s_pMBF32LESearchImpl;
-            break;
+            return &ra::services::impl::s_pMBF32LESearchImpl;
     }
+}
+
+void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchType nType)
+{
+    m_nType = nType;
+    m_pImpl = GetSearchImpl(nType);
+
+    const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
+    const auto nTotalMemorySize = pEmulatorContext.TotalMemorySize();
+    if (nAddress > nTotalMemorySize)
+        nAddress = 0;
+    if (nBytes + nAddress > nTotalMemorySize)
+        nBytes = nTotalMemorySize - nAddress;
 
     const unsigned int nPadding = m_pImpl->GetPadding();
     if (nPadding >= nBytes)
@@ -1846,6 +1832,86 @@ void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchTy
     }
 }
 
+_Use_decl_annotations_
+void SearchResults::Initialize(const std::vector<Result>& vResults, SearchType nType)
+{
+    m_nType = nType;
+    m_pImpl = GetSearchImpl(nType);
+
+    const auto bAligned = m_pImpl->ConvertToRealAddress(m_pImpl->ConvertFromRealAddress(7)) != 7;
+    const auto nSize = ra::data::MemSizeBytes(m_pImpl->GetMemSize());
+    bool bNeedsReversed = false;
+    switch (nType)
+    {
+        case SearchType::SixteenBitBigEndian:
+        case SearchType::SixteenBitBigEndianAligned:
+        case SearchType::ThirtyTwoBitBigEndian:
+        case SearchType::ThirtyTwoBitBigEndianAligned:
+            bNeedsReversed = true;
+            break;
+    }
+
+    auto nFirstAddress = vResults.front().nAddress;
+    auto nLastAddressPlusOne = vResults.back().nAddress + nSize;
+    if (bAligned)
+    {
+        nFirstAddress = m_pImpl->ConvertToRealAddress(m_pImpl->ConvertFromRealAddress(nFirstAddress));
+        nLastAddressPlusOne = m_pImpl->ConvertToRealAddress(m_pImpl->ConvertFromRealAddress(nLastAddressPlusOne));
+    }
+
+    const auto nMemorySize = gsl::narrow_cast<size_t>(nLastAddressPlusOne) - nFirstAddress;
+    std::vector<unsigned char> vMemory;
+    vMemory.resize(nMemorySize);
+
+    std::vector<ra::ByteAddress> vAddresses;
+    vAddresses.reserve(vResults.size());
+
+    for (const auto& pResult : vResults)
+    {
+        if (pResult.nAddress < nFirstAddress || pResult.nAddress + nSize > nLastAddressPlusOne)
+            continue;
+
+        const auto nVirtualAddress = m_pImpl->ConvertFromRealAddress(pResult.nAddress);
+
+        // ignore unaligned values for aligned types
+        if (bAligned && m_pImpl->ConvertToRealAddress(nVirtualAddress) != pResult.nAddress)
+            continue;
+
+        vAddresses.push_back(nVirtualAddress);
+
+        auto nOffset = pResult.nAddress - nFirstAddress;        
+        auto nValue = pResult.nValue;
+        if (bNeedsReversed)
+        {
+            nValue = (((nValue & 0xFF000000) >> 24) |
+                      ((nValue & 0x00FF0000) >> 8) |
+                      ((nValue & 0x0000FF00) << 8) |
+                      ((nValue & 0x000000FF) << 24));
+        }
+
+        switch (nSize)
+        {
+            case 4:
+                vMemory.at(nOffset++) = nValue & 0xFF;
+                nValue >>= 8;
+                _FALLTHROUGH;
+            case 3:
+                vMemory.at(nOffset++) = nValue & 0xFF;
+                nValue >>= 8;
+                _FALLTHROUGH;
+            case 2:
+                vMemory.at(nOffset++) = nValue & 0xFF;
+                nValue >>= 8;
+                _FALLTHROUGH;
+            default:
+                vMemory.at(nOffset) = nValue & 0xFF;
+                break;
+        }
+    }
+
+    m_pImpl->AddBlocks(*this, vAddresses, vMemory, nFirstAddress, m_pImpl->GetPadding());
+}
+
 bool SearchResults::ContainsAddress(ra::ByteAddress nAddress) const
 {
     if (m_pImpl)
diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h
index 2d4f5049..cc0e7bd7 100644
--- a/src/services/SearchResults.h
+++ b/src/services/SearchResults.h
@@ -68,9 +68,15 @@ public:
         if (!AreAllAddressesMatching())
         {
             if ((m_nMatchingAddresses + 7) / 8 > sizeof(m_vAddresses))
-                std::memcpy(m_pAddresses, other.m_pAddresses, (m_nMatchingAddresses + 7) / 8);
+            {
+                auto* pAddresses = AllocateMatchingAddresses();
+                if (pAddresses != nullptr)
+                    std::memcpy(pAddresses, other.m_pAddresses, (m_nMatchingAddresses + 7) / 8);
+            }
             else
+            {
                 std::memcpy(m_vAddresses, other.m_vAddresses, sizeof(m_vAddresses));
+            }
         }
     }
 
@@ -242,6 +248,13 @@ public:
         MemSize nSize{};
     };
 
+    /// <summary>
+    /// Initializes a result set from a list of address/value pairs
+    /// </summary>
+    /// <param name="srResults">The results to populate from.</param>
+    /// <param name="nType">Type of search to initialize.</param>
+    void Initialize(_In_ const std::vector<Result>& srResults, _In_ SearchType nSearchType);
+
     /// <summary>
     /// Gets the nIndex'th matching address.
     /// </summary>
diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp
index 28d25037..de326e5e 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.cpp
+++ b/src/ui/viewmodels/MemorySearchViewModel.cpp
@@ -487,6 +487,7 @@ void MemorySearchViewModel::ClearResults()
         m_vSelectedAddresses.clear();
         m_vSearchResults.clear();
         m_vResults.Clear();
+        m_nSelectedSearchResult = 0;
     }
 
     SetValue(FilterSummaryProperty, FilterSummaryProperty.GetDefaultValue());
@@ -1290,6 +1291,146 @@ void MemorySearchViewModel::SaveResults(ra::services::TextWriter& sFile, std::fu
     }
 }
 
+void MemorySearchViewModel::ImportResults()
+{
+    switch (GetSearchType())
+    {
+        case ra::services::SearchType::AsciiText:
+        case ra::services::SearchType::BitCount:
+        case ra::services::SearchType::FourBit:
+            ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+                ra::StringPrintf(L"Cannot import %s search results",
+                    m_vSearchTypes.GetLabelForId(ra::etoi(GetSearchType()))));
+            return;
+    }
+
+    if (m_vResults.Count() != 0 || m_nSelectedSearchResult != 0)
+    {
+        if (ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Start new search?",
+            L"This will initialize a new search with previously exported search results.",
+            ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo) == ra::ui::DialogResult::No)
+        {
+            return;
+        }
+    }
+
+    ra::ui::viewmodels::FileDialogViewModel vmFileDialog;
+    vmFileDialog.SetWindowTitle(L"Import Search Results");
+    vmFileDialog.AddFileType(L"CSV File", L"*.csv");
+    vmFileDialog.SetDefaultExtension(L"csv");
+
+    const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
+    vmFileDialog.SetFileName(ra::StringPrintf(L"%u-SearchResults.csv", pGameContext.GameId()));
+
+    if (vmFileDialog.ShowOpenFileDialog() == ra::ui::DialogResult::OK)
+    {
+        const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
+        auto pTextReader = pFileSystem.OpenTextFile(vmFileDialog.GetFileName());
+        if (pTextReader == nullptr)
+        {
+            ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+                ra::StringPrintf(L"Could not open %s", vmFileDialog.GetFileName()));
+        }
+        else
+        {
+            std::string sLine;
+            if (!pTextReader->GetLine(sLine) || sLine != "Address,Value,PreviousValue,InitialValue")
+            {
+                ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+                    L"File does not appear to be an exported search result");
+            }
+            else
+            {
+                if (m_bIsContinuousFiltering)
+                    ToggleContinuousFilter();
+
+                std::unique_ptr<SearchResult> pResult;
+                pResult.reset(new SearchResult());
+                pResult->sSummary =
+                    ra::StringPrintf(L"Imported Search", SearchTypes().GetLabelForId(ra::etoi(GetSearchType())));
+
+                LoadResults(*pTextReader, *pResult);
+
+                std::unique_ptr<SearchResult> pResult2;
+                pResult2.reset(new SearchResult());
+                pResult2->sSummary = pResult->sSummary;
+
+                pResult2->pResults.Initialize(pResult->pResults,
+                    [pResults = &pResult->pResults](ra::ByteAddress nAddress, uint8_t* pBuffer, size_t nCount) noexcept {
+                        pResults->GetBytes(nAddress, pBuffer, nCount);
+                    },
+                    ComparisonType::Equals, GetValueType(), GetFilterValue());
+
+                SetValue(FilterSummaryProperty, pResult->sSummary);
+                SetValue(SelectedPageProperty, L"1/1");
+                SetValue(ScrollOffsetProperty, 0);
+                SetValue(ScrollMaximumProperty, 0);
+                SetValue(ResultCountProperty, gsl::narrow_cast<int>(pResult->pResults.MatchingAddressCount()));
+                SetValue(ResultMemSizeProperty, ra::etoi(pResult->pResults.GetSize()));
+
+                {
+                    std::lock_guard lock(m_oMutex);
+
+                    m_vSearchResults.clear();
+                    m_vSearchResults.push_back(std::move(pResult));
+                    m_vSearchResults.push_back(std::move(pResult2));
+                }
+
+                ChangePage(1);
+            }
+        }
+    }
+}
+
+void MemorySearchViewModel::LoadResults(ra::services::TextReader& pTextReader,
+                                        MemorySearchViewModel::SearchResult& vmResult) const
+{
+    std::vector<ra::services::SearchResults::Result> vEntries;
+    ra::services::SearchResults::Result pResult{};
+
+    ra::services::SearchResults pTemp;
+    pTemp.Initialize(0, 0, GetSearchType());
+    pResult.nSize = pTemp.GetSize();
+
+    const bool bIsFloat = ra::data::MemSizeIsFloat(pResult.nSize);
+
+    std::string sLine;
+    while (pTextReader.GetLine(sLine))
+    {
+        const auto index = sLine.find(',');
+        if (index == std::string::npos)
+            continue;
+        const auto index2 = sLine.find(',', index + 1);
+        if (index2 == std::string::npos)
+            continue;
+
+        pResult.nAddress = ra::ByteAddressFromString(sLine.substr(0, index));
+
+        const auto sValue = sLine.substr(index + 1, index2 - index - 1);
+        if (bIsFloat)
+        {
+            char* pEnd = nullptr;
+            const auto fValue = std::strtof(sValue.c_str(), &pEnd);
+            if (pEnd && *pEnd != '\0')
+                continue;
+            pResult.nValue = ra::data::FloatToU32(fValue, pResult.nSize);
+        }
+        else
+        {
+            char* pEnd = nullptr;
+            const auto nValue = std::strtoul(sValue.c_str(), &pEnd, 16);
+            if (pEnd && *pEnd != '\0')
+                continue;
+            pResult.nValue = gsl::narrow_cast<unsigned int>(nValue);
+        }
+
+        vEntries.push_back(pResult);
+    }
+
+    if (!vEntries.empty())
+        vmResult.pResults.Initialize(vEntries, GetSearchType());
+}
+
 } // namespace viewmodels
 } // namespace ui
 } // namespace ra
diff --git a/src/ui/viewmodels/MemorySearchViewModel.hh b/src/ui/viewmodels/MemorySearchViewModel.hh
index 11b04195..4dc61ce3 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.hh
+++ b/src/ui/viewmodels/MemorySearchViewModel.hh
@@ -7,6 +7,7 @@
 #include "data\context\GameContext.hh"
 
 #include "services\SearchResults.h"
+#include "services\TextReader.hh"
 #include "services\TextWriter.hh"
 
 #include "ui\WindowViewModelBase.hh"
@@ -445,6 +446,11 @@ public:
     /// </summary>
     void ExportResults() const;
 
+    /// <summary>
+    /// Allows the user to imports the contents of a CSV file as a search starting point.
+    /// </summary>
+    void ImportResults();
+
     std::wstring GetTooltip(const SearchResultViewModel& vmResult) const;
 
 protected:
@@ -497,6 +503,8 @@ private:
         std::wstring sSummary;
     };
 
+    void LoadResults(ra::services::TextReader& sFile, SearchResult& pResult) const;
+
     void AddNewPage(std::unique_ptr<SearchResult>&& pNewPage);
     void ChangePage(size_t nNewPage);
 
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/OverlayRecentGamesPageViewModel.cpp b/src/ui/viewmodels/OverlayRecentGamesPageViewModel.cpp
index 1dcd6b12..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,24 +93,39 @@ 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;
-
-                    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)
-                    {
-                        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);
-                    });
+                    bAnyMissing = true;
                 }
             }
+
+            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());
+
+                rc_api_request_t request;
+                if (rc_api_init_fetch_game_titles_request(&request, &api_params) != RC_OK)
+                    return;
+
+                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_titles_server_response(&response, &server_response) == RC_OK &&
+                            response.response.succeeded)
+                        {
+                            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 2a1a11a7..6ca6b2e1 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -136,6 +136,18 @@ void TriggerConditionViewModel::SerializeAppend(std::string& sBuffer) const
                 sBuffer.push_back('^');
                 break;
 
+            case TriggerOperatorType::Modulus:
+                sBuffer.push_back('%');
+                break;
+
+            case TriggerOperatorType::Add:
+                sBuffer.push_back('+');
+                break;
+
+            case TriggerOperatorType::Subtract:
+                sBuffer.push_back('-');
+                break;
+
             default:
                 assert(!"Unknown comparison");
                 break;
@@ -333,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;
     }
 }
 
@@ -515,6 +526,9 @@ static constexpr bool IsModifyingOperator(TriggerOperatorType nType)
         case TriggerOperatorType::Divide:
         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 63c0ce98..dac7af23 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -56,7 +56,10 @@ enum class TriggerOperatorType : uint8_t
     Multiply = RC_OPERATOR_MULT,
     Divide = RC_OPERATOR_DIV,
     BitwiseAnd = RC_OPERATOR_AND,
-    BitwiseXor = RC_OPERATOR_XOR
+    BitwiseXor = RC_OPERATOR_XOR,
+    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 90fc78c2..ae9eab7c 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -84,6 +84,9 @@ TriggerViewModel::TriggerViewModel() noexcept
     m_vOperatorTypes.Add(ra::etoi(TriggerOperatorType::None), L"");
     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/DialogBase.hh b/src/ui/win32/DialogBase.hh
index 2459f13b..2d6a3bf2 100644
--- a/src/ui/win32/DialogBase.hh
+++ b/src/ui/win32/DialogBase.hh
@@ -65,7 +65,7 @@ public:
     void SetDialogResult(DialogResult nResult);
 
     /// <summary>
-    /// Returns <c>true</c> if the current thread is the UI thread.
+    /// Returns <c>true</c> if the current thread is the UI thread (or invoking to the UI thread is disabled).
     /// </summary>
     bool IsOnUIThread() const noexcept { return ra::ui::win32::bindings::WindowBinding::IsOnUIThread(); }
 
diff --git a/src/ui/win32/MemoryInspectorDialog.cpp b/src/ui/win32/MemoryInspectorDialog.cpp
index 9fd09a89..cd3b9e7b 100644
--- a/src/ui/win32/MemoryInspectorDialog.cpp
+++ b/src/ui/win32/MemoryInspectorDialog.cpp
@@ -176,6 +176,7 @@ MemoryInspectorDialog::MemoryInspectorDialog(MemoryInspectorViewModel& vmMemoryI
     m_bindWindow.BindEnabled(IDC_RA_FILTER_VALUE, MemorySearchViewModel::CanEditFilterValueProperty);
     m_bindWindow.BindEnabled(IDC_RA_APPLY_FILTER, MemorySearchViewModel::CanFilterProperty);
     m_bindWindow.BindEnabled(IDC_RA_RESULTS_EXPORT, MemorySearchViewModel::CanFilterProperty);
+    m_bindWindow.BindEnabled(IDC_RA_RESULTS_IMPORT, MemorySearchViewModel::CanBeginNewSearchProperty);
     m_bindWindow.BindEnabled(IDC_RA_CONTINUOUS_FILTER, MemorySearchViewModel::CanContinuousFilterProperty);
     m_bindWindow.BindLabel(IDC_RA_CONTINUOUS_FILTER, MemorySearchViewModel::ContinuousFilterLabelProperty);
 
@@ -408,6 +409,14 @@ BOOL MemoryInspectorDialog::OnCommand(WORD nCommand)
             return TRUE;
         }
 
+        case IDC_RA_RESULTS_IMPORT: {
+            auto* vmMemoryInspector = dynamic_cast<MemoryInspectorViewModel*>(&m_vmWindow);
+            if (vmMemoryInspector)
+                vmMemoryInspector->Search().ImportResults();
+
+            return TRUE;
+        }
+
         case IDC_RA_VIEW_CODENOTES:
         {
             auto* vmMemoryInspector = dynamic_cast<MemoryInspectorViewModel*>(&m_vmWindow);
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;
diff --git a/src/ui/win32/bindings/WindowBinding.cpp b/src/ui/win32/bindings/WindowBinding.cpp
index 7ed47343..f9b620c1 100644
--- a/src/ui/win32/bindings/WindowBinding.cpp
+++ b/src/ui/win32/bindings/WindowBinding.cpp
@@ -51,6 +51,7 @@ static DispatchingWindow::ViewModel s_vmDispatchingWindow;
 
 std::vector<WindowBinding*> WindowBinding::s_vKnownBindings;
 DWORD WindowBinding::s_hUIThreadId;
+bool WindowBinding::s_bInvokeOnUIThreadDisabled = true;
 
 void WindowBinding::SetHWND(DialogBase* pDialog, HWND hWnd)
 {
@@ -545,16 +546,26 @@ void WindowBinding::SetUIThread(DWORD hThreadId) noexcept
 
 void WindowBinding::EnableInvokeOnUIThread()
 {
-    if (s_pDispatchingWindow == nullptr && IsOnUIThread())
+    if (s_pDispatchingWindow == nullptr)
     {
-        // create a hidden dummy window for dispatching UI messages
+        if (GetCurrentThreadId() == s_hUIThreadId)
+        {
+            // create a hidden dummy window for dispatching UI messages
+
+            s_pDispatchingWindow.reset(new DispatchingWindow(s_vmDispatchingWindow));
+            if (!s_pDispatchingWindow->CreateDialogWindow(MAKEINTRESOURCE(IDD_RA_RICHPRESENCE),
+                                                          &s_pDispatchingWindow->m_pPresenter))
+            {
+                RA_LOG_ERR("Could not create dispatching window!");
+                s_pDispatchingWindow.reset();
+            }
 
-        s_pDispatchingWindow.reset(new DispatchingWindow(s_vmDispatchingWindow));
-        if (!s_pDispatchingWindow->CreateDialogWindow(MAKEINTRESOURCE(IDD_RA_RICHPRESENCE),
-                                                      &s_pDispatchingWindow->m_pPresenter))
+            s_bInvokeOnUIThreadDisabled = (s_pDispatchingWindow == nullptr);
+        }
+        else
         {
-            RA_LOG_ERR("Could not create Code Notes dialog!");
-            s_pDispatchingWindow.reset();
+            // can't create the dummy window unless we're on the UI thread. disable InvokeOnUIThread
+            s_bInvokeOnUIThreadDisabled = true;
         }
     }
 }
diff --git a/src/ui/win32/bindings/WindowBinding.hh b/src/ui/win32/bindings/WindowBinding.hh
index 308ddaf3..02cf1ae2 100644
--- a/src/ui/win32/bindings/WindowBinding.hh
+++ b/src/ui/win32/bindings/WindowBinding.hh
@@ -138,9 +138,9 @@ public:
     static void EnableInvokeOnUIThread();
 
     /// <summary>
-    /// Returns <c>true</c> if the current thread is the UI thread.
+    /// Returns <c>true</c> if the current thread is the UI thread (or invoking to the UI thread is disabled).
     /// </summary>
-    static bool IsOnUIThread() noexcept { return GetCurrentThreadId() == s_hUIThreadId; }
+    static bool IsOnUIThread() noexcept { return s_bInvokeOnUIThreadDisabled || GetCurrentThreadId() == s_hUIThreadId; }
 
     /// <summary>
     /// Dispatches a function to the UI thread.
@@ -212,6 +212,7 @@ private:
     HWND m_hWnd{};
 
     static DWORD s_hUIThreadId;
+    static bool s_bInvokeOnUIThreadDisabled;
 
     static std::vector<WindowBinding*> s_vKnownBindings;
 };


Test patch:
diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp
index 1f0c193f..e73010fa 100644
--- a/tests/services/SearchResults_Tests.cpp
+++ b/tests/services/SearchResults_Tests.cpp
@@ -2582,6 +2582,152 @@ public:
         Assert::IsTrue(results1.UpdateValue(pResult, &sFormattedValue, mockEmulatorContext));
         Assert::AreEqual(std::wstring(L"5 (00111011)"), sFormattedValue);
     }
+    
+    TEST_METHOD(TestInitializeFromListEightBit)
+    {
+        std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        std::vector<SearchResults::Result> vResults;
+        vResults.push_back({2, 0x07, MemSize::Unknown});
+        vResults.push_back({3, 0x16, MemSize::Unknown});
+
+        SearchResults results;
+        results.Initialize(vResults, ra::services::SearchType::EightBit);
+
+        Assert::AreEqual({2U}, results.MatchingAddressCount());
+
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsFalse(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsTrue(results.ContainsAddress(3U));
+        Assert::IsFalse(results.ContainsAddress(4U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x07U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::AreEqual(3U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x16U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromListThirtyTwoBit)
+    {
+        std::array<unsigned char, 16> memory{};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        std::vector<SearchResults::Result> vResults;
+        vResults.push_back({2, 0x12345678, MemSize::Unknown});
+        vResults.push_back({4, 0x55551234, MemSize::Unknown});
+        vResults.push_back({12, 0xdeadbeef, MemSize::Unknown});
+
+        SearchResults results;
+        results.Initialize(vResults, ra::services::SearchType::ThirtyTwoBit);
+
+        Assert::AreEqual({3U}, results.MatchingAddressCount());
+
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsTrue(results.ContainsAddress(4U));
+        Assert::IsFalse(results.ContainsAddress(5U));
+        Assert::IsTrue(results.ContainsAddress(12U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBit, result.nSize);
+        Assert::AreEqual(0x12345678U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBit, result.nSize);
+        Assert::AreEqual(0x55551234U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(2U, result));
+        Assert::AreEqual(12U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBit, result.nSize);
+        Assert::AreEqual(0xdeadbeefU, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromListThirtyTwoBitAligned)
+    {
+        std::array<unsigned char, 16> memory{};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        std::vector<SearchResults::Result> vResults;
+        vResults.push_back({2, 0x12345678, MemSize::Unknown}); // not aligned - should be ignored
+        vResults.push_back({4, 0x55551234, MemSize::Unknown});
+        vResults.push_back({12, 0xdeadbeef, MemSize::Unknown});
+
+        SearchResults results;
+        results.Initialize(vResults, ra::services::SearchType::ThirtyTwoBitAligned);
+
+        Assert::AreEqual({2U}, results.MatchingAddressCount());
+
+        Assert::IsFalse(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsTrue(results.ContainsAddress(4U));
+        Assert::IsFalse(results.ContainsAddress(5U));
+        Assert::IsTrue(results.ContainsAddress(12U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBit, result.nSize);
+        Assert::AreEqual(0x55551234U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::AreEqual(12U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBit, result.nSize);
+        Assert::AreEqual(0xdeadbeefU, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromListThirtyTwoBitBigEndian)
+    {
+        std::array<unsigned char, 16> memory{};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        std::vector<SearchResults::Result> vResults;
+        vResults.push_back({2, 0x12345678, MemSize::Unknown});
+        vResults.push_back({4, 0x56785555, MemSize::Unknown});
+        vResults.push_back({12, 0xdeadbeef, MemSize::Unknown});
+
+        SearchResults results;
+        results.Initialize(vResults, ra::services::SearchType::ThirtyTwoBitBigEndian);
+
+        Assert::AreEqual({3U}, results.MatchingAddressCount());
+
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsTrue(results.ContainsAddress(4U));
+        Assert::IsFalse(results.ContainsAddress(5U));
+        Assert::IsTrue(results.ContainsAddress(12U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBitBigEndian, result.nSize);
+        Assert::AreEqual(0x12345678U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBitBigEndian, result.nSize);
+        Assert::AreEqual(0x56785555U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(2U, result));
+        Assert::AreEqual(12U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBitBigEndian, result.nSize);
+        Assert::AreEqual(0xdeadbeefU, result.nValue);
+    }
+
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
index 3cf5f63e..4a8915f2 100644
--- a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
@@ -135,7 +135,7 @@ private:
     };
 
     void AssertRow(MemorySearchViewModelHarness& search, gsl::index nRow, ra::ByteAddress nAddress,
-        const wchar_t* sAddress, const wchar_t* sCurrentValue)
+                   const wchar_t* sAddress, const wchar_t* sCurrentValue)
     {
         auto* pRow = search.Results().GetItemAt(nRow);
         Assert::IsNotNull(pRow);
@@ -1965,6 +1965,237 @@ public:
         Assert::IsTrue(bMessageSeen);
         Assert::IsFalse(bDialogSeen);
     }
+
+    TEST_METHOD(TestImportResults)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+
+        const std::string sContents = "Address,Value,PreviousValue,InitialValue\n0x0005,0x05,0x08,0x05\n0x000c,0x07,0x09,0x0c\n";
+        search.mockFileSystem.MockFile(L"E:\\Data\\3-SearchResults.csv", sContents);
+        
+        bool bDialogSeen = false;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::FileDialogViewModel>(
+            [&bDialogSeen](ra::ui::viewmodels::FileDialogViewModel& vmFileDialog) {
+                bDialogSeen = true;
+
+                Assert::AreEqual(std::wstring(L"Import Search Results"), vmFileDialog.GetWindowTitle());
+                Assert::AreEqual({1U}, vmFileDialog.GetFileTypes().size());
+                Assert::AreEqual(std::wstring(L"csv"), vmFileDialog.GetDefaultExtension());
+                Assert::AreEqual(std::wstring(L"3-SearchResults.csv"), vmFileDialog.GetFileName());
+
+                vmFileDialog.SetFileName(L"E:\\Data\\3-SearchResults.csv");
+
+                return DialogResult::OK;
+            });
+
+        search.ImportResults();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({2U}, search.Results().Count());
+        AssertRow(search, 0, 5U, L"0x0005", L"0x05");
+        AssertRow(search, 1, 12U, L"0x000c", L"0x0c");
+
+        // use tooltip to validate "initial" value for $000c is 0x07
+        const auto sTooltip = search.GetTooltip(*search.Results().GetItemAt(1));
+        Assert::AreEqual(std::wstring(L"0x000c\n0x0c | Current\n0x07 | Last Filter\n0x07 | Initial"), sTooltip);
+    }
+
+    TEST_METHOD(TestImportResultsThirtyTwoBit)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+        search.SetSearchType(ra::services::SearchType::ThirtyTwoBit);
+
+        const std::string sContents =
+            "Address,Value,PreviousValue,InitialValue\n0x0005,0x12345678,0x08,0x05\n";
+        search.mockFileSystem.MockFile(L"E:\\Data\\3-SearchResults.csv", sContents);
+
+        bool bDialogSeen = false;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::FileDialogViewModel>(
+            [&bDialogSeen](ra::ui::viewmodels::FileDialogViewModel& vmFileDialog) {
+                bDialogSeen = true;
+
+                Assert::AreEqual(std::wstring(L"Import Search Results"), vmFileDialog.GetWindowTitle());
+                Assert::AreEqual({1U}, vmFileDialog.GetFileTypes().size());
+                Assert::AreEqual(std::wstring(L"csv"), vmFileDialog.GetDefaultExtension());
+                Assert::AreEqual(std::wstring(L"3-SearchResults.csv"), vmFileDialog.GetFileName());
+
+                vmFileDialog.SetFileName(L"E:\\Data\\3-SearchResults.csv");
+
+                return DialogResult::OK;
+            });
+
+        search.ImportResults();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({1U}, search.Results().Count());
+        AssertRow(search, 0, 5U, L"0x0005", L"0x08070605");
+
+        // use tooltip to validate "initial" value
+        const auto sTooltip = search.GetTooltip(*search.Results().GetItemAt(0));
+        Assert::AreEqual(std::wstring(L"0x0005\n0x08070605 | Current\n0x12345678 | Last Filter\n0x12345678 | Initial"), sTooltip);
+    }
+
+    TEST_METHOD(TestImportResultsFloat)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+        search.memory.at(4) = 0xDB;
+        search.memory.at(5) = 0x0F;
+        search.memory.at(6) = 0x49;
+        search.memory.at(7) = 0x40;
+        search.SetSearchType(ra::services::SearchType::Float);
+
+        const std::string sContents = "Address,Value,PreviousValue,InitialValue\n0x0004,1.5,2.6,2.6\n";
+        search.mockFileSystem.MockFile(L"E:\\Data\\3-SearchResults.csv", sContents);
+
+        bool bDialogSeen = false;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::FileDialogViewModel>(
+            [&bDialogSeen](ra::ui::viewmodels::FileDialogViewModel& vmFileDialog) {
+                bDialogSeen = true;
+
+                Assert::AreEqual(std::wstring(L"Import Search Results"), vmFileDialog.GetWindowTitle());
+                Assert::AreEqual({1U}, vmFileDialog.GetFileTypes().size());
+                Assert::AreEqual(std::wstring(L"csv"), vmFileDialog.GetDefaultExtension());
+                Assert::AreEqual(std::wstring(L"3-SearchResults.csv"), vmFileDialog.GetFileName());
+
+                vmFileDialog.SetFileName(L"E:\\Data\\3-SearchResults.csv");
+
+                return DialogResult::OK;
+            });
+
+        search.ImportResults();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({1U}, search.Results().Count());
+        AssertRow(search, 0, 4U, L"0x0004", L"3.141593");
+
+        // use tooltip to validate "initial" value
+        const auto sTooltip = search.GetTooltip(*search.Results().GetItemAt(0));
+        Assert::AreEqual(std::wstring(L"0x0004\n3.141593 | Current\n1.5 | Last Filter\n1.5 | Initial"), sTooltip);
+    }
+
+    TEST_METHOD(TestImportResultsAsciiText)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+        search.SetSearchType(ra::services::SearchType::AsciiText);
+
+        bool bDialogSeen = false;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>(
+            [&bDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+                bDialogSeen = true;
+                Assert::AreEqual(std::wstring(L"Cannot import ASCII Text search results"), vmMessageBox.GetMessage());
+                return ra::ui::DialogResult::OK;
+            });
+
+        search.ImportResults();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({0U}, search.Results().Count());
+    }
+
+    TEST_METHOD(TestImportResultsConfirmOverwrite)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+        search.BeginNewSearch();
+        search.ApplyFilter();
+        Assert::AreEqual({32U}, search.GetResultCount());
+
+        const std::string sContents =
+            "Address,Value,PreviousValue,InitialValue\n0x0005,0x05,0x08,0x05\n0x000c,0x07,0x09,0x0c\n";
+        search.mockFileSystem.MockFile(L"E:\\Data\\3-SearchResults.csv", sContents);
+
+        int nDialogSeen = 0;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>(
+            [&nDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+                nDialogSeen++;
+
+                Assert::AreEqual(std::wstring(L"Start new search?"), vmMessageBox.GetHeader());
+                Assert::AreEqual(std::wstring(L"This will initialize a new search with previously exported search results."), vmMessageBox.GetMessage());
+                Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
+
+                return (nDialogSeen == 1) ? DialogResult::No : DialogResult::Yes;
+            });
+
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::FileDialogViewModel>(
+            [&nDialogSeen](ra::ui::viewmodels::FileDialogViewModel& vmFileDialog) {
+                nDialogSeen++;
+
+                Assert::AreEqual(std::wstring(L"Import Search Results"), vmFileDialog.GetWindowTitle());
+                Assert::AreEqual({1U}, vmFileDialog.GetFileTypes().size());
+                Assert::AreEqual(std::wstring(L"csv"), vmFileDialog.GetDefaultExtension());
+                Assert::AreEqual(std::wstring(L"3-SearchResults.csv"), vmFileDialog.GetFileName());
+
+                vmFileDialog.SetFileName(L"E:\\Data\\3-SearchResults.csv");
+
+                return DialogResult::OK;
+            });
+
+        search.ImportResults();
+        Assert::AreEqual(1, nDialogSeen); // one dialog, canceled
+
+        Assert::AreEqual({32U}, search.GetResultCount());
+
+        search.ImportResults();
+        Assert::AreEqual(3, nDialogSeen); // second confirmation dialog + file selection dialog
+
+        Assert::AreEqual({2U}, search.GetResultCount());
+        AssertRow(search, 0, 5U, L"0x0005", L"0x05");
+        AssertRow(search, 1, 12U, L"0x000c", L"0x0c");
+
+        // use tooltip to validate "initial" value for $000c is 0x07
+        const auto sTooltip = search.GetTooltip(*search.Results().GetItemAt(1));
+        Assert::AreEqual(std::wstring(L"0x000c\n0x0c | Current\n0x07 | Last Filter\n0x07 | Initial"), sTooltip);
+    }
+
+    TEST_METHOD(TestImportResultsEmptyFile)
+    {
+        MemorySearchViewModelHarness search;
+        search.mockGameContext.SetGameId(3);
+        search.InitializeMemory();
+
+        const std::string sContents = "";
+        search.mockFileSystem.MockFile(L"E:\\Data\\3-SearchResults.csv", sContents);
+
+        int nDialogSeen = 0;
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::FileDialogViewModel>(
+            [&nDialogSeen](ra::ui::viewmodels::FileDialogViewModel& vmFileDialog) {
+                nDialogSeen++;
+
+                Assert::AreEqual(std::wstring(L"Import Search Results"), vmFileDialog.GetWindowTitle());
+                Assert::AreEqual({1U}, vmFileDialog.GetFileTypes().size());
+                Assert::AreEqual(std::wstring(L"csv"), vmFileDialog.GetDefaultExtension());
+                Assert::AreEqual(std::wstring(L"3-SearchResults.csv"), vmFileDialog.GetFileName());
+
+                vmFileDialog.SetFileName(L"E:\\Data\\3-SearchResults.csv");
+
+                return DialogResult::OK;
+            });
+
+        search.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>(
+            [&nDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+                nDialogSeen++;
+                Assert::AreEqual(std::wstring(L"File does not appear to be an exported search result"), vmMessageBox.GetMessage());
+                return ra::ui::DialogResult::OK;
+            });
+
+        search.ImportResults();
+
+        Assert::AreEqual(2, nDialogSeen);
+        Assert::AreEqual({0U}, search.Results().Count());
+    }
 };
 
 } // namespace tests
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 bf41a30f..9d7f942a 100644
--- a/tests/ui/viewmodels/TriggerConditionAsserts.hh
+++ b/tests/ui/viewmodels/TriggerConditionAsserts.hh
@@ -103,6 +103,12 @@ std::wstring ToString<ra::ui::viewmodels::TriggerOperatorType>(
             return L"Divide";
         case ra::ui::viewmodels::TriggerOperatorType::BitwiseAnd:
             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 db944cbf..70175b64 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -189,6 +189,18 @@ public:
         vmCondition.SetOperator(TriggerOperatorType::BitwiseAnd);
         Assert::AreEqual(TriggerOperatorType::BitwiseAnd, vmCondition.GetOperator());
         Assert::IsTrue(vmCondition.HasTarget());
+
+        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)
@@ -487,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
     }
 
@@ -1124,6 +1139,7 @@ public:
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseXor)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Modulus)));
 
         condition.SetType(TriggerConditionType::AddAddress);
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Equals)));
@@ -1137,6 +1153,7 @@ public:
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseXor)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Modulus)));
     }
 
     TEST_METHOD(TestIsComparisonVisibleMeasured)
@@ -1155,6 +1172,7 @@ public:
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseXor)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Modulus)));
 
         TriggerViewModel vmTrigger;
         vmTrigger.SetIsValue(true);
@@ -1172,6 +1190,7 @@ public:
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
         Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseXor)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Modulus)));
     }
 
     TEST_METHOD(TestFormatValueNumber)
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 7ca89e2d..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({ 11U }, 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());
@@ -233,10 +233,16 @@ public:
         Assert::AreEqual(std::wstring(L"*"), vmTrigger.OperatorTypes().GetItemAt(7)->GetLabel());
         Assert::AreEqual((int)TriggerOperatorType::Divide, vmTrigger.OperatorTypes().GetItemAt(8)->GetId());
         Assert::AreEqual(std::wstring(L"/"), vmTrigger.OperatorTypes().GetItemAt(8)->GetLabel());
-        Assert::AreEqual((int)TriggerOperatorType::BitwiseAnd, vmTrigger.OperatorTypes().GetItemAt(9)->GetId());
-        Assert::AreEqual(std::wstring(L"&"), vmTrigger.OperatorTypes().GetItemAt(9)->GetLabel());
-        Assert::AreEqual((int)TriggerOperatorType::BitwiseXor, vmTrigger.OperatorTypes().GetItemAt(10)->GetId());
-        Assert::AreEqual(std::wstring(L"^"), vmTrigger.OperatorTypes().GetItemAt(10)->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::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)