diff --git a/src/devkit/data/CapturedMemoryBlock.cpp b/src/devkit/data/CapturedMemoryBlock.cpp index 57587e07..0a8cb0c3 100644 --- a/src/devkit/data/CapturedMemoryBlock.cpp +++ b/src/devkit/data/CapturedMemoryBlock.cpp @@ -330,6 +330,55 @@ bool CapturedMemoryBlock::ContainsMatchingAddress(ra::data::ByteAddress nAddress return (pAddresses[nIndex >> 3] & nBit); } +gsl::index CapturedMemoryBlock::GetAddressIndex(ra::data::ByteAddress nAddress) const +{ + if (nAddress < m_nFirstAddress) + return -1; + + auto nIndex = nAddress - m_nFirstAddress; + if (nIndex >= m_nAddressCount) + return -1; + + if (AreAllAddressesMatching()) + return gsl::narrow_cast(nIndex); + + const uint8_t* pAddresses = GetMatchingAddressPointer(); + Expects(pAddresses != nullptr); + const auto nBit = 1 << (nIndex & 7); + if (!(pAddresses[nIndex >> 3] & nBit)) + return -1; + + gsl::index nMatchingIndex = 0; + + if (nIndex >= 8) + { + const std::array vBitsSet = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; + do + { + const auto nByte = *pAddresses++; + if (nByte != 0) + { + nMatchingIndex += vBitsSet.at(nByte & 0x0F); + nMatchingIndex += vBitsSet.at(nByte >> 4); + } + + nIndex -= 8; + } while (nIndex >= 8); + } + + uint8_t nMask = 0x01; + const uint8_t nMatch = *pAddresses; + while (nMask < nBit) + { + if (nMatch & nMask) + nMatchingIndex++; + + nMask <<= 1; + } + + return nMatchingIndex; +} + ra::data::ByteAddress CapturedMemoryBlock::GetMatchingAddress(gsl::index nIndex) const noexcept { if (AreAllAddressesMatching()) diff --git a/src/devkit/data/CapturedMemoryBlock.hh b/src/devkit/data/CapturedMemoryBlock.hh index c97d9716..d17b81ec 100644 --- a/src/devkit/data/CapturedMemoryBlock.hh +++ b/src/devkit/data/CapturedMemoryBlock.hh @@ -117,6 +117,12 @@ public: /// bool ContainsMatchingAddress(ByteAddress nAddress) const; + /// + /// Gets the index of the matching address for the specified address. + /// + /// Matching index, -1 if not found. + gsl::index GetAddressIndex(ra::data::ByteAddress nAddress) const; + /// /// Specifies which addresses are matching from a slice of an address list. /// diff --git a/src/services/Search/SearchImpl.cpp b/src/services/Search/SearchImpl.cpp index 671dd195..c5fccead 100644 --- a/src/services/Search/SearchImpl.cpp +++ b/src/services/Search/SearchImpl.cpp @@ -21,6 +21,25 @@ bool SearchImpl::ContainsAddress(const SearchResults& srResults, ra::data::ByteA return false; } +gsl::index SearchImpl::GetAddressIndex(const SearchResults& srResults, ra::data::ByteAddress nAddress) const +{ + if (nAddress & (GetStride() - 1)) + return -1; + + nAddress = ConvertFromRealAddress(nAddress); + const auto nIndex = GetIndexOfBlockForVirtualAddress(srResults, nAddress); + if (nIndex < srResults.m_vBlocks.size()) + { + gsl::index nOffset = 0; + for (size_t nScan = 0; nScan < nIndex; ++nScan) + nOffset += srResults.m_vBlocks.at(nScan).GetMatchingAddressCount(); + + return nOffset + srResults.m_vBlocks.at(nIndex).GetAddressIndex(nAddress); + } + + return -1; +} + bool SearchImpl::ExcludeResult(SearchResults& srResults, const SearchResult& pResult) const { if (pResult.nAddress & (GetStride() - 1)) diff --git a/src/services/Search/SearchImpl.hh b/src/services/Search/SearchImpl.hh index 123fd72e..7c049ef4 100644 --- a/src/services/Search/SearchImpl.hh +++ b/src/services/Search/SearchImpl.hh @@ -80,6 +80,8 @@ public: void EnumerateMatches(const SearchResults& srResults, std::function fCallback) const; + gsl::index GetAddressIndex(const SearchResults& srResults, ra::data::ByteAddress nAddress) const; + /// /// Gets a value from the search results using the provided virtual address (in result.nAddress) /// diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp index eac33f49..893c27c2 100644 --- a/src/services/SearchResults.cpp +++ b/src/services/SearchResults.cpp @@ -384,6 +384,14 @@ bool SearchResults::ExcludeResult(const SearchResult& pResult) return false; } +gsl::index SearchResults::GetAddressIndex(ra::data::ByteAddress nAddress) const noexcept +{ + if (m_pImpl != nullptr) + return m_pImpl->GetAddressIndex(*this, nAddress); + + return -1; +} + void SearchResults::EnumerateMatches(std::function fCallback) const { if (m_pImpl != nullptr) diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h index b6541a17..97f24bee 100644 --- a/src/services/SearchResults.h +++ b/src/services/SearchResults.h @@ -147,6 +147,11 @@ class SearchResults /// true if result was populated, false if the index was invalid. bool GetMatchingAddress(const SearchResult& pSrcResult, _Out_ SearchResult& result) const noexcept; + /// + /// Gets the index of the specified address. Returns -1 if the address was not captured. + /// + gsl::index GetAddressIndex(ra::data::ByteAddress nAddress) const noexcept; + /// /// Gets the raw bytes at the specified address. /// diff --git a/src/ui/viewmodels/MemoryInspectorViewModel.cpp b/src/ui/viewmodels/MemoryInspectorViewModel.cpp index a8066d1e..49e06a16 100644 --- a/src/ui/viewmodels/MemoryInspectorViewModel.cpp +++ b/src/ui/viewmodels/MemoryInspectorViewModel.cpp @@ -300,6 +300,23 @@ void MemoryInspectorViewModel::OnCurrentAddressChanged(ra::data::ByteAddress nNe UpdateNoteButtons(); + // update selected items in the search results + if (m_pSearch.HasSelection()) + { + bool isSelected = false; + for (auto& pResult : m_pSearch.Results()) + { + if (pResult.nAddress == nNewAddress) + { + isSelected = pResult.IsSelected(); + break; + } + } + + if (!isSelected) + m_pSearch.ClearSelection(); + } + // update viewer first so memory will be ready for GetValueAtAddress call m_pViewer.SetAddress(nNewAddress); diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp index d003f6ba..26e833b1 100644 --- a/src/ui/viewmodels/MemorySearchViewModel.cpp +++ b/src/ui/viewmodels/MemorySearchViewModel.cpp @@ -1129,6 +1129,16 @@ void MemorySearchViewModel::PreviousPage() } } +void MemorySearchViewModel::ClearSelection() +{ + if (m_vSearchResults.empty()) + return; + + m_vSelectedAddresses.clear(); + for (auto& pResult : m_vResults) + pResult.SetSelected(false); +} + void MemorySearchViewModel::SelectRange(gsl::index nFrom, gsl::index nTo, bool bValue) { if (m_vSearchResults.empty()) @@ -1138,6 +1148,7 @@ void MemorySearchViewModel::SelectRange(gsl::index nFrom, gsl::index nTo, bool b if (!bValue && nFrom == 0 && nTo >= gsl::narrow_cast(pCurrentResults.MatchingAddressCount()) - 1) { m_vSelectedAddresses.clear(); + SetValue(HasSelectionProperty, false); return; } @@ -1180,6 +1191,12 @@ void MemorySearchViewModel::SelectRange(gsl::index nFrom, gsl::index nTo, bool b } } + for (auto& vmResult : m_vResults) + { + const bool bIsSelected = m_vSelectedAddresses.find(vmResult.nAddress) != m_vSelectedAddresses.end(); + vmResult.SetSelected(bIsSelected); + } + m_vResults.AddNotifyTarget(*this); SetValue(HasSelectionProperty, (m_vSelectedAddresses.size() > 0)); diff --git a/src/ui/viewmodels/MemorySearchViewModel.hh b/src/ui/viewmodels/MemorySearchViewModel.hh index a1cd1a1d..deef49a9 100644 --- a/src/ui/viewmodels/MemorySearchViewModel.hh +++ b/src/ui/viewmodels/MemorySearchViewModel.hh @@ -475,6 +475,8 @@ public: /// void ExcludeSelected(); static const BoolModelProperty HasSelectionProperty; + bool HasSelection() const { return GetValue(HasSelectionProperty); } + void ClearSelection(); /// /// Bookmarks the currently selected items from the search results. diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp index 668fe80a..a76b8c4f 100644 --- a/src/ui/win32/bindings/GridBinding.cpp +++ b/src/ui/win32/bindings/GridBinding.cpp @@ -329,6 +329,19 @@ void GridBinding::OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs& } return; } + + if (m_pEnsureVisibleProperty && *m_pEnsureVisibleProperty == args.Property) + { + const auto nIndex = gsl::narrow_cast(args.tNewValue); + if (nIndex >= 0 && nIndex < gsl::narrow_cast(GetValue(*m_pScrollMaximumProperty))) + { + InvokeOnUIThread([this, nIndex]() noexcept { + ListView_EnsureVisible(m_hWnd, gsl::narrow_cast(nIndex), FALSE); + }); + } + + SetValue(*m_pEnsureVisibleProperty, -1); + } } else if (m_pEnsureVisibleProperty && *m_pEnsureVisibleProperty == args.Property) { diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp index 6b487b49..f38a85b5 100644 --- a/tests/services/SearchResults_Tests.cpp +++ b/tests/services/SearchResults_Tests.cpp @@ -47,16 +47,22 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(1U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0x12U, result.nValue); + Assert::AreEqual({ 0 }, results.GetAddressIndex(1U)); Assert::IsTrue(results.GetMatchingAddress(1U, result)); Assert::AreEqual(2U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0x34U, result.nValue); + Assert::AreEqual({ 1 }, results.GetAddressIndex(2U)); Assert::IsTrue(results.GetMatchingAddress(2U, result)); Assert::AreEqual(3U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0xABU, result.nValue); + Assert::AreEqual({ 2 }, results.GetAddressIndex(3U)); + + Assert::AreEqual({ -1 }, results.GetAddressIndex(0U)); + Assert::AreEqual({ -1 }, results.GetAddressIndex(4U)); } TEST_METHOD(TestInitializeFromMemorySixteenBit) @@ -131,11 +137,15 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(0U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::SixteenBit, result.nSize); Assert::AreEqual(0x1200U, result.nValue); + Assert::AreEqual({ 0 }, results.GetAddressIndex(0U)); Assert::IsTrue(results.GetMatchingAddress(1U, result)); Assert::AreEqual(2U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::SixteenBit, result.nSize); Assert::AreEqual(0xAB34U, result.nValue); + Assert::AreEqual({ 1 }, results.GetAddressIndex(2U)); + + Assert::AreEqual({ -1 }, results.GetAddressIndex(1U)); } TEST_METHOD(TestInitializeFromMemoryThirtyTwoBitAligned) @@ -163,11 +173,15 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(0U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::ThirtyTwoBit, result.nSize); Assert::AreEqual(0xAB341200U, result.nValue); + Assert::AreEqual({ 0 }, results.GetAddressIndex(0U)); Assert::IsTrue(results.GetMatchingAddress(1U, result)); Assert::AreEqual(4U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::ThirtyTwoBit, result.nSize); Assert::AreEqual(0x2044CD56U, result.nValue); + Assert::AreEqual({ 1 }, results.GetAddressIndex(4U)); + + Assert::AreEqual({ -1 }, results.GetAddressIndex(2U)); } TEST_METHOD(TestInitializeFromMemorySixteenBitBigEndian) @@ -390,6 +404,8 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(3U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0xABU, result.nValue); + + Assert::AreEqual({ 0 }, results2.GetAddressIndex(3U)); } TEST_METHOD(TestInitializeFromResultsEightBitEqualsInvalidConstant) @@ -443,16 +459,19 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(0U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0x00U, result.nValue); + Assert::AreEqual({ 0 }, results3.GetAddressIndex(0U)); Assert::IsTrue(results3.GetMatchingAddress(1U, result)); Assert::AreEqual(2U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0x00U, result.nValue); + Assert::AreEqual({ 1 }, results3.GetAddressIndex(2U)); Assert::IsTrue(results3.GetMatchingAddress(2U, result)); Assert::AreEqual(5U, result.nAddress); Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); Assert::AreEqual(0x00U, result.nValue); + Assert::AreEqual({ 2 }, results3.GetAddressIndex(5U)); } TEST_METHOD(TestInitializeFromResultsEightBitNotEqualsConstant) diff --git a/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp b/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp index a73c1a54..3bd4587d 100644 --- a/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp +++ b/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp @@ -260,6 +260,41 @@ TEST_CLASS(MemoryInspectorViewModel_Tests) Assert::IsFalse(inspector.CanRevertCurrentAddressNote()); } + TEST_METHOD(TestSetCurrentAddressKeepsSelectedSearchResult) + { + MemoryInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.Search().BeginNewSearch(); + inspector.Search().ApplyFilter(); + Assert::AreEqual({ 9U }, inspector.Search().Results().Count()); + inspector.Search().Results().GetItemAt(3)->SetSelected(true); + + inspector.SetCurrentAddress({ 3U }); + + Assert::IsTrue(inspector.Search().Results().GetItemAt(3)->IsSelected()); + Assert::IsTrue(inspector.Search().HasSelection()); + } + + TEST_METHOD(TestSetCurrentAddressClearsSelectedSearchResultForAlternateAddress) + { + MemoryInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.Search().BeginNewSearch(); + inspector.Search().ApplyFilter(); + Assert::AreEqual({ 9U }, inspector.Search().Results().Count()); + inspector.Search().Results().GetItemAt(3)->SetSelected(true); + + inspector.SetCurrentAddress({ 4U }); + + // Item 3 is no longer selected + Assert::IsFalse(inspector.Search().Results().GetItemAt(3)->IsSelected()); + + // Item 4 does not get selected + Assert::IsFalse(inspector.Search().Results().GetItemAt(4)->IsSelected()); + + Assert::IsFalse(inspector.Search().HasSelection()); + } + TEST_METHOD(TestSetViewerAddress) { MemoryInspectorViewModelHarness inspector; diff --git a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp index 706d486a..64f493c4 100644 --- a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp +++ b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp @@ -128,7 +128,6 @@ TEST_CLASS(MemorySearchViewModel_Tests) bool CanEditFilterValue() const { return GetValue(CanEditFilterValueProperty); } bool CanGoToPreviousPage() const { return GetValue(CanGoToPreviousPageProperty); } bool CanGoToNextPage() const { return GetValue(CanGoToNextPageProperty); } - bool HasSelection() const { return GetValue(HasSelectionProperty); } void SetScrollOffset(int nValue) { SetValue(ScrollOffsetProperty, nValue); }