Repository: RetroAchievements/RAIntegration
Pull request #935 (2022-10-28 00:31:25)

fix display of parse errors in rich presence monitor
Instead of saying "Rich presence not active".


Patch:
diff --git a/rcheevos b/rcheevos
index e99fa55b..beae1022 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit e99fa55b1cfeb665aab209cf0b3dd9970e9caa31
+Subproject commit beae1022e90f2cb8351093aa9380e749b201596a
diff --git a/src/data/context/ConsoleContext.cpp b/src/data/context/ConsoleContext.cpp
index 07ee2519..3d9020ec 100644
--- a/src/data/context/ConsoleContext.cpp
+++ b/src/data/context/ConsoleContext.cpp
@@ -24,6 +24,7 @@ ConsoleContext::ConsoleContext(ConsoleID nId) noexcept
             auto& pMemoryRegion = m_vRegions.emplace_back();
             pMemoryRegion.StartAddress = pRegion.start_address;
             pMemoryRegion.EndAddress = pRegion.end_address;
+            pMemoryRegion.RealAddress = pRegion.real_address;
             pMemoryRegion.Description = pRegion.description;
 
             switch (pRegion.type)
@@ -77,6 +78,22 @@ const ConsoleContext::MemoryRegion* ConsoleContext::GetMemoryRegion(ra::ByteAddr
     return nullptr;
 }
 
+ra::ByteAddress ConsoleContext::ByteAddressFromRealAddress(ra::ByteAddress nRealAddress) const noexcept
+{
+    for (const auto& pRegion : m_vRegions)
+    {
+        if (pRegion.RealAddress < nRealAddress)
+        {
+            const auto nRegionSize = pRegion.EndAddress - pRegion.StartAddress;
+            if (nRealAddress < pRegion.RealAddress + nRegionSize)
+                return (nRealAddress - pRegion.RealAddress) + pRegion.StartAddress;
+        }
+    }
+
+    return 0xFFFFFFFF;
+}
+
+
 } // namespace context
 } // namespace data
 } // namespace ra
diff --git a/src/data/context/ConsoleContext.hh b/src/data/context/ConsoleContext.hh
index e2211847..102ed9d9 100644
--- a/src/data/context/ConsoleContext.hh
+++ b/src/data/context/ConsoleContext.hh
@@ -79,6 +79,7 @@ public:
     {
         ra::ByteAddress StartAddress = 0U;
         ra::ByteAddress EndAddress = 0U;
+        ra::ByteAddress RealAddress = 0U;
         AddressType Type = AddressType::Unknown;
         std::string Description;
     };
@@ -94,6 +95,12 @@ public:
     /// <returns>Matching <see cref="MemoryRegion"/>, <c>nullptr</c> if not found.
     const MemoryRegion* GetMemoryRegion(ra::ByteAddress nAddress) const;
 
+    /// <summary>
+    /// Converts a real address into the "RetroAchievements" address where the data should be.
+    /// </summary>
+    /// <returns>Converted address, or <c>0xFFFFFFFF</c> if conversion could not be completed.
+    ra::ByteAddress ByteAddressFromRealAddress(ra::ByteAddress nRealAddress) const noexcept;
+
 protected:
     ConsoleID m_nId{};
     std::wstring m_sName;
diff --git a/src/data/models/CodeNotesModel.cpp b/src/data/models/CodeNotesModel.cpp
index e3d76afa..d10f019c 100644
--- a/src/data/models/CodeNotesModel.cpp
+++ b/src/data/models/CodeNotesModel.cpp
@@ -6,6 +6,7 @@
 #include "api\FetchCodeNotes.hh"
 #include "api\UpdateCodeNote.hh"
 
+#include "data\context\ConsoleContext.hh"
 #include "data\context\EmulatorContext.hh"
 #include "data\context\UserContext.hh"
 
@@ -303,6 +304,24 @@ void CodeNotesModel::ExtractSize(CodeNote& pNote)
     }
 }
 
+static unsigned ReadPointer(const ra::data::context::EmulatorContext& pEmulatorContext,
+    ra::ByteAddress nPointerAddress, MemSize nSize)
+{
+    auto nAddress = pEmulatorContext.ReadMemory(nPointerAddress, nSize);
+
+    // assume anything annotated as a 32-bit pointer is providing a real (non-translated) address and
+    // attempt to do the translation ourself.
+    if (nSize == MemSize::ThirtyTwoBit)
+    {
+        const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+        const auto nConvertedAddress = pConsoleContext.ByteAddressFromRealAddress(nAddress);
+        if (nConvertedAddress != 0xFFFFFFFF)
+            nAddress = nConvertedAddress;
+    }
+
+    return nAddress;
+}
+
 void CodeNotesModel::AddCodeNote(ra::ByteAddress nAddress, const std::string& sAuthor, const std::wstring& sNote)
 {
     auto nIndex = sNote.find(L'\n');
@@ -321,7 +340,8 @@ void CodeNotesModel::AddCodeNote(ra::ByteAddress nAddress, const std::string& sA
             {
                 OffsetCodeNote offsetNote;
                 const auto nNextIndex = sNote.find(L"\n+", nIndex);
-                const auto sNextNote = sNote.substr(nIndex, nNextIndex - nIndex);
+                auto sNextNote = sNote.substr(nIndex, nNextIndex - nIndex);
+                ra::Trim(sNextNote);
 
                 wchar_t* pEnd = nullptr;
 
@@ -375,7 +395,7 @@ void CodeNotesModel::AddCodeNote(ra::ByteAddress nAddress, const std::string& sA
 
                     // capture the initial value of the pointer
                     const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
-                    const auto nPointerValue = pEmulatorContext.ReadMemory(nAddress, pointerNote.MemSize);
+                    const auto nPointerValue = ReadPointer(pEmulatorContext, nAddress, pointerNote.MemSize);
                     pointerData->PointerValue = nPointerValue;
                     pointerNote.PointerData = std::move(pointerData);
 
@@ -670,6 +690,31 @@ const CodeNotesModel::CodeNote* CodeNotesModel::FindCodeNoteInternal(ra::ByteAdd
     return nullptr;
 }
 
+const std::wstring* CodeNotesModel::FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const noexcept
+{
+    if (!m_bHasPointers)
+        return nullptr;
+
+    for (const auto& pCodeNote : m_mCodeNotes)
+    {
+        if (pCodeNote.second.PointerData == nullptr)
+            continue;
+
+        if (nAddress == pCodeNote.first)
+        {
+            for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes)
+            {
+                if (pOffsetNote.Offset == ra::to_signed(nOffset))
+                    return &pOffsetNote.Note;
+            }
+
+            break;
+        }
+    }
+
+    return nullptr;
+}
+
 ra::ByteAddress CodeNotesModel::GetIndirectSource(ra::ByteAddress nAddress) const noexcept
 {
     if (m_bHasPointers)
@@ -829,7 +874,8 @@ void CodeNotesModel::DoFrame()
         if (!pNote.second.PointerData)
             continue;
 
-        const auto nNewAddress = pEmulatorContext.ReadMemory(pNote.first, pNote.second.MemSize);
+        const auto nNewAddress = ReadPointer(pEmulatorContext, pNote.first, pNote.second.MemSize);
+
         const auto nOldAddress = pNote.second.PointerData->PointerValue;
         if (nNewAddress == nOldAddress)
             continue;
diff --git a/src/data/models/CodeNotesModel.hh b/src/data/models/CodeNotesModel.hh
index 126b6ed9..3283e5b8 100644
--- a/src/data/models/CodeNotesModel.hh
+++ b/src/data/models/CodeNotesModel.hh
@@ -68,6 +68,12 @@ public:
     /// <remarks>Does not find notes derived from pointers</remarks>
     const std::wstring* FindCodeNote(ra::ByteAddress nAddress, _Inout_ std::string& sAuthor) const;
 
+    /// <summary>
+    /// Returns the note associated with the specified address.
+    /// </summary>
+    /// <returns>The note associated to the address, <c>nullptr</c> if no note is associated to the address.</returns>
+    const std::wstring* FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const noexcept;
+
     /// <summary>
     /// Returns the number of bytes associated to the code note at the specified address.
     /// </summary>
diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp
index ece8a8c4..5c441b83 100644
--- a/src/services/SearchResults.cpp
+++ b/src/services/SearchResults.cpp
@@ -978,6 +978,64 @@ protected:
     }
 };
 
+class BitCountSearchImpl : public SearchImpl
+{
+protected:
+    // captured value (result.nValue) is actually the raw byte
+    MemSize GetMemSize() const noexcept override { return MemSize::BitCount; }
+
+    unsigned int BuildValue(const unsigned char* ptr) const noexcept override
+    {
+        GSL_SUPPRESS_F6 Expects(ptr != nullptr);
+        return GetBitCount(ptr[0]);
+    }
+
+    bool UpdateValue(const SearchResults& pResults, SearchResults::Result& pResult,
+        _Out_ std::wstring* sFormattedValue, const ra::data::context::EmulatorContext& pEmulatorContext) const override
+    {
+        const unsigned int nPreviousValue = pResult.nValue;
+        pResult.nValue = pEmulatorContext.ReadMemory(pResult.nAddress, MemSize::EightBit);
+
+        if (sFormattedValue)
+            *sFormattedValue = GetFormattedValue(pResults, pResult);
+
+        return (pResult.nValue != nPreviousValue);
+    }
+
+    std::wstring GetFormattedValue(const SearchResults&, const SearchResults::Result& pResult) const override
+    {
+        return ra::StringPrintf(L"%u (%c%c%c%c%c%c%c%c)", GetBitCount(pResult.nValue),
+            (pResult.nValue & 0x80) ? '1' : '0',
+            (pResult.nValue & 0x40) ? '1' : '0',
+            (pResult.nValue & 0x20) ? '1' : '0',
+            (pResult.nValue & 0x10) ? '1' : '0',
+            (pResult.nValue & 0x08) ? '1' : '0',
+            (pResult.nValue & 0x04) ? '1' : '0',
+            (pResult.nValue & 0x02) ? '1' : '0',
+            (pResult.nValue & 0x01) ? '1' : '0');
+    }
+
+private:
+    bool GetValueFromMemBlock(const impl::MemBlock& block, SearchResults::Result& result) const noexcept override
+    {
+        if (result.nAddress < block.GetFirstAddress())
+            return false;
+
+        const unsigned int nOffset = result.nAddress - block.GetFirstAddress();
+        if (nOffset >= block.GetBytesSize() - GetPadding())
+            return false;
+
+        result.nValue = *(block.GetBytes() + nOffset);
+        return true;
+    }
+
+    static unsigned int GetBitCount(unsigned nValue) noexcept
+    {
+        static const std::array<unsigned,16> vBits = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 };
+        return vBits.at(nValue & 0x0F) + vBits.at((nValue >> 4) & 0x0F);
+    }
+};
+
 class AsciiTextSearchImpl : public SearchImpl
 {
 public:
@@ -1385,6 +1443,7 @@ static SixteenBitAlignedSearchImpl s_pSixteenBitAlignedSearchImpl;
 static ThirtyTwoBitAlignedSearchImpl s_pThirtyTwoBitAlignedSearchImpl;
 static SixteenBitBigEndianSearchImpl s_pSixteenBitBigEndianSearchImpl;
 static ThirtyTwoBitBigEndianSearchImpl s_pThirtyTwoBitBigEndianSearchImpl;
+static BitCountSearchImpl s_pBitCountSearchImpl;
 static AsciiTextSearchImpl s_pAsciiTextSearchImpl;
 static FloatSearchImpl s_pFloatSearchImpl;
 static MBF32SearchImpl s_pMBF32SearchImpl;
@@ -1582,6 +1641,9 @@ void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchTy
         case SearchType::ThirtyTwoBitBigEndian:
             m_pImpl = &ra::services::impl::s_pThirtyTwoBitBigEndianSearchImpl;
             break;
+        case SearchType::BitCount:
+            m_pImpl = &ra::services::impl::s_pBitCountSearchImpl;
+            break;
         case SearchType::AsciiText:
             m_pImpl = &ra::services::impl::s_pAsciiTextSearchImpl;
             break;
diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h
index 171026e0..6f79f2ea 100644
--- a/src/services/SearchResults.h
+++ b/src/services/SearchResults.h
@@ -23,6 +23,7 @@ enum class SearchType
     MBF32,
     MBF32LE,
     AsciiText,
+    BitCount,
 };
 
 enum class SearchFilterType
diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp
index 157468ec..abd27fcc 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.cpp
+++ b/src/ui/viewmodels/MemorySearchViewModel.cpp
@@ -102,6 +102,7 @@ MemorySearchViewModel::MemorySearchViewModel()
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::ThirtyTwoBitAligned), L"32-bit (aligned)");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::SixteenBitBigEndian), L"16-bit BE");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::ThirtyTwoBitBigEndian), L"32-bit BE");
+    m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::BitCount), L"BitCount");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::Float), L"Float");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::MBF32), L"MBF32");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::MBF32LE), L"MBF32 LE");
diff --git a/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp b/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp
index 03140750..63830df4 100644
--- a/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp
+++ b/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp
@@ -157,23 +157,30 @@ void RichPresenceMonitorViewModel::UpdateDisplayString()
         {
             SetDisplayString(DisplayStringProperty.GetDefaultValue());
         }
-        else if (pRichPresence->GetState() != ra::data::models::AssetState::Active)
-        {
-            SetDisplayString(L"Rich Presence not active.");
-        }
         else
         {
-            auto& pRuntime = ra::services::ServiceLocator::Get<ra::services::AchievementRuntime>();
-            const std::wstring sDisplayString = ra::Widen(pRuntime.GetRichPresenceDisplayString());
-            SetDisplayString(sDisplayString);
-
-            if (m_nState == MonitorState::Static)
+            switch (pRichPresence->GetState())
             {
-                m_nState = MonitorState::Active;
-                ScheduleUpdateDisplayString();
-            }
+                case ra::data::models::AssetState::Active:
+                case ra::data::models::AssetState::Disabled: // parse error, still display it
+                {
+                    auto& pRuntime = ra::services::ServiceLocator::Get<ra::services::AchievementRuntime>();
+                    const std::wstring sDisplayString = ra::Widen(pRuntime.GetRichPresenceDisplayString());
+                    SetDisplayString(sDisplayString);
 
-            return;
+                    if (m_nState == MonitorState::Static)
+                    {
+                        m_nState = MonitorState::Active;
+                        ScheduleUpdateDisplayString();
+                    }
+
+                    return;
+                }
+
+                default:
+                    SetDisplayString(L"Rich Presence not active.");
+                    break;
+            }
         }
     }
 
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 77d584be..522ae809 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -552,9 +552,14 @@ std::wstring TriggerConditionViewModel::GetTooltip(const StringModelProperty& nP
             return L"";
 
         if (IsIndirect())
-            return GetAddressTooltip(GetIndirectAddress(GetSourceAddress()), true);
+        {
+            ra::ByteAddress nPointerAddress = 0;
+            const auto nOffset = GetSourceAddress();
+            const auto nIndirectAddress = GetIndirectAddress(nOffset, &nPointerAddress);
+            return GetAddressTooltip(nIndirectAddress, nPointerAddress, nOffset);
+        }
 
-        return GetAddressTooltip(GetSourceAddress(), false);
+        return GetAddressTooltip(GetSourceAddress(), 0, 0);
     }
 
     if (nProperty == TargetValueProperty)
@@ -567,9 +572,14 @@ std::wstring TriggerConditionViewModel::GetTooltip(const StringModelProperty& nP
             return L"";
 
         if (IsIndirect())
-            return GetAddressTooltip(GetIndirectAddress(GetTargetAddress()), true);
+        {
+            ra::ByteAddress nPointerAddress = 0;
+            const auto nOffset = GetTargetAddress();
+            const auto nIndirectAddress = GetIndirectAddress(nOffset, &nPointerAddress);
+            return GetAddressTooltip(nIndirectAddress, nPointerAddress, nOffset);
+        }
 
-        return GetAddressTooltip(GetTargetAddress(), false);
+        return GetAddressTooltip(GetTargetAddress(), 0, 0);
     }
 
     return L"";
@@ -589,8 +599,11 @@ static bool IsIndirectMemref(const rc_operand_t& operand) noexcept
     return rc_operand_is_memref(&operand) && operand.value.memref->value.is_indirect;
 }
 
-unsigned int TriggerConditionViewModel::GetIndirectAddress(unsigned int nAddress) const
+ra::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::ByteAddress nAddress, ra::ByteAddress* pPointerAddress) const
 {
+    if (pPointerAddress != nullptr)
+        *pPointerAddress = 0xFFFFFFFF;
+
     const auto* pTriggerViewModel = dynamic_cast<const TriggerViewModel*>(m_pTriggerViewModel);
     if (pTriggerViewModel == nullptr)
         return nAddress;
@@ -678,6 +691,10 @@ unsigned int TriggerConditionViewModel::GetIndirectAddress(unsigned int nAddress
                 rc_evaluate_condition_value(&value, pCondition, &oEvalState);
                 rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED);
                 oEvalState.add_address = value.value.u32;
+
+                if (pPointerAddress != nullptr && rc_operand_is_memref(&pCondition->operand1))
+                    *pPointerAddress = (bIsIndirect) ? 0xFFFFFFFF : pCondition->operand1.value.memref->address;
+
                 bIsIndirect = true;
             }
         }
@@ -691,29 +708,46 @@ unsigned int TriggerConditionViewModel::GetIndirectAddress(unsigned int nAddress
     return nAddress;
 }
 
-std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned int nAddress, bool bIsIndirect)
+std::wstring TriggerConditionViewModel::GetAddressTooltip(ra::ByteAddress nAddress,
+    ra::ByteAddress nPointerAddress, ra::ByteAddress nOffset)
 {
     const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
     const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes();
+    const std::wstring* pNote = nullptr;
     std::wstring sAddress;
 
-    const auto nStartAddress = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNoteStart(nAddress) : 0xFFFFFFFF;
-    if (nStartAddress != nAddress && nStartAddress != 0xFFFFFFFF)
+    if (pCodeNotes == nullptr)
+        return ra::StringPrintf(L"%s%s\r\n[No code note]", ra::ByteAddressToString(nAddress), nOffset ? L" (indirect)" : L"");
+
+    if (nOffset)
     {
-        sAddress = ra::StringPrintf(L"%s [%s+%d]", ra::ByteAddressToString(nAddress),
-            ra::ByteAddressToString(nStartAddress), nAddress - nStartAddress);
+        sAddress = ra::StringPrintf(L"%s (indirect)", ra::ByteAddressToString(nAddress));
+
+        if (nPointerAddress != 0xFFFFFFFF)
+            pNote = pCodeNotes->FindIndirectCodeNote(nPointerAddress, nOffset);
+        else
+            pNote = pCodeNotes->FindCodeNote(nAddress);
     }
     else
     {
         sAddress = ra::Widen(ra::ByteAddressToString(nAddress));
+        pNote = pCodeNotes->FindCodeNote(nAddress);
     }
 
-    if (bIsIndirect)
-        sAddress.append(L" (indirect)");
-
-    const auto* pNote = (nStartAddress != 0xFFFFFFFF) ? pCodeNotes->FindCodeNote(nStartAddress) : nullptr;
     if (!pNote)
-        return ra::StringPrintf(L"%s\r\n[No code note]", sAddress);
+    {
+        const auto nStartAddress = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNoteStart(nAddress) : 0xFFFFFFFF;
+        if (nStartAddress != nAddress && nStartAddress != 0xFFFFFFFF)
+        {
+            sAddress = ra::StringPrintf(L"%s [%s+%d]%s", ra::ByteAddressToString(nAddress),
+                                        ra::ByteAddressToString(nStartAddress), nAddress - nStartAddress,
+                                        nOffset ? L" (indirect)" : L"");
+            pNote = pCodeNotes->FindCodeNote(nStartAddress);
+        }
+
+        if (!pNote)
+            return ra::StringPrintf(L"%s\r\n[No code note]", sAddress);
+    }
 
     // limit the tooltip to the first 20 lines of the code note
     size_t nLines = 0;
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index fc05d4e1..29c1167a 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -120,7 +120,7 @@ public:
     static const BoolModelProperty IsIndirectProperty;
     bool IsIndirect() const { return GetValue(IsIndirectProperty); }
     void SetIndirect(bool bValue) { SetValue(IsIndirectProperty, bValue); }
-    unsigned int GetIndirectAddress(unsigned int nAddress) const;
+    ra::ByteAddress GetIndirectAddress(ra::ByteAddress nAddress, ra::ByteAddress* pPointerAddress) const;
 
     static const BoolModelProperty IsSelectedProperty;
     bool IsSelected() const { return GetValue(IsSelectedProperty); }
@@ -152,7 +152,7 @@ private:
     void SerializeAppendOperand(std::string& sBuffer, TriggerOperandType nType, MemSize nSize, const std::wstring& nValue) const;
 
     static std::wstring GetValueTooltip(unsigned int nValue);
-    static std::wstring GetAddressTooltip(unsigned int nAddress, bool bIsIndirect);
+    static std::wstring GetAddressTooltip(ra::ByteAddress nAddress, ra::ByteAddress nPointerAddress, ra::ByteAddress nOffset);
     ra::ByteAddress GetSourceAddress() const;
     ra::ByteAddress GetTargetAddress() const;
     void SetOperand(const IntModelProperty& pTypeProperty, const IntModelProperty& pSizeProperty,
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 233ba305..63333323 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -232,7 +232,7 @@ public:
             {
                 const auto* pConditionViewModel = dynamic_cast<const TriggerConditionViewModel*>(vmItems.GetViewModelAt(nIndex));
                 Expects(pConditionViewModel != nullptr);
-                nAddress = pConditionViewModel->GetIndirectAddress(nAddress);
+                nAddress = pConditionViewModel->GetIndirectAddress(nAddress, nullptr);
             }
 
             auto& pMemoryInspector = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryInspector;
diff --git a/src/ui/win32/MemoryInspectorDialog.cpp b/src/ui/win32/MemoryInspectorDialog.cpp
index f982fa58..e524fbce 100644
--- a/src/ui/win32/MemoryInspectorDialog.cpp
+++ b/src/ui/win32/MemoryInspectorDialog.cpp
@@ -94,7 +94,9 @@ void MemoryInspectorDialog::SearchResultsGridBinding::UpdateColumnWidths()
 
     // value column
     auto nMaxChars = (ra::data::MemSizeBits(nSize) + 3) / 4; // 4 bits per nibble
-    if (nMaxChars == 0)
+    if (nSize == MemSize::BitCount)
+        nMaxChars = 9;
+    else if (nMaxChars == 0)
         nMaxChars = 16;
     nWidth = nCharWidth * (nMaxChars + 2) + nPadding * 2;
     m_vColumns.at(1)->SetWidth(GridColumnBinding::WidthType::Pixels, nWidth);


Test patch:
diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj
index 78c10887..66dd1fed 100644
--- a/tests/RA_Integration.Tests.vcxproj
+++ b/tests/RA_Integration.Tests.vcxproj
@@ -453,6 +453,7 @@
     <ClCompile Include="..\src\ui\viewmodels\MessageBoxViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\RichPresenceMonitorViewModel.cpp" />
     <ClCompile Include="..\src\ui\WindowViewModelBase.cpp" />
+    <ClCompile Include="data\context\ConsoleContext_Tests.cpp" />
     <ClCompile Include="data\context\EmulatorContext_Tests.cpp" />
     <ClCompile Include="data\context\GameAssets_Tests.cpp" />
     <ClCompile Include="data\context\GameContext_Tests.cpp" />
diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters
index 54420dda..a8c83093 100644
--- a/tests/RA_Integration.Tests.vcxproj.filters
+++ b/tests/RA_Integration.Tests.vcxproj.filters
@@ -447,6 +447,9 @@
     <ClCompile Include="..\src\ui\viewmodels\EmulatorViewModel.cpp">
       <Filter>Code</Filter>
     </ClCompile>
+    <ClCompile Include="data\context\ConsoleContext_Tests.cpp">
+      <Filter>Tests\Data\Context</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <None Include="base.props" />
diff --git a/tests/data/context/ConsoleContext_Tests.cpp b/tests/data/context/ConsoleContext_Tests.cpp
new file mode 100644
index 00000000..234db269
--- /dev/null
+++ b/tests/data/context/ConsoleContext_Tests.cpp
@@ -0,0 +1,118 @@
+#include "CppUnitTest.h"
+
+#include "data\context\ConsoleContext.hh"
+
+#include "tests\data\DataAsserts.hh"
+
+using namespace Microsoft::VisualStudio::CppUnitTestFramework;
+
+namespace ra {
+namespace data {
+namespace context {
+namespace tests {
+
+TEST_CLASS(ConsoleContext_Tests)
+{
+private:
+    static void AssertRegion(const ConsoleContext& context, ra::ByteAddress nAddress,
+        const ConsoleContext::MemoryRegion* pExpectedRegion)
+    {
+        const auto* pFoundRegion = context.GetMemoryRegion(nAddress);
+        if (pFoundRegion != pExpectedRegion)
+        {
+            if (pExpectedRegion == nullptr)
+                Assert::Fail(ra::StringPrintf(L"Found region for address %08x", nAddress).c_str());
+            else if (pFoundRegion == nullptr)
+                Assert::Fail(ra::StringPrintf(L"Did not find region for address %08x", nAddress).c_str());
+            else
+                Assert::Fail(ra::StringPrintf(L"Found wrong region for address %08x", nAddress).c_str());
+        }
+    }
+
+public:
+    TEST_METHOD(TestAtari2600)
+    {
+        ConsoleContext context(ConsoleID::Atari2600);
+
+        Assert::AreEqual(25, (int)context.Id());
+        Assert::AreEqual(std::wstring(L"Atari 2600"), context.Name());
+
+        const auto& vRegions = context.MemoryRegions();
+        Assert::AreEqual({1}, vRegions.size());
+        Assert::AreEqual(0x00U, vRegions.at(0).StartAddress);
+        Assert::AreEqual(0x7FU, vRegions.at(0).EndAddress);
+        Assert::AreEqual(0x80U, vRegions.at(0).RealAddress);
+        Assert::AreEqual((int)ConsoleContext::AddressType::SystemRAM, (int)vRegions.at(0).Type);
+        Assert::AreEqual(std::string("System RAM"), vRegions.at(0).Description);
+
+        AssertRegion(context, 0x00U, &vRegions.at(0));
+        AssertRegion(context, 0x7FU, &vRegions.at(0));
+        AssertRegion(context, 0x80U, nullptr);
+
+        Assert::AreEqual(0x14U, context.ByteAddressFromRealAddress(0x94U));
+        Assert::AreEqual(0xFFFFFFFFU, context.ByteAddressFromRealAddress(0x14U));
+    }
+
+    TEST_METHOD(TestGameboyAdvance)
+    {
+        ConsoleContext context(ConsoleID::GBA);
+
+        Assert::AreEqual(5, (int)context.Id());
+        Assert::AreEqual(std::wstring(L"GameBoy Advance"), context.Name());
+
+        const auto& vRegions = context.MemoryRegions();
+        Assert::AreEqual({2}, vRegions.size());
+        Assert::AreEqual(0x00000000U, vRegions.at(0).StartAddress);
+        Assert::AreEqual(0x00007FFFU, vRegions.at(0).EndAddress);
+        Assert::AreEqual(0x03000000U, vRegions.at(0).RealAddress);
+        Assert::AreEqual((int)ConsoleContext::AddressType::SaveRAM, (int)vRegions.at(0).Type);
+        Assert::AreEqual(std::string("Cartridge RAM"), vRegions.at(0).Description);
+        Assert::AreEqual(0x00008000U, vRegions.at(1).StartAddress);
+        Assert::AreEqual(0x00047FFFU, vRegions.at(1).EndAddress);
+        Assert::AreEqual(0x02000000U, vRegions.at(1).RealAddress);
+        Assert::AreEqual((int)ConsoleContext::AddressType::SystemRAM, (int)vRegions.at(1).Type);
+        Assert::AreEqual(std::string("System RAM"), vRegions.at(1).Description);
+
+        AssertRegion(context, 0x00000000U, &vRegions.at(0));
+        AssertRegion(context, 0x00007FFFU, &vRegions.at(0));
+        AssertRegion(context, 0x00008000U, &vRegions.at(1));
+        AssertRegion(context, 0x00047FFFU, &vRegions.at(1));
+        AssertRegion(context, 0x00048000U, nullptr);
+
+        Assert::AreEqual(0x00019234U, context.ByteAddressFromRealAddress(0x02011234U));
+    }
+
+    TEST_METHOD(TestPlayStationPortable)
+    {
+        ConsoleContext context(ConsoleID::PSP);
+
+        Assert::AreEqual(41, (int)context.Id());
+        Assert::AreEqual(std::wstring(L"PlayStation Portable"), context.Name());
+
+        const auto& vRegions = context.MemoryRegions();
+        Assert::AreEqual({2}, vRegions.size());
+        Assert::AreEqual(0x00000000U, vRegions.at(0).StartAddress);
+        Assert::AreEqual(0x007FFFFFU, vRegions.at(0).EndAddress);
+        Assert::AreEqual(0x08000000U, vRegions.at(0).RealAddress);
+        Assert::AreEqual((int)ConsoleContext::AddressType::SystemRAM, (int)vRegions.at(0).Type);
+        Assert::AreEqual(std::string("Kernel RAM"), vRegions.at(0).Description);
+        Assert::AreEqual(0x00800000U, vRegions.at(1).StartAddress);
+        Assert::AreEqual(0x01FFFFFFU, vRegions.at(1).EndAddress);
+        Assert::AreEqual(0x08800000U, vRegions.at(1).RealAddress);
+        Assert::AreEqual((int)ConsoleContext::AddressType::SystemRAM, (int)vRegions.at(1).Type);
+        Assert::AreEqual(std::string("System RAM"), vRegions.at(1).Description);
+
+        AssertRegion(context, 0x00000000U, &vRegions.at(0));
+        AssertRegion(context, 0x007FFFFFU, &vRegions.at(0));
+        AssertRegion(context, 0x00800000U, &vRegions.at(1));
+        AssertRegion(context, 0x01FFFFFFU, &vRegions.at(1));
+        AssertRegion(context, 0x02000000U, nullptr);
+
+        Assert::AreEqual(0x01123456U, context.ByteAddressFromRealAddress(0x09123456U));
+    }
+};
+
+} // namespace tests
+} // namespace context
+} // namespace data
+} // namespace ra
diff --git a/tests/data/models/CodeNotesModel_Tests.cpp b/tests/data/models/CodeNotesModel_Tests.cpp
index 6380d4e1..d9dd5dbe 100644
--- a/tests/data/models/CodeNotesModel_Tests.cpp
+++ b/tests/data/models/CodeNotesModel_Tests.cpp
@@ -7,6 +7,7 @@
 #include "tests\RA_UnitTestHelpers.h"
 #include "tests\data\DataAsserts.hh"
 
+#include "tests\mocks\MockConsoleContext.hh"
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockServer.hh"
@@ -27,6 +28,7 @@ private:
     {
     public:
         ra::api::mocks::MockServer mockServer;
+        ra::data::context::mocks::MockConsoleContext mockConsoleContext;
         ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
         ra::data::context::mocks::MockUserContext mockUserContext;
         ra::services::mocks::MockThreadPool mockThreadPool;
@@ -90,6 +92,14 @@ private:
             }
         }
 
+        void AssertIndirectNote(ra::ByteAddress nAddress, unsigned nOffset, const std::wstring& sExpected)
+        {
+            const auto* pNote = FindIndirectCodeNote(nAddress, nOffset);
+            Assert::IsNotNull(pNote, ra::StringPrintf(L"Note not found for address %04X + %u", nAddress, nOffset).c_str());
+            Ensures(pNote != nullptr);
+            Assert::AreEqual(sExpected, *pNote);
+        }
+
         void AssertSerialize(const std::string& sExpected)
         {
             std::string sSerialized = "N0";
@@ -557,12 +567,12 @@ public:
     {
         CodeNotesModelHarness notes;
         const std::wstring sNote =
-            L"Pointer\n"
-            L"[OFFSETS]\n"
-            L"+2 = EXP (32-bit)\n"
-            L"+5 = Base Level (8-bit)\n"
-            L"+6 = Job Level (8-bit)\n"
-            L"+20 = Stat Points (16-bit)\n"
+            L"Pointer\r\n"
+            L"[OFFSETS]\r\n"
+            L"+2 = EXP (32-bit)\r\n"
+            L"+5 = Base Level (8-bit)\r\n"
+            L"+6 = Job Level (8-bit)\r\n"
+            L"+20 = Stat Points (16-bit)\r\n"
             L"+22 = Skill Points (8-bit)";
         notes.AddCodeNote(1234, "Author", sNote);
 
@@ -666,6 +676,26 @@ public:
         Assert::AreEqual(std::wstring(), notes.FindCodeNote(18, MemSize::SixteenBit));
     }
 
+    TEST_METHOD(TestFindIndirectCodeNote)
+    {
+        CodeNotesModelHarness notes;
+        const std::wstring sNote =
+            L"Pointer\r\n"
+            L"[OFFSETS]\r\n"
+            L"+2 = EXP (32-bit)\r\n"
+            L"+5 = Base Level (8-bit)\r\n"
+            L"+6 = Job Level (8-bit)\r\n"
+            L"+20 = Stat Points (16-bit)\r\n"
+            L"+22 = Skill Points (8-bit)";
+        notes.AddCodeNote(1234, "Author", sNote);
+
+        notes.AssertIndirectNote(1234U, 5, L"Base Level (8-bit)");
+        notes.AssertIndirectNote(1234U, 22, L"Skill Points (8-bit)");
+        Assert::IsNull(notes.FindIndirectCodeNote(1234U, 0)); // no offset
+        Assert::IsNull(notes.FindIndirectCodeNote(1234U, 21)); // unknown offset
+        Assert::IsNull(notes.FindIndirectCodeNote(1235U, 5)); // wrong base address
+    }
+
     TEST_METHOD(TestEnumerateCodeNotes)
     {
         CodeNotesModelHarness notes;
@@ -835,6 +865,53 @@ public:
         Assert::AreEqual({0U}, notes.mNewNotes.size());
     }
 
+    TEST_METHOD(TestDoFrameRealAddressConversion)
+    {
+        CodeNotesModelHarness notes;
+        notes.MonitorCodeNoteChanges();
+        notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80);
+
+        std::array<unsigned char, 32> memory{};
+        for (uint8_t i = 4; i < memory.size(); i++)
+            memory.at(i) = i;
+        notes.mockEmulatorContext.MockMemory(memory);
+        memory.at(0) = 0x90; // start with initial value for pointer (real address = 0x90, RA address = 0x10)
+
+        const std::wstring sNote =
+            L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion
+            L"+1 = Small (8-bit)\n"
+            L"+2 = Medium (16-bit)\n"
+            L"+4 = Large (32-bit)";
+        notes.AddCodeNote(0x0000, "Author", sNote);
+
+        // should receive notifications for the pointer note, and for each subnote
+        Assert::AreEqual({4U}, notes.mNewNotes.size());
+        Assert::AreEqual(sNote, notes.mNewNotes[0x00]);
+        Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x11]);
+        Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x12]);
+        Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x14]);
+
+        notes.AssertNoNote(0x02U);
+        notes.AssertNote(0x12U, L"Medium (16-bit)", MemSize::SixteenBit, 2);
+
+        // calling DoFrame after updating the pointer should notify about all the affected subnotes
+        notes.mNewNotes.clear();
+        memory.at(0) = 0x88;
+        notes.DoFrame();
+
+        Assert::AreEqual({6U}, notes.mNewNotes.size());
+        Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x11]);
+        Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x12]);
+        Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x14]);
+        Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x09]);
+        Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x0A]);
+        Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x0C]);
+
+        notes.AssertNoNote(0x02U);
+        notes.AssertNoNote(0x12U);
+        notes.AssertNote(0x0AU, L"Medium (16-bit)", MemSize::SixteenBit, 2);
+    }
+
     TEST_METHOD(TestFindCodeNoteStartPointer)
     {
         CodeNotesModelHarness notes;
diff --git a/tests/mocks/MockConsoleContext.hh b/tests/mocks/MockConsoleContext.hh
index 64cf3b3d..3ffd6913 100644
--- a/tests/mocks/MockConsoleContext.hh
+++ b/tests/mocks/MockConsoleContext.hh
@@ -31,11 +31,19 @@ public:
 
     void ResetMemoryRegions() noexcept { m_vRegions.clear(); }
 
-    void AddMemoryRegion(ra::ByteAddress nStartAddress, ra::ByteAddress nEndAddress, AddressType nAddressType, const std::string& sDescription = "")
+    void AddMemoryRegion(ra::ByteAddress nStartAddress, ra::ByteAddress nEndAddress, 
+        AddressType nAddressType, const std::string& sDescription = "")
+    { 
+        AddMemoryRegion(nStartAddress, nEndAddress, nAddressType, nStartAddress, sDescription);
+    }
+
+    void AddMemoryRegion(ra::ByteAddress nStartAddress, ra::ByteAddress nEndAddress, 
+        AddressType nAddressType, ra::ByteAddress nRealAddress, const std::string& sDescription = "")
     { 
         auto& pRegion = m_vRegions.emplace_back();
         pRegion.StartAddress = nStartAddress;
         pRegion.EndAddress = nEndAddress;
+        pRegion.RealAddress = nRealAddress;
         pRegion.Type = nAddressType;
         pRegion.Description = sDescription;
     }
diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp
index 9371f583..a8531b9c 100644
--- a/tests/services/SearchResults_Tests.cpp
+++ b/tests/services/SearchResults_Tests.cpp
@@ -2171,6 +2171,141 @@ public:
 
         Assert::AreEqual(std::wstring(), results.GetFormattedValue(0U, MemSize::Text));
     }
+
+    TEST_METHOD(TestInitializeFromMemoryBitCount)
+    {
+        std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(1U, 3U, ra::services::SearchType::BitCount);
+
+        Assert::AreEqual({ 3U }, results.MatchingAddressCount());
+
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsTrue(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(1U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0x12U, result.nValue); // captured values are raw bytes for GetFormattedValue
+
+        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0x34U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(2U, result));
+        Assert::AreEqual(3U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0xABU, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsBitCountEqualsPrevious)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 5U, ra::services::SearchType::BitCount);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14; // 0x12 (3) => 0x14 (3)
+        memory.at(2) = 0x55; // 0x34 (3) => 0x55 (4)
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
+
+        Assert::AreEqual({ 4U }, results2.MatchingAddressCount());
+        Assert::IsTrue(results2.ContainsAddress(0U));
+        Assert::IsTrue(results2.ContainsAddress(1U));
+        Assert::IsFalse(results2.ContainsAddress(2U));
+        Assert::IsTrue(results2.ContainsAddress(3U));
+        Assert::IsTrue(results2.ContainsAddress(4U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(2U, result));
+        Assert::AreEqual(3U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0xABU, result.nValue);
+
+        // no change - tests the "entire block matches" optimization
+        SearchResults results3;
+        memory.at(3) = 0x99; // 0xAB (5) => 0x99 (4)
+        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
+
+        Assert::AreEqual({ 3U }, results3.MatchingAddressCount());
+        Assert::IsTrue(results3.ContainsAddress(0U));
+        Assert::IsTrue(results3.ContainsAddress(1U));
+        Assert::IsFalse(results3.ContainsAddress(2U));
+        Assert::IsFalse(results3.ContainsAddress(3U));
+        Assert::IsTrue(results3.ContainsAddress(4U));
+
+        Assert::IsTrue(results3.GetMatchingAddress(2U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0x56U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsBitCountEqualsPreviousPlusOne)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 5U, ra::services::SearchType::BitCount);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14; // 0x12 (3) => 0x14 (3)
+        memory.at(2) = 0x55; // 0x34 (3) => 0x55 (4)
+        memory.at(3) = 0xAC; // 0xAB (5) => 0xAC (4)
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValuePlus, L"1");
+
+        Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
+        Assert::IsFalse(results2.ContainsAddress(0U));
+        Assert::IsFalse(results2.ContainsAddress(1U));
+        Assert::IsTrue(results2.ContainsAddress(2U));
+        Assert::IsFalse(results2.ContainsAddress(3U));
+        Assert::IsFalse(results2.ContainsAddress(4U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::BitCount, result.nSize);
+        Assert::AreEqual(0x55U, result.nValue);
+    }
+
+    TEST_METHOD(TestGetFormattedValueBitCount)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 8U, ra::services::SearchType::BitCount);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L"0 (00000000)"), results1.GetFormattedValue(0U, MemSize::BitCount));
+        Assert::AreEqual(std::wstring(L"2 (00010010)"), results1.GetFormattedValue(1U, MemSize::BitCount));
+        Assert::AreEqual(std::wstring(L"3 (00110100)"), results1.GetFormattedValue(2U, MemSize::BitCount));
+        Assert::AreEqual(std::wstring(L"5 (10101011)"), results1.GetFormattedValue(3U, MemSize::BitCount));
+        Assert::AreEqual(std::wstring(L"4 (01010110)"), results1.GetFormattedValue(4U, MemSize::BitCount));
+
+        memory.at(3) = 0x3B; // still 5 bits, but different value
+
+        std::wstring sFormattedValue;
+        SearchResults::Result pResult;
+        results1.GetMatchingAddress(3, pResult);
+        Assert::IsTrue(results1.UpdateValue(pResult, &sFormattedValue, mockEmulatorContext));
+        Assert::AreEqual(std::wstring(L"5 (00111011)"), sFormattedValue);
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
index 0db84252..e3979767 100644
--- a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
@@ -158,7 +158,7 @@ public:
         Assert::AreEqual(std::wstring(L""), search.GetFilterRange());
         Assert::AreEqual(std::wstring(L""), search.GetFilterValue());
 
-        Assert::AreEqual({ 12U }, search.SearchTypes().Count());
+        Assert::AreEqual({ 13U }, search.SearchTypes().Count());
         Assert::AreEqual((int)ra::services::SearchType::FourBit, search.SearchTypes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L"4-bit"), search.SearchTypes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)ra::services::SearchType::EightBit, search.SearchTypes().GetItemAt(1)->GetId());
@@ -175,14 +175,16 @@ public:
         Assert::AreEqual(std::wstring(L"16-bit BE"), search.SearchTypes().GetItemAt(6)->GetLabel());
         Assert::AreEqual((int)ra::services::SearchType::ThirtyTwoBitBigEndian, search.SearchTypes().GetItemAt(7)->GetId());
         Assert::AreEqual(std::wstring(L"32-bit BE"), search.SearchTypes().GetItemAt(7)->GetLabel());
-        Assert::AreEqual((int)ra::services::SearchType::Float, search.SearchTypes().GetItemAt(8)->GetId());
-        Assert::AreEqual(std::wstring(L"Float"), search.SearchTypes().GetItemAt(8)->GetLabel());
-        Assert::AreEqual((int)ra::services::SearchType::MBF32, search.SearchTypes().GetItemAt(9)->GetId());
-        Assert::AreEqual(std::wstring(L"MBF32"), search.SearchTypes().GetItemAt(9)->GetLabel());
-        Assert::AreEqual((int)ra::services::SearchType::MBF32LE, search.SearchTypes().GetItemAt(10)->GetId());
-        Assert::AreEqual(std::wstring(L"MBF32 LE"), search.SearchTypes().GetItemAt(10)->GetLabel());
-        Assert::AreEqual((int)ra::services::SearchType::AsciiText, search.SearchTypes().GetItemAt(11)->GetId());
-        Assert::AreEqual(std::wstring(L"ASCII Text"), search.SearchTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::BitCount, search.SearchTypes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"BitCount"), search.SearchTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::Float, search.SearchTypes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"Float"), search.SearchTypes().GetItemAt(9)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::MBF32, search.SearchTypes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"MBF32"), search.SearchTypes().GetItemAt(10)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::MBF32LE, search.SearchTypes().GetItemAt(11)->GetId());
+        Assert::AreEqual(std::wstring(L"MBF32 LE"), search.SearchTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::AsciiText, search.SearchTypes().GetItemAt(12)->GetId());
+        Assert::AreEqual(std::wstring(L"ASCII Text"), search.SearchTypes().GetItemAt(12)->GetLabel());
         Assert::AreEqual(ra::services::SearchType::EightBit, search.GetSearchType());
 
         Assert::AreEqual({ 6U }, search.ComparisonTypes().Count());
diff --git a/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp b/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp
index 5ddb0005..b92430f3 100644
--- a/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp
@@ -66,6 +66,38 @@ TEST_CLASS(RichPresenceMonitorViewModel_Tests)
         Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
     }
 
+    TEST_METHOD(TestUpdateDisplayStringParseError)
+    {
+        RichPresenceMonitorViewModelHarness vmRichPresence;
+
+        vmRichPresence.UpdateDisplayString();
+        Assert::AreEqual(std::wstring(L"No game loaded."), vmRichPresence.GetDisplayString());
+        Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
+
+        vmRichPresence.mockGameContext.SetGameId(1);
+        vmRichPresence.UpdateDisplayString();
+        Assert::AreEqual(std::wstring(L"No Rich Presence defined."), vmRichPresence.GetDisplayString());
+        Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
+
+        vmRichPresence.mockGameContext.SetRichPresenceDisplayString(L"Hello, world!");
+        vmRichPresence.mockGameContext.Assets().FindRichPresence()->SetScript(
+            "Lookup:a\n0=Zero\n1One\n\nDisplay:@a(0xH1234)");
+        vmRichPresence.mockGameContext.Assets().FindRichPresence()->Activate();
+        vmRichPresence.UpdateDisplayString();
+        Assert::AreEqual(std::wstring(L"Parse error -16 (line 3): Missing value expression"), vmRichPresence.GetDisplayString());
+        Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
+
+        vmRichPresence.mockGameContext.Assets().FindRichPresence()->SetScript("Display:Not Fixed");
+        vmRichPresence.UpdateDisplayString();
+        Assert::AreEqual(std::wstring(L"Parse error -18 (line 2): Missing display string"), vmRichPresence.GetDisplayString());
+        Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
+
+        vmRichPresence.mockGameContext.Assets().FindRichPresence()->SetScript("Display:\nFixed");
+        vmRichPresence.UpdateDisplayString();
+        Assert::AreEqual(std::wstring(L"Fixed"), vmRichPresence.GetDisplayString());
+        Assert::AreEqual({ 0U }, vmRichPresence.mockThreadPool.PendingTasks());
+    }
+
     TEST_METHOD(TestStartMonitoringNoGame)
     {
         RichPresenceMonitorViewModelHarness vmRichPresence;
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index 2f513e61..459ff7f8 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -6,6 +6,7 @@
 
 #include "tests\RA_UnitTestHelpers.h"
 #include "tests\mocks\MockConfiguration.hh"
+#include "tests\mocks\MockConsoleContext.hh"
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockUserContext.hh"
@@ -637,7 +638,7 @@ public:
         IndirectAddressTriggerViewModelHarness vmTrigger;
         vmTrigger.Parse("I:0xH0001_0xH0002=3");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 3U }, L"This is a note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+2=This is a note.");
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
         Expects(pCondition != nullptr);
@@ -648,7 +649,7 @@ public:
 
         // $0001 = 3, 3+2 = $0005
         vmTrigger.SetMemory({ 1 }, 3);
-        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
     }
 
     TEST_METHOD(TestTooltipIndirectAddressMultiNote)
@@ -656,8 +657,7 @@ public:
         IndirectAddressTriggerViewModelHarness vmTrigger;
         vmTrigger.Parse("I:0xH0001_0xH0002=0xH0004");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 3U }, L"This is a note.");
-        vmTrigger.mockGameContext.SetCodeNote({ 5U }, L"This is another note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+2=This is a note.\n+4=This is another note.");
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
         Expects(pCondition != nullptr);
@@ -669,24 +669,24 @@ public:
 
         // $0001 = 3, 3+2 = $0005, 3+4 = $0007
         vmTrigger.SetMemory({ 1 }, 3);
-        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is another note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
-        Assert::AreEqual(std::wstring(L"0x0007 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::TargetValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0007 (indirect)\r\nThis is another note."), pCondition->GetTooltip(TriggerConditionViewModel::TargetValueProperty));
     }
 
     TEST_METHOD(TestTooltipIndirectAddressMultiByteNote)
     {
         IndirectAddressTriggerViewModelHarness vmTrigger;
-        vmTrigger.Parse("I:0xH0001_0xH0002=0xH0004");
+        vmTrigger.Parse("I:0xH0001_0xH0002=0xH0005");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 4U }, L"[8 bytes] This is a note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+4=[8 bytes] This is a note.");
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
         Expects(pCondition != nullptr);
         Assert::IsTrue(pCondition->IsIndirect());
 
-        // $0001 = 1, 1+2 = $0003, 1+4 = $0005
+        // $0001 = 1, 1+2 = $0003, 1+5 = $0006
         Assert::AreEqual(std::wstring(L"0x0003 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
-        Assert::AreEqual(std::wstring(L"0x0005 [0x0004+1] (indirect)\r\n[8 bytes] This is a note."), pCondition->GetTooltip(TriggerConditionViewModel::TargetValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0006 [0x0005+1] (indirect)\r\n[8 bytes] This is a note."), pCondition->GetTooltip(TriggerConditionViewModel::TargetValueProperty));
     }
 
     TEST_METHOD(TestTooltipIndirectAddressMultiplyNoCodeNote)
@@ -712,7 +712,7 @@ public:
         IndirectAddressTriggerViewModelHarness vmTrigger;
         vmTrigger.Parse("I:0xH0001_0xH0002=3S0=0S1=1");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 3U }, L"This is a note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+2=This is a note.");
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
         Expects(pCondition != nullptr);
@@ -723,7 +723,7 @@ public:
 
         // $0001 = 3, 3+2 = $0005
         vmTrigger.SetMemory({ 1 }, 3);
-        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
     }
 
     TEST_METHOD(TestTooltipIndirectAddressInAlt1CodeNote)
@@ -731,7 +731,7 @@ public:
         IndirectAddressTriggerViewModelHarness vmTrigger;
         vmTrigger.Parse("0=0SI:0xH0001_0xH0002=3S1=1");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 3U }, L"This is a note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+2=This is a note.");
         vmTrigger.SetSelectedGroupIndex(1);
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
@@ -743,7 +743,7 @@ public:
 
         // $0001 = 3, 3+2 = $0005
         vmTrigger.SetMemory({ 1 }, 3);
-        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
     }
 
     TEST_METHOD(TestTooltipIndirectAddressInAlt2CodeNote)
@@ -751,7 +751,7 @@ public:
         IndirectAddressTriggerViewModelHarness vmTrigger;
         vmTrigger.Parse("0=0S1=1SI:0xH0001_0xH0002=3");
         vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true);
-        vmTrigger.mockGameContext.SetCodeNote({ 3U }, L"This is a note.");
+        vmTrigger.mockGameContext.SetCodeNote({ 1U }, L"[8-bit pointer]\n+2=This is a note.");
         vmTrigger.SetSelectedGroupIndex(2);
 
         const auto* pCondition = vmTrigger.Conditions().GetItemAt(1);
@@ -763,7 +763,7 @@ public:
 
         // $0001 = 3, 3+2 = $0005
         vmTrigger.SetMemory({ 1 }, 3);
-        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\n[No code note]"), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
+        Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty));
     }
 
     TEST_METHOD(TestTooltipDoubleIndirectAddress)