Repository: RetroAchievements/RAIntegration
Pull request #857 (2022-01-13 01:03:01)

prevent deserialization failure if description is empty


Patch:
diff --git a/src/data/models/LeaderboardModel.cpp b/src/data/models/LeaderboardModel.cpp
index 0d52696a..7a1fea7a 100644
--- a/src/data/models/LeaderboardModel.cpp
+++ b/src/data/models/LeaderboardModel.cpp
@@ -294,7 +294,7 @@ bool LeaderboardModel::Deserialize(ra::Tokenizer& pTokenizer)
 
     // field 8: description
     std::string sDescription;
-    if (!ReadPossiblyQuoted(pTokenizer, sDescription))
+    if (!pTokenizer.EndOfString() && !ReadPossiblyQuoted(pTokenizer, sDescription))
         return false;
 
     // line is valid
diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp
index c5bf49e7..88874e92 100644
--- a/src/services/SearchResults.cpp
+++ b/src/services/SearchResults.cpp
@@ -97,6 +97,15 @@ public:
         return false;
     }
 
+    // Removes the result associated to the specified real address from the collection of matched addresses.
+    virtual bool ExcludeResult(SearchResults& srResults, const SearchResults::Result& pResult) const
+    {
+        if (pResult.nAddress & (GetStride() - 1))
+            return false;
+
+        return ExcludeAddress(srResults, ConvertFromRealAddress(pResult.nAddress));
+    }
+
     virtual bool ValidateFilterValue(SearchResults& srNew) const noexcept(false)
     {
         // convert the filter string into a value
@@ -521,6 +530,21 @@ protected:
         return srResults.m_vBlocks;
     }
 
+    // Removes the result associated to the specified virtual address from the collection of matched addresses.
+    static bool ExcludeAddress(SearchResults& srResults, ra::ByteAddress nAddress)
+    {
+        for (auto& pBlock : srResults.m_vBlocks)
+        {
+            if (pBlock.ContainsAddress(nAddress))
+            {
+                pBlock.ExcludeMatchingAddress(nAddress);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /// <summary>
     /// Gets a value from a memory block using the provided virtual address (in result.nAddress)
     /// </summary>
@@ -622,6 +646,15 @@ class FourBitSearchImpl : public SearchImpl
         return false;
     }
 
+    bool ExcludeResult(SearchResults& srResults, const SearchResults::Result& pResult) const override
+    {
+        auto nAddress = pResult.nAddress << 1;
+        if (pResult.nSize == MemSize::Nibble_Upper)
+            nAddress |= 1;
+
+        return ExcludeAddress(srResults, nAddress);
+    }
+
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
         const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
@@ -1570,10 +1603,12 @@ void SearchResults::MergeSearchResults(const SearchResults& srMemory, const Sear
 
     for (const auto pSrcBlock : srAddresses.m_vBlocks)
     {
-        unsigned int nSize = pSrcBlock.GetBytesSize();
-        ra::ByteAddress nAddress = pSrcBlock.GetFirstAddress();
-        const auto nMaxAddresses = pSrcBlock.GetMaxAddresses();
-        auto& pNewBlock = m_vBlocks.emplace_back(nAddress, nSize, nMaxAddresses);
+        // copy the block from the srAddresses collection, then update the memory from the srMemory collection.
+        // this creates a new block with the memory from the srMemory collection and the addresses from the
+        // srAddresses collection.
+        auto& pNewBlock = m_vBlocks.emplace_back(pSrcBlock);
+        unsigned int nSize = pNewBlock.GetBytesSize();
+        ra::ByteAddress nAddress = pNewBlock.GetFirstAddress();
         unsigned char* pWrite = pNewBlock.GetBytes();
 
         for (const auto pMemBlock : srMemory.m_vBlocks)
@@ -1641,19 +1676,12 @@ size_t SearchResults::MatchingAddressCount() const noexcept
     return nCount;
 }
 
-void SearchResults::ExcludeAddress(ra::ByteAddress nAddress)
+bool SearchResults::ExcludeResult(const SearchResults::Result& pResult)
 {
-    if (m_nFilterType == SearchFilterType::None)
-        return;
+    if (m_nFilterType != SearchFilterType::None && m_pImpl != nullptr)
+        return m_pImpl->ExcludeResult(*this, pResult);
 
-    for (auto& pBlock : m_vBlocks)
-    {
-        if (pBlock.ContainsAddress(nAddress))
-        {
-            pBlock.ExcludeMatchingAddress(nAddress);
-            break;
-        }
-    }
+    return false;
 }
 
 bool SearchResults::GetMatchingAddress(gsl::index nIndex, _Out_ SearchResults::Result& result) const noexcept
diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h
index e57c59c7..3499ef77 100644
--- a/src/services/SearchResults.h
+++ b/src/services/SearchResults.h
@@ -302,15 +302,17 @@ public:
     /// Determines whether the specified address appears in the matching address list.
     /// </summary>
     /// <param name="nAddress">The n address.</param>
-    /// <returns><c>true</c> if the specified address is in the matching address list; otherwise,
+    /// <returns><c>true</c> if the specified address is in the matching address list; otherwise
     /// <c>false</c>.</returns>
     bool ContainsAddress(ra::ByteAddress nAddress) const;
 
     /// <summary>
-    /// Removes an address from the matching address list.
+    /// Removes an entry from the matching address list.
     /// </summary>
-    /// <param name="nAddress">The address to remove.</param>
-    void ExcludeAddress(ra::ByteAddress nAddress);
+    /// <param name="pResult">The result to remove.</param>
+    /// <returns><c>true</c> if the specified address was removed from the matching address list;
+    /// otherwise <c>false</c>.</returns>
+    bool ExcludeResult(const SearchResults::Result& pResult);
 
 private:
     void MergeSearchResults(const SearchResults& srMemory, const SearchResults& srAddresses);
diff --git a/src/ui/viewmodels/AssetEditorViewModel.cpp b/src/ui/viewmodels/AssetEditorViewModel.cpp
index 02662b39..788fdd44 100644
--- a/src/ui/viewmodels/AssetEditorViewModel.cpp
+++ b/src/ui/viewmodels/AssetEditorViewModel.cpp
@@ -695,7 +695,18 @@ void AssetEditorViewModel::OnTriggerChanged()
     }
 
     if (nSize < 0)
+    {
         m_bIgnoreTriggerUpdate = false;
+    }
+    else
+    {
+        auto* pGroup = Trigger().Groups().GetItemAt(Trigger().GetSelectedGroupIndex());
+        if (pGroup && pGroup->m_pConditionSet == nullptr)
+        {
+            // if the trigger didn't actually change, UpdateTriggerBinding won't get called, forcibly do so now.
+            UpdateTriggerBinding();
+        }
+    }
 }
 
 void AssetEditorViewModel::UpdateTriggerBinding()
diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp
index 39170bec..8c3b0ab0 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.cpp
+++ b/src/ui/viewmodels/MemorySearchViewModel.cpp
@@ -1010,9 +1010,27 @@ void MemorySearchViewModel::ExcludeSelected()
 
     const SearchResult& pPreviousResult = m_vSearchResults.back();
     SearchResult pResult{ pPreviousResult }; // clone previous item
+    ra::services::SearchResults::Result pItem {};
 
-    for (const auto nAddress : m_vSelectedAddresses)
-        pResult.pResults.ExcludeAddress(nAddress);
+    const auto nSize = pPreviousResult.pResults.GetSize();
+    if (nSize == MemSize::Nibble_Lower)
+    {
+        for (const auto nAddress : m_vSelectedAddresses)
+        {
+            pItem.nAddress = nAddress >> 1;
+            pItem.nSize = (nAddress & 1) ? MemSize::Nibble_Upper : MemSize::Nibble_Lower;
+            pResult.pResults.ExcludeResult(pItem);
+        }
+    }
+    else
+    {
+        pItem.nSize = nSize;
+        for (const auto nAddress : m_vSelectedAddresses)
+        {
+            pItem.nAddress = nAddress;
+            pResult.pResults.ExcludeResult(pItem);
+        }
+    }
 
     // attempt to keep scroll offset after filtering.
     // adjust for any items removed above the first visible address.
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index cd39350e..117b68aa 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -263,7 +263,7 @@ void TriggerConditionViewModel::SerializeAppendOperand(std::string& sBuffer, Tri
     }
 }
 
-std::wstring FormatValue(rc_typed_value_t& pValue, TriggerOperandType nType)
+static std::wstring FormatTypedValue(rc_typed_value_t& pValue, TriggerOperandType nType)
 {
     switch (nType)
     {
@@ -298,6 +298,22 @@ std::wstring FormatValue(rc_typed_value_t& pValue, TriggerOperandType nType)
     }
 }
 
+std::wstring TriggerConditionViewModel::FormatValue(unsigned nValue, TriggerOperandType nType)
+{
+    rc_typed_value_t pValue;
+    pValue.type = RC_VALUE_TYPE_UNSIGNED;
+    pValue.value.u32 = nValue;
+    return FormatTypedValue(pValue, nType);
+}
+
+std::wstring TriggerConditionViewModel::FormatValue(float fValue, TriggerOperandType nType)
+{
+    rc_typed_value_t pValue;
+    pValue.type = RC_VALUE_TYPE_FLOAT;
+    pValue.value.f32 = fValue;
+    return FormatTypedValue(pValue, nType);
+}
+
 void TriggerConditionViewModel::ChangeOperandType(const StringModelProperty& sValueProperty, TriggerOperandType nOldType, TriggerOperandType nNewType)
 {
     const auto& sValue = GetValue(sValueProperty);
@@ -318,7 +334,7 @@ void TriggerConditionViewModel::ChangeOperandType(const StringModelProperty& sVa
             ra::ParseUnsignedInt(sValue, 0xFFFFFFFF, pValue.value.u32, sError);
     }
 
-    SetValue(sValueProperty, FormatValue(pValue, nNewType));
+    SetValue(sValueProperty, FormatTypedValue(pValue, nNewType));
 }
 
 void TriggerConditionViewModel::SetOperand(const IntModelProperty& pTypeProperty,
@@ -360,7 +376,7 @@ void TriggerConditionViewModel::SetOperand(const IntModelProperty& pTypeProperty
             break;
     }
 
-    SetValue(pValueProperty, FormatValue(pValue, nType));
+    SetValue(pValueProperty, FormatTypedValue(pValue, nType));
 }
 
 void TriggerConditionViewModel::InitializeFrom(const rc_condition_t& pCondition)
@@ -483,10 +499,7 @@ void TriggerConditionViewModel::OnValueChanged(const BoolModelProperty::ChangeAr
 
 void TriggerConditionViewModel::SetSourceValue(unsigned int nValue)
 {
-    rc_typed_value_t pValue;
-    pValue.type = RC_VALUE_TYPE_UNSIGNED;
-    pValue.value.u32 = nValue;
-    SetValue(SourceValueProperty, FormatValue(pValue, GetSourceType()));
+    SetValue(SourceValueProperty, FormatValue(nValue, GetSourceType()));
 }
 
 ra::ByteAddress TriggerConditionViewModel::GetSourceAddress() const
@@ -499,18 +512,12 @@ ra::ByteAddress TriggerConditionViewModel::GetSourceAddress() const
 
 void TriggerConditionViewModel::SetTargetValue(unsigned int nValue)
 {
-    rc_typed_value_t pValue;
-    pValue.type = RC_VALUE_TYPE_UNSIGNED;
-    pValue.value.u32 = nValue;
-    SetValue(TargetValueProperty, FormatValue(pValue, GetTargetType()));
+    SetValue(TargetValueProperty, FormatValue(nValue, GetTargetType()));
 }
 
 void TriggerConditionViewModel::SetTargetValue(float fValue)
 {
-    rc_typed_value_t pValue;
-    pValue.type = RC_VALUE_TYPE_FLOAT;
-    pValue.value.f32 = fValue;
-    SetValue(TargetValueProperty, FormatValue(pValue, GetTargetType()));
+    SetValue(TargetValueProperty, FormatValue(fValue, GetTargetType()));
 }
 
 ra::ByteAddress TriggerConditionViewModel::GetTargetAddress() const
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index 4563439b..6ea78338 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -136,6 +136,8 @@ public:
     bool IsModifying() const { return IsModifying(GetType()); }
 
     static bool IsComparisonVisible(const ViewModelBase& vmItem, int nValue);
+    static std::wstring FormatValue(unsigned nValue, TriggerOperandType nType);
+    static std::wstring FormatValue(float fValue, TriggerOperandType nType);
 
     static const IntModelProperty RowColorProperty;
     Color GetRowColor() const { return Color(ra::to_unsigned(GetValue(RowColorProperty))); }
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 7f2e2c8f..5bd04c5a 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -154,7 +154,8 @@ public:
                     return false;
                 }
 
-                vmItems.SetItemValue(nIndex, *m_pBoundProperty, sValue);
+                vmItems.SetItemValue(nIndex, *m_pBoundProperty,
+                    ra::ui::viewmodels::TriggerConditionViewModel::FormatValue(fValue, nOperandType));
                 return true;
             }
 
@@ -172,7 +173,8 @@ public:
                         return false;
                     }
 
-                    vmItems.SetItemValue(nIndex, *m_pBoundProperty, sValue);
+                    vmItems.SetItemValue(nIndex, *m_pBoundProperty,
+                        ra::ui::viewmodels::TriggerConditionViewModel::FormatValue(nValue, nOperandType));
                     return true;
                 }
             }
@@ -189,7 +191,9 @@ public:
                     return false;
                 }
 
-                vmItems.SetItemValue(nIndex, *m_pBoundProperty, ra::StringPrintf(L"0x%02x", nValue));
+                vmItems.SetItemValue(nIndex, *m_pBoundProperty,
+                    ra::ui::viewmodels::TriggerConditionViewModel::FormatValue(nValue, nOperandType));
+
                 return true;
             }
         }


Test patch:
diff --git a/tests/data/models/LeaderboardModel_Tests.cpp b/tests/data/models/LeaderboardModel_Tests.cpp
index 5d478a3a..9fcc315b 100644
--- a/tests/data/models/LeaderboardModel_Tests.cpp
+++ b/tests/data/models/LeaderboardModel_Tests.cpp
@@ -192,6 +192,44 @@ public:
         Assert::IsNull(pLeaderboard);
     }
 
+    TEST_METHOD(TestDeserialize)
+    {
+        LeaderboardModelHarness leaderboard;
+
+        const std::string sSerialized = ":\"0xH1234=1\":\"0xH1234=2\":\"0xH1234=3\":\"0xH1234\":FRAMES:Title:Desc";
+        ra::Tokenizer pTokenizer(sSerialized);
+        pTokenizer.Consume(':');
+
+        Assert::IsTrue(leaderboard.Deserialize(pTokenizer));
+
+        Assert::AreEqual(std::string("0xH1234=1"), leaderboard.GetStartTrigger());
+        Assert::AreEqual(std::string("0xH1234=2"), leaderboard.GetCancelTrigger());
+        Assert::AreEqual(std::string("0xH1234=3"), leaderboard.GetSubmitTrigger());
+        Assert::AreEqual(std::string("0xH1234"), leaderboard.GetValueDefinition());
+        Assert::AreEqual(ValueFormat::Frames, leaderboard.GetValueFormat());
+        Assert::AreEqual(std::wstring(L"Title"), leaderboard.GetName());
+        Assert::AreEqual(std::wstring(L"Desc"), leaderboard.GetDescription());
+    }
+
+    TEST_METHOD(TestDeserializeEmpty)
+    {
+        LeaderboardModelHarness leaderboard;
+
+        const std::string sSerialized = ":\"\":\"\":\"\":\"\":::";
+        ra::Tokenizer pTokenizer(sSerialized);
+        pTokenizer.Consume(':');
+
+        Assert::IsTrue(leaderboard.Deserialize(pTokenizer));
+
+        Assert::AreEqual(std::string(), leaderboard.GetStartTrigger());
+        Assert::AreEqual(std::string(), leaderboard.GetCancelTrigger());
+        Assert::AreEqual(std::string(), leaderboard.GetSubmitTrigger());
+        Assert::AreEqual(std::string(), leaderboard.GetValueDefinition());
+        Assert::AreEqual(ValueFormat::Value, leaderboard.GetValueFormat());
+        Assert::AreEqual(std::wstring(), leaderboard.GetName());
+        Assert::AreEqual(std::wstring(), leaderboard.GetDescription());
+    }
+
     void TestSerializeValueFormat(ValueFormat nFormat, const std::string& sFormat)
     {
         LeaderboardModelHarness leaderboard;
diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp
index 274c8e03..951303ec 100644
--- a/tests/services/SearchResults_Tests.cpp
+++ b/tests/services/SearchResults_Tests.cpp
@@ -1257,7 +1257,8 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         // exclude doesn't do anything to unfiltered results
-        results1.ExcludeAddress(1U);
+        SearchResults::Result excludeResult{ 1U, 0U, MemSize::EightBit };
+        results1.ExcludeResult(excludeResult);
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         memory.at(1) = 0x14;
@@ -1272,7 +1273,7 @@ public:
         Assert::IsFalse(results.ContainsAddress(3U));
         Assert::IsFalse(results.ContainsAddress(4U));
 
-        results.ExcludeAddress(1U);
+        results.ExcludeResult(excludeResult);
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
         Assert::IsFalse(results.ContainsAddress(1U));
@@ -1287,6 +1288,105 @@ public:
         Assert::AreEqual(0x55U, result.nValue);
     }
 
+    TEST_METHOD(TestExcludeAddressFourBit)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(1U, 3U, ra::services::SearchType::FourBit);
+        Assert::AreEqual({ 6U }, results1.MatchingAddressCount());
+
+        // exclude doesn't do anything to unfiltered results
+        SearchResults::Result excludeResult{ 1U, 0U, MemSize::Nibble_Lower };
+        results1.ExcludeResult(excludeResult);
+        Assert::AreEqual({ 6U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14;
+        memory.at(2) = 0x65;
+        SearchResults results;
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
+
+        Assert::AreEqual({ 3U }, results.MatchingAddressCount());
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsTrue(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsFalse(results.ContainsAddress(4U));
+
+        results.ExcludeResult(excludeResult);
+        Assert::AreEqual({ 2U }, results.MatchingAddressCount());
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsFalse(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(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::Nibble_Lower, result.nSize);
+        Assert::AreEqual(0x5U, result.nValue);
+
+        excludeResult.nAddress = 2;
+        excludeResult.nSize = MemSize::Nibble_Upper;
+        results.ExcludeResult(excludeResult);
+        Assert::AreEqual({ 1U }, results.MatchingAddressCount());
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsFalse(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsFalse(results.ContainsAddress(4U));
+
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::Nibble_Lower, result.nSize);
+        Assert::AreEqual(0x5U, result.nValue);
+    }
+
+    TEST_METHOD(TestExcludeAddressSixteenBitAligned)
+    {
+        std::array<unsigned char, 6> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56, 0x88 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 5U, ra::services::SearchType::SixteenBitAligned);
+        Assert::AreEqual({ 2U }, results1.MatchingAddressCount());
+
+        // exclude doesn't do anything to unfiltered results
+        SearchResults::Result excludeResult{ 0U, 0U, MemSize::EightBit };
+        results1.ExcludeResult(excludeResult);
+        Assert::AreEqual({ 2U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14;
+        memory.at(2) = 0x55;
+        SearchResults results;
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
+
+        Assert::AreEqual({ 2U }, results.MatchingAddressCount());
+        Assert::IsTrue(results.ContainsAddress(0U));
+        Assert::IsFalse(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(results.ContainsAddress(3U));
+        Assert::IsFalse(results.ContainsAddress(4U));
+
+        results.ExcludeResult(excludeResult);
+        Assert::AreEqual({ 1U }, results.MatchingAddressCount());
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsFalse(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsFalse(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::SixteenBit, result.nSize);
+        Assert::AreEqual(0xAB55U, result.nValue);
+    }
+
     TEST_METHOD(TestInitializeFromMemoryEightBitLargeMemory)
     {
         auto memory = std::make_unique<unsigned char[]>(BIG_BLOCK_SIZE);
@@ -1722,7 +1822,8 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         // exclude doesn't do anything to unfiltered results
-        results1.ExcludeAddress(0U);
+        SearchResults::Result excludeResult{ 0U, 0U, MemSize::EightBit };
+        results1.ExcludeResult(excludeResult);
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         memory.at(1) = 0x14;
diff --git a/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp b/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp
index 54e212ca..af4882fc 100644
--- a/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp
@@ -1563,6 +1563,63 @@ public:
         Assert::IsNotNull(group.m_pConditionSet);
     }
 
+    TEST_METHOD(TestTriggerUnmodifiedUpdate)
+    {
+        AssetEditorViewModelHarness editor;
+        editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
+        AchievementModel achievement;
+        achievement.SetID(1234U);
+        achievement.SetTrigger("0xH1234=1_0xH1235=2");
+        achievement.CreateServerCheckpoint();
+        achievement.CreateLocalCheckpoint();
+        achievement.Activate();
+
+        editor.LoadAsset(&achievement);
+
+        // make sure the record got loaded into the runtime
+        const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U);
+        Expects(pTrigger != nullptr);
+
+        Assert::AreEqual({ 2U }, editor.Trigger().Conditions().Count());
+        editor.Trigger().Conditions().GetItemAt(1)->SetSelected(true);
+        editor.Trigger().MoveSelectedConditionsUp();
+
+        // make sure the trigger definition got updated
+        Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), achievement.GetTrigger());
+        Assert::IsNotNull(editor.Trigger().Groups().GetItemAt(0)->m_pConditionSet);
+
+        // make sure the UI picked up the updated value
+        Assert::AreEqual({ 2U }, editor.Trigger().Conditions().Count());
+        const auto* pCondition = editor.Trigger().Conditions().GetItemAt(0);
+        Expects(pCondition != nullptr);
+        Assert::AreEqual(std::wstring(L"0x1235"), pCondition->GetSourceValue());
+        Assert::AreEqual(std::wstring(L"2"), pCondition->GetTargetValue());
+        pCondition = editor.Trigger().Conditions().GetItemAt(1);
+        Expects(pCondition != nullptr);
+        Assert::AreEqual(std::wstring(L"0x1234"), pCondition->GetSourceValue());
+        Assert::AreEqual(std::wstring(L"1"), pCondition->GetTargetValue());
+
+        // second attempt to move will not actually do anything
+        Assert::IsTrue(editor.Trigger().Conditions().GetItemAt(0)->IsSelected());
+        Assert::IsFalse(editor.Trigger().Conditions().GetItemAt(1)->IsSelected());
+        editor.Trigger().MoveSelectedConditionsUp();
+
+        // make sure the trigger definition did not update
+        Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), achievement.GetTrigger());
+        Assert::IsNotNull(editor.Trigger().Groups().GetItemAt(0)->m_pConditionSet);
+
+        // make sure the UI did not update
+        Assert::AreEqual({ 2U }, editor.Trigger().Conditions().Count());
+        pCondition = editor.Trigger().Conditions().GetItemAt(0);
+        Expects(pCondition != nullptr);
+        Assert::AreEqual(std::wstring(L"0x1235"), pCondition->GetSourceValue());
+        Assert::AreEqual(std::wstring(L"2"), pCondition->GetTargetValue());
+        pCondition = editor.Trigger().Conditions().GetItemAt(1);
+        Expects(pCondition != nullptr);
+        Assert::AreEqual(std::wstring(L"0x1234"), pCondition->GetSourceValue());
+        Assert::AreEqual(std::wstring(L"1"), pCondition->GetTargetValue());
+    }
+
     TEST_METHOD(TestValueUpdated)
     {
         AssetEditorViewModelHarness editor;
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index de4388d4..e55f249b 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -995,6 +995,73 @@ public:
         Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
     }
 
+    TEST_METHOD(TestFormatValueNumber)
+    {
+        TriggerConditionViewModelHarness condition;
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
+
+        Assert::AreEqual(std::wstring(L"0"), TriggerConditionViewModel::FormatValue(0U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"1"), TriggerConditionViewModel::FormatValue(1U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"10"), TriggerConditionViewModel::FormatValue(10U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"100"), TriggerConditionViewModel::FormatValue(100U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"1000000000"), TriggerConditionViewModel::FormatValue(1000000000U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"4294967295"), TriggerConditionViewModel::FormatValue(0xFFFFFFFFU, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"12"), TriggerConditionViewModel::FormatValue(12.34f, TriggerOperandType::Value));
+
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, false);
+
+        Assert::AreEqual(std::wstring(L"0x00"), TriggerConditionViewModel::FormatValue(0U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0x01"), TriggerConditionViewModel::FormatValue(1U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0x0a"), TriggerConditionViewModel::FormatValue(10U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0x64"), TriggerConditionViewModel::FormatValue(100U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0x3b9aca00"), TriggerConditionViewModel::FormatValue(1000000000U, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0xffffffff"), TriggerConditionViewModel::FormatValue(0xFFFFFFFFU, TriggerOperandType::Value));
+        Assert::AreEqual(std::wstring(L"0x0c"), TriggerConditionViewModel::FormatValue(12.34f, TriggerOperandType::Value));
+    }
+
+    TEST_METHOD(TestFormatValueFloat)
+    {
+        TriggerConditionViewModelHarness condition;
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
+
+        Assert::AreEqual(std::wstring(L"0.0"), TriggerConditionViewModel::FormatValue(0.0f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"1.0"), TriggerConditionViewModel::FormatValue(1.0f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"1.23"), TriggerConditionViewModel::FormatValue(1.23f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"-3.14159"), TriggerConditionViewModel::FormatValue(-3.14159f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"960.75"), TriggerConditionViewModel::FormatValue(960.75f, TriggerOperandType::Float));
+
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, false);
+
+        Assert::AreEqual(std::wstring(L"0.0"), TriggerConditionViewModel::FormatValue(0.0f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"1.0"), TriggerConditionViewModel::FormatValue(1.0f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"1.23"), TriggerConditionViewModel::FormatValue(1.23f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"-3.14159"), TriggerConditionViewModel::FormatValue(-3.14159f, TriggerOperandType::Float));
+        Assert::AreEqual(std::wstring(L"960.75"), TriggerConditionViewModel::FormatValue(960.75f, TriggerOperandType::Float));
+    }
+
+    TEST_METHOD(TestFormatValueAddress)
+    {
+        TriggerConditionViewModelHarness condition;
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
+
+        Assert::AreEqual(std::wstring(L"0x0000"), TriggerConditionViewModel::FormatValue(0U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0xc3500"), TriggerConditionViewModel::FormatValue(800000U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Delta));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Prior));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::BCD));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660.25f, TriggerOperandType::Address));
+
+        condition.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, false);
+
+        Assert::AreEqual(std::wstring(L"0x0000"), TriggerConditionViewModel::FormatValue(0U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0xc3500"), TriggerConditionViewModel::FormatValue(800000U, TriggerOperandType::Address));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Delta));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::Prior));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660U, TriggerOperandType::BCD));
+        Assert::AreEqual(std::wstring(L"0x1234"), TriggerConditionViewModel::FormatValue(4660.25f, TriggerOperandType::Address));
+    }
 };
 
 } // namespace tests