From 09e960a448de11387eff328e9d85593bc8f69705 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 16 Aug 2026 05:47:22 -0600 Subject: [PATCH 1/9] provide custom filter method for SearchResults --- src/services/Search/SearchImpl.cpp | 33 +++++++++++++++++++++++++ src/services/Search/SearchImpl.hh | 4 +++ src/services/SearchResults.cpp | 16 ++++++++++++ src/services/SearchResults.h | 9 +++++++ tests/services/SearchResults_Tests.cpp | 34 +++++++++++++++++++++++++- 5 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/services/Search/SearchImpl.cpp b/src/services/Search/SearchImpl.cpp index 34ae0b91..b7a47525 100644 --- a/src/services/Search/SearchImpl.cpp +++ b/src/services/Search/SearchImpl.cpp @@ -147,6 +147,39 @@ void SearchImpl::ApplyFilter(SearchResults& srNew, const SearchResults& srPrevio } } +void SearchImpl::ApplyFilter(SearchResults & srNew, const SearchResults & srPrevious, + std::function fFilter) const +{ + std::vector vMatches; + SearchResult pResult; + pResult.nSize = GetMemSize(); + + for (auto& block : srPrevious.m_vBlocks) + { + const auto* pBlockBytes = block.GetBytes(); + const auto* pBlockBytesStop = pBlockBytes + block.GetBytesSize() - GetPadding(); + const auto nStride = GetStride(); + const auto nBlockAddress = block.GetFirstAddress(); + pResult.nAddress = ConvertToRealAddress(nBlockAddress); + + for (const auto* pScan = pBlockBytes; pScan < pBlockBytesStop; pScan += nStride) + { + pResult.nValue = BuildValue(pScan); + if (fFilter(pResult)) + vMatches.push_back(ConvertFromRealAddress(pResult.nAddress)); + + pResult.nAddress += nStride; + } + + if (!vMatches.empty()) + { + std::vector vMemory(pBlockBytes, pBlockBytes + block.GetBytesSize()); + AddBlocks(srNew, vMatches, vMemory, block.GetFirstAddress(), GetPadding()); + vMatches.clear(); + } + } +} + bool SearchImpl::GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResult& result) const noexcept { result.nSize = GetMemSize(); diff --git a/src/services/Search/SearchImpl.hh b/src/services/Search/SearchImpl.hh index cc25a226..99c17e4a 100644 --- a/src/services/Search/SearchImpl.hh +++ b/src/services/Search/SearchImpl.hh @@ -68,6 +68,10 @@ public: virtual void ApplyFilter(SearchResults& srNew, const SearchResults& srPrevious, std::function pReadMemory) const; + // populates a vector of addresses that match the specified filter when applied to a previous search result + void ApplyFilter(SearchResults& srNew, const SearchResults& srPrevious, + std::function fFilter) const; + // gets the nIndex'th search result bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResult& result) const noexcept; diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp index e64fd7aa..aafc907d 100644 --- a/src/services/SearchResults.cpp +++ b/src/services/SearchResults.cpp @@ -323,6 +323,22 @@ bool SearchResults::Initialize(const SearchResults& srFirst, std::function fFilter) +{ + m_nType = srFirst.m_nType; + m_pImpl = srFirst.m_pImpl; + m_nCompareType = ComparisonType::Equals; + m_nFilterType = SearchFilterType::LastKnownValue; + m_sFilterValue = L"[Custom]"; + + m_pImpl->ApplyFilter(*this, srFirst, fFilter); + + RA_LOG_INFO("Allocated %zu bytes for filtered search", CalcSize(m_vBlocks)); + + return true; +} + _Use_decl_annotations_ bool SearchResults::Initialize(const SearchResults& srSource, ComparisonType nCompareType, SearchFilterType nFilterType, const std::wstring& sFilterValue) diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h index ebae438f..456bbc3b 100644 --- a/src/services/SearchResults.h +++ b/src/services/SearchResults.h @@ -105,6 +105,15 @@ class SearchResults bool Initialize(_In_ const SearchResults& srFirst, _In_ std::function pReadMemory, _In_ ComparisonType nCompareType, _In_ SearchFilterType nFilterType, _In_ const std::wstring& sFilterValue); + /// + /// Initializes a result set by comparing provided memory against another result set. + /// + /// The result set to filter. + /// A function that provides current values of memory. + /// A function that determines whether or not to exclude an address (returns true to keep + /// true if initialization was successful, false if the filter value was not supported + bool Initialize(_In_ const SearchResults& srFirst, _In_ std::function fFilter); + /// /// Gets the number of matching addresses. /// diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp index 18619ad2..6b487b49 100644 --- a/tests/services/SearchResults_Tests.cpp +++ b/tests/services/SearchResults_Tests.cpp @@ -915,6 +915,39 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(0x37U, result.nValue); } + TEST_METHOD(TestInitializeFromResultsEightBitCustomFilter) + { + std::array memory{ 0x00, 0x11, 0x34, 0xAB, 0x56 }; + ra::context::mocks::MockEmulatorMemoryContext mockMemoryContext; + mockMemoryContext.MockMemory(memory); + + SearchResults results1; + results1.Initialize(0U, 5U, ra::services::SearchType::EightBit); + Assert::AreEqual({ 5U }, results1.MatchingAddressCount()); + + // find the odd values + SearchResults results2; + results2.Initialize(results1, [](const SearchResult& pResult) { return pResult.nValue % 2 != 0; }); + + Assert::AreEqual({ 2U }, results2.MatchingAddressCount()); + Assert::IsFalse(results2.ContainsAddress(0U)); + Assert::IsTrue(results2.ContainsAddress(1U)); + Assert::IsFalse(results2.ContainsAddress(2U)); + Assert::IsTrue(results2.ContainsAddress(3U)); + Assert::IsFalse(results2.ContainsAddress(4U)); + + SearchResult result; + Assert::IsTrue(results2.GetMatchingAddress(0U, result)); + Assert::AreEqual(1U, result.nAddress); + Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); + Assert::AreEqual(0x11U, result.nValue); + + Assert::IsTrue(results2.GetMatchingAddress(1U, result)); + Assert::AreEqual(3U, result.nAddress); + Assert::AreEqual(ra::data::Memory::Size::EightBit, result.nSize); + Assert::AreEqual(0xABU, result.nValue); + } + TEST_METHOD(TestInitializeFromResultsSixteenBitNotEqualPrevious) { std::array memory{0x00, 0x12, 0x34, 0xAB, 0x56}; @@ -2939,7 +2972,6 @@ TEST_CLASS(SearchResults_Tests) Assert::AreEqual(ra::data::Memory::Size::ThirtyTwoBitBigEndian, result.nSize); Assert::AreEqual(0xdeadbeefU, result.nValue); } - }; } // namespace tests From d8d671e08a0576cc1030c841991d818e3715d4a2 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 16 Aug 2026 06:09:44 -0600 Subject: [PATCH 2/9] restrict pointer search space to addresses containing valid non-null pointers --- src/services/SearchResults.h | 2 +- src/ui/viewmodels/PointerFinderViewModel.cpp | 295 ++++++++++++------ src/ui/viewmodels/PointerFinderViewModel.hh | 22 ++ src/ui/win32/PointerFinderDialog.cpp | 2 +- .../data/models/MemoryNoteModel_Tests.cpp | 17 + .../PointerFinderViewModel_Tests.cpp | 35 ++- 6 files changed, 265 insertions(+), 108 deletions(-) diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h index 456bbc3b..44c46283 100644 --- a/src/services/SearchResults.h +++ b/src/services/SearchResults.h @@ -134,7 +134,7 @@ class SearchResults bool GetMatchingAddress(gsl::index nIndex, _Out_ SearchResult& result) const noexcept; /// - /// Gets and item from the results matching a result from another set of results. + /// Gets an item from the results matching a result from another set of results. /// /// The result to find a match for. /// The result. diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 6c6d4efa..2b3058d2 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -3,6 +3,8 @@ #include "RA_Defs.h" #include "util\Strings.hh" +#include "context/IConsoleContext.hh" + #include "data/context/EmulatorContext.hh" #include "data/context/GameContext.hh" @@ -32,6 +34,8 @@ const StringModelProperty PointerFinderViewModel::PotentialPointerViewModel::Poi const StringModelProperty PointerFinderViewModel::PotentialPointerViewModel::PointerValue4Property("PotentialPointerViewModel", "PointerValue4", L""); const BoolModelProperty PointerFinderViewModel::PotentialPointerViewModel::IsSelectedProperty("PotentialPointerViewModel", "IsSelected", false); +constexpr uint32_t MAX_OFFSET = 1024; + void PointerFinderViewModel::StateViewModel::DoFrame() { if (CanCapture()) @@ -91,11 +95,22 @@ void PointerFinderViewModel::StateViewModel::Capture() } DispatchMemoryRead([this]() { + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); const auto& pMemoryContext = ra::services::ServiceLocator::Get(); const auto nMemorySize = gsl::narrow(pMemoryContext.TotalMemorySize()); + ra::services::SearchResults pInitialResults; + pInitialResults.Initialize(0, nMemorySize, m_pOwner->GetSearchType()); + m_pCapture.reset(new ra::services::SearchResults()); - m_pCapture->Initialize(0, nMemorySize, m_pOwner->GetSearchType()); + + m_pCapture->Initialize(pInitialResults, + [&pConsoleContext](const ra::services::SearchResult& pSearchResult) { + if (pSearchResult.nValue == 0) + return false; + + return (pConsoleContext.ByteAddressFromRealAddress(pSearchResult.nValue) != 0xFFFFFFFF); + }); }); SetValue(CaptureButtonTextProperty, L"Release"); @@ -152,114 +167,52 @@ void PointerFinderViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& } } -static std::wstring FormatValue(const ra::services::SearchResults& srResults, ra::data::ByteAddress nAddress) -{ - auto sValue = srResults.GetFormattedValue(nAddress, srResults.GetSize()); - if (sValue.at(1) == 'x' && sValue.at(0) == '0') - sValue.erase(0, 2); - - return sValue; -} - void PointerFinderViewModel::Find() { + std::vector vPotentialPointers; + ra::data::Memory::Size nSize = ra::data::Memory::Size::Unknown; bool bPerformedSearch = false; - // TODO: capture/restore selected address + // TODO: capture/restore selected address (find may be clicked again after changing captures) m_vResults.BeginUpdate(); m_vResults.Clear(); - const auto& pMemoryContext = ra::services::ServiceLocator::Get(); for (size_t i = 0; i < m_vStates.size(); i++) { const auto& pStateI = m_vStates.at(i); if (pStateI.CanCapture()) continue; - for (size_t j = i + 1; j < m_vStates.size(); j++) + if (vPotentialPointers.empty()) { - const auto& pStateJ = m_vStates.at(j); - if (pStateJ.CanCapture()) - continue; - - const auto nAddressI = pStateI.Viewer().GetAddress(); - const auto nAddressJ = pStateJ.Viewer().GetAddress(); - if (nAddressI == nAddressJ) - continue; - - // flag all items as having not been seen - anything that's still not seen after merging will be discarded - for (auto& pResult : m_vResults) - pResult.m_bMatched = false; - - // compare the two memory states - ra::services::SearchResults pResults; - auto pReadMemory = [srSecond = pStateJ.CapturedMemory()](ra::data::ByteAddress nAddress, uint8_t* pBuffer, size_t nBufferSize) noexcept { - srSecond->GetBytes(nAddress, pBuffer, nBufferSize); - }; - if (nAddressI > nAddressJ) - pResults.Initialize(*pStateI.CapturedMemory(), pReadMemory, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValueMinus, std::to_wstring(nAddressI - nAddressJ)); - else - pResults.Initialize(*pStateI.CapturedMemory(), pReadMemory, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValuePlus, std::to_wstring(nAddressJ - nAddressI)); - - // merge the new potential items into the list - gsl::index nResultIndex = 0; - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults.MatchingAddressCount()); nIndex++) - { - ra::services::SearchResult pResult; - if (pResults.GetMatchingAddress(nIndex, pResult)) - { - PotentialPointerViewModel* pPointer = nullptr; - while (nResultIndex < gsl::narrow_cast(m_vResults.Count())) - { - PotentialPointerViewModel* pItem = m_vResults.GetItemAt(nResultIndex); - if (pItem->m_nAddress > pResult.nAddress) - break; - - if (pItem->m_nAddress == pResult.nAddress) - { - pPointer = pItem; - nResultIndex++; - break; - } - - nResultIndex++; - } - - if (pPointer == nullptr) - { - if (bPerformedSearch) // new address not in set, ignore - continue; - - pPointer = &m_vResults.Add(); - pPointer->m_nAddress = pResult.nAddress; - pPointer->SetPointerAddress(pMemoryContext.FormatAddress(pResult.nAddress)); - const auto nOffset = (nAddressJ - pResult.nValue); - pPointer->SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); - - pPointer->SetPointerValue(i, FormatValue(*pStateI.CapturedMemory(), pResult.nAddress)); - } - else if (pPointer->GetPointerValue(i).empty()) - { - pPointer->SetPointerValue(i, FormatValue(*pStateI.CapturedMemory(), pResult.nAddress)); - } - - pPointer->SetPointerValue(j, FormatValue(*pStateJ.CapturedMemory(), pResult.nAddress)); - pPointer->m_bMatched = true; - } - } + FindBestChains(vPotentialPointers, pStateI, i); + nSize = pStateI.CapturedMemory()->GetSize(); + } + else + { + FindMatches(vPotentialPointers, pStateI, i); + bPerformedSearch = true; + } - // remove any items that didn't also match the most recent search - if (bPerformedSearch) - { - for (gsl::index nIndex = m_vResults.Count() - 1; nIndex >= 0; nIndex--) - { - if (!m_vResults.GetItemAt(nIndex)->m_bMatched) - m_vResults.RemoveAt(nIndex); - } - } + if (vPotentialPointers.empty()) + break; + } - bPerformedSearch = true; + const auto& pMemoryContext = ra::services::ServiceLocator::Get(); + for (const auto& pPotentialPointer : vPotentialPointers) + { + auto& pPointer = m_vResults.Add(); + const auto& pNode = pPotentialPointer.vNodes.at(0); + pPointer.m_nAddress = pNode.nAddress; + pPointer.SetPointerAddress(pMemoryContext.FormatAddress(pPointer.m_nAddress)); + pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", pNode.nOffset)); + + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vStates.size()); ++nIndex) + { + const auto nValue = pNode.nValue.at(nIndex); + if (nValue) + pPointer.SetPointerValue(nIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); } } @@ -276,11 +229,167 @@ void PointerFinderViewModel::Find() m_vResults.EndUpdate(); - if (!bPerformedSearch) ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Cannot find.", L"At least two unique addresses must be captured before potential pointers can be located."); } +void PointerFinderViewModel::FindBestChains(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex) +{ + // extract all pointers from the search results + const auto* pResults = pState.CapturedMemory(); + Expects(pResults != nullptr); + + std::vector vPointerAddresses; + GetPointerAddresses(vPointerAddresses, *pResults); + + // limit the results to addresses within MAX_OFFSET of the pointer value + const auto nSearchAddress = pState.Viewer().GetAddress(); + const auto pRange = NarrowSearch(vPointerAddresses, nSearchAddress); + + // get things pointing to the filtered addresses + FindPointers(vPotentialPointers, *pResults, nStateIndex, vPointerAddresses, pRange, nSearchAddress); +} + +void PointerFinderViewModel::GetPointerAddresses(std::vector& vPointerAddresses, const ra::services::SearchResults& pResults) +{ + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults.MatchingAddressCount()); nIndex++) + { + ra::services::SearchResult pResult; + if (!pResults.GetMatchingAddress(nIndex, pResult)) + continue; + + // ignore null values + if (pResult.nValue == 0) + continue; + + // ignore non-pointer values + const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); + if (nPointerAddress == 0xFFFFFFFF) + continue; + + // add to the list if not already there + const auto pInsertIter = std::lower_bound(vPointerAddresses.begin(), vPointerAddresses.end(), nPointerAddress); + if (pInsertIter == vPointerAddresses.end() || *pInsertIter != nPointerAddress) + vPointerAddresses.insert(pInsertIter, nPointerAddress); + } +} + +PointerFinderViewModel::PointerAddressRange PointerFinderViewModel::NarrowSearch( + const std::vector& vPointerAddresses, ra::data::ByteAddress nSearchAddress) +{ + if (vPointerAddresses.size() == 0) + return PointerAddressRange(vPointerAddresses.data(), vPointerAddresses.data()); + + const auto pSearchIter = std::lower_bound(vPointerAddresses.begin(), vPointerAddresses.end(), nSearchAddress); + auto pEnd = pSearchIter; + auto pStart = pSearchIter; + + uint32_t nMaxOffset = MAX_OFFSET; + for (int i = 0; i < 4; ++i, nMaxOffset *= 2) + { + while (pEnd < vPointerAddresses.end() && *pEnd - nSearchAddress <= nMaxOffset) + ++pEnd; + + while (pStart > vPointerAddresses.begin() && nSearchAddress - *(pStart - 1) <= nMaxOffset) + --pStart; + + const auto nMatches = gsl::narrow_cast(pEnd - pStart); + if (nMatches >= 8 || nMatches == vPointerAddresses.size()) + break; + } + + return PointerAddressRange(vPointerAddresses.data() + (pStart - vPointerAddresses.begin()), + vPointerAddresses.data() + (pEnd - vPointerAddresses.begin())); +} + +void PointerFinderViewModel::FindPointers(std::vector& vPotentialPointers, const ra::services::SearchResults& pResults, size_t nStateIndex, + const std::vector& vPointerAddresses, PointerFinderViewModel::PointerAddressRange pRange, ra::data::ByteAddress nSearchAddress) +{ + if (pRange.first == pRange.second) + return; + + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults.MatchingAddressCount()); nIndex++) + { + ra::services::SearchResult pResult; + if (!pResults.GetMatchingAddress(nIndex, pResult)) + continue; + + // ignore null values + if (pResult.nValue == 0) + continue; + + // ignore non-pointer values + const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); + if (nPointerAddress == 0xFFFFFFFF) + continue; + + // ignore pointers not in the search range + if (!std::binary_search(pRange.first, pRange.second, nPointerAddress)) + continue; + + auto& pPointerChain = vPotentialPointers.emplace_back(); + auto& pPointer = pPointerChain.vNodes.emplace_back(); + memset(&pPointer, 0, sizeof(pPointer)); + pPointer.nAddress = pResult.nAddress; + pPointer.nOffset = ra::to_signed(nSearchAddress) - nPointerAddress; + pPointer.nValue.at(nStateIndex) = pResult.nValue; + } +} + +void PointerFinderViewModel::FindMatches(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex) +{ + if (vPotentialPointers.empty()) + return; + + auto pPotentialPointer = vPotentialPointers.begin(); + auto nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + + const auto* pResults = pState.CapturedMemory(); + Expects(pResults != nullptr); + const auto nSearchAddress = pState.Viewer().GetAddress(); + + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults->MatchingAddressCount()); nIndex++) + { + ra::services::SearchResult pResult; + if (!pResults->GetMatchingAddress(nIndex, pResult)) + continue; + + if (pResult.nAddress < nNextAddress) + continue; + + bool bIsMatch = false; + if (pResult.nAddress == nNextAddress && pResult.nValue != 0) + { + const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); + if (nPointerAddress != 0xFFFFFFFF) + { + auto& pNode = *pPotentialPointer->vNodes.begin(); + if (nPointerAddress + pNode.nOffset == nSearchAddress) + { + pNode.nValue.at(nStateIndex) = pResult.nValue; + bIsMatch = true; + } + } + } + + pPotentialPointer->bPrune = !bIsMatch; + ++pPotentialPointer; + + if (pPotentialPointer == vPotentialPointers.end()) + break; + + nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; + } + + vPotentialPointers.erase(std::remove_if( + vPotentialPointers.begin(), vPotentialPointers.end(), + [](const PotentialPointerChain& pChain) { return pChain.bPrune; } + ), vPotentialPointers.end()); +} + void PointerFinderViewModel::BookmarkSelected() { auto& vmBookmarks = ra::services::ServiceLocator::GetMutable().MemoryBookmarks; diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 0a2158e6..97d66176 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.hh +++ b/src/ui/viewmodels/PointerFinderViewModel.hh @@ -267,6 +267,7 @@ public: private: friend class PointerFinderViewModel; ra::data::ByteAddress m_nAddress = 0; + int32_t m_nOffset = 0; bool m_bMatched = false; }; @@ -286,6 +287,27 @@ protected: private: std::array m_vStates; + struct PotentialPointerNode + { + ra::data::ByteAddress nAddress = 0; + int32_t nOffset = 0; + std::array nValue {}; + }; + struct PotentialPointerChain + { + std::vector vNodes; + int nScore = 0; + bool bPrune = false; + }; + typedef std::pair PointerAddressRange; + void FindBestChains(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex); + void FindPointers(std::vector& vPotentialPointers, const ra::services::SearchResults& pResults, size_t nStateIndex, + const std::vector& vPointerAddresses, PointerAddressRange pRange, ra::data::ByteAddress nSearchAddress); + void FindMatches(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex); + + static void GetPointerAddresses(std::vector& vPointerAddresses, const ra::services::SearchResults& pResults); + static PointerAddressRange NarrowSearch(const std::vector& vPointerAddresses, ra::data::ByteAddress nSearchAddress); + ra::ui::ViewModelCollection m_vResults; LookupItemViewModelCollection m_vSearchTypes; }; diff --git a/src/ui/win32/PointerFinderDialog.cpp b/src/ui/win32/PointerFinderDialog.cpp index 2224b99e..3e788dd2 100644 --- a/src/ui/win32/PointerFinderDialog.cpp +++ b/src/ui/win32/PointerFinderDialog.cpp @@ -117,7 +117,7 @@ PointerFinderDialog::PointerFinderDialog(PointerFinderViewModel& vmPointerFinder auto pOffsetColumn = std::make_unique( PointerFinderViewModel::PotentialPointerViewModel::OffsetProperty); pOffsetColumn->SetHeader(L"Offset"); - pOffsetColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 80); + pOffsetColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 90); m_bindResults.BindColumn(1, std::move(pOffsetColumn)); auto pValue1Column = std::make_unique( diff --git a/tests/devkit/data/models/MemoryNoteModel_Tests.cpp b/tests/devkit/data/models/MemoryNoteModel_Tests.cpp index b6a81f4c..a3cc92ed 100644 --- a/tests/devkit/data/models/MemoryNoteModel_Tests.cpp +++ b/tests/devkit/data/models/MemoryNoteModel_Tests.cpp @@ -390,6 +390,23 @@ TEST_CLASS(MemoryNoteModel_Tests) Memory::Size::Array, 24); } + TEST_METHOD(TestGetPointerNoteAtNegativeOffset) + { + MemoryNoteModelHarness note; + const std::wstring sNote = + L"Bomb Timer Pointer (24-bit)\r\n" + L"+0xFFFFFFFE - Bombs Defused\r\n" // -2 + L"+0xFFFFFFFC - Bomb Timer"; // -4 + note.SetNote(sNote); + + Assert::AreEqual(Memory::Size::TwentyFourBit, note.GetMemSize()); + Assert::AreEqual(sNote, note.GetNote()); // full note for pointer address + + // extracted notes for offset fields + AssertIndirectNote(note, 0xFFFFFFFEU, L"Bombs Defused", Memory::Size::Unknown, 1); + AssertIndirectNote(note, 0xFFFFFFFCU, L"Bomb Timer", Memory::Size::Unknown, 1); + } + TEST_METHOD(TestHeaderedPointer) { MemoryNoteModelHarness note; diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index 261e8b3a..653d96df 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -7,6 +7,7 @@ #include "tests\RA_UnitTestHelpers.h" +#include "tests\devkit\context\mocks\MockConsoleContext.hh" #include "tests\devkit\context\mocks\MockEmulatorMemoryContext.hh" #include "tests\devkit\services\mocks\MockFileSystem.hh" #include "tests\mocks\MockConfiguration.hh" @@ -27,6 +28,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) class PointerFinderViewModelHarness : public PointerFinderViewModel { public: + ra::context::mocks::MockConsoleContext mockConsoleContext; ra::context::mocks::MockEmulatorMemoryContext mockEmulatorMemoryContext; ra::data::context::mocks::MockGameContext mockGameContext; ra::services::mocks::MockConfiguration mockConfiguration; @@ -36,7 +38,6 @@ TEST_CLASS(PointerFinderViewModel_Tests) GSL_SUPPRESS_F6 PointerFinderViewModelHarness() : PointerFinderViewModel() { - } ~PointerFinderViewModelHarness() @@ -48,6 +49,14 @@ TEST_CLASS(PointerFinderViewModel_Tests) PointerFinderViewModelHarness(PointerFinderViewModelHarness&&) noexcept = delete; PointerFinderViewModelHarness& operator=(PointerFinderViewModelHarness&&) noexcept = delete; + template + void MockMemory(std::array& pMemory) + { + mockEmulatorMemoryContext.MockMemory(pMemory); + mockConsoleContext.AddMemoryRegion(0, gsl::narrow_cast(pMemory.size()), + ra::data::MemoryRegion::Type::SystemRAM); + } + void AssertRow(gsl::index nIndex, const std::wstring& sPointerAddress, const std::wstring& sOffset, const std::wstring& sPointerValue1, const std::wstring& sPointerValue2, const std::wstring& sPointerValue3, const std::wstring& sPointerValue4) @@ -102,7 +111,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) PointerFinderViewModelHarness vmPointerFinder; vmPointerFinder.mockGameContext.SetGameId(1U); std::array pMemory{}; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); bool bDialogSeen = false; vmPointerFinder.mockDesktop.ExpectWindow([&bDialogSeen](MessageBoxViewModel& vmMessageBox) { @@ -215,7 +224,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) PointerFinderViewModelHarness vmPointerFinder; vmPointerFinder.mockGameContext.SetGameId(1U); std::array pMemory{}; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); bool bDialogSeen = false; vmPointerFinder.mockDesktop.ExpectWindow([&bDialogSeen](MessageBoxViewModel& vmMessageBox) { @@ -240,7 +249,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) PointerFinderViewModelHarness vmPointerFinder; vmPointerFinder.mockGameContext.SetGameId(1U); std::array pMemory{}; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); bool bDialogSeen = false; vmPointerFinder.mockDesktop.ExpectWindow([&bDialogSeen](MessageBoxViewModel& vmMessageBox) { @@ -272,7 +281,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x1c) = 0x55; pMemory.at(0x1d) = 0x46; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x1c"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -301,7 +310,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x1c) = 0x55; pMemory.at(0x1d) = 0x46; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -330,7 +339,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x1c) = 0x55; pMemory.at(0x1d) = 0x46; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x18"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -361,7 +370,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x1d) = 0x46; pMemory.at(0x70) = 0x1c; pMemory.at(0x9c) = 0x20; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -395,7 +404,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x70) = 0x1c; pMemory.at(0x9c) = 0x20; pMemory.at(0xa4) = 0x20; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -432,7 +441,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) std::array pMemory{}; pMemory.at(0x08) = 0x1c; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x18"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -468,7 +477,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x70) = 0x1c; pMemory.at(0x9c) = 0x20; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -509,7 +518,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x70) = 0x1c; pMemory.at(0x9c) = 0x20; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); @@ -562,7 +571,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) pMemory.at(0x08) = 0x1c; pMemory.at(0x70) = 0x1c; pMemory.at(0x9c) = 0x20; - vmPointerFinder.mockEmulatorMemoryContext.MockMemory(pMemory); + vmPointerFinder.MockMemory(pMemory); vmPointerFinder.States().at(0).SetAddress(L"0x20"); vmPointerFinder.States().at(0).ToggleCapture(); From b2c502466daf2c510320ad3b5a221e477ae30974 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 16 Aug 2026 13:35:32 -0600 Subject: [PATCH 3/9] enumerate matched results --- src/devkit/data/CapturedMemoryBlock.cpp | 83 ++++++++++++-------- src/devkit/data/CapturedMemoryBlock.hh | 8 ++ src/services/Search/SearchImpl.cpp | 23 ++++++ src/services/Search/SearchImpl.hh | 4 + src/services/SearchResults.cpp | 6 ++ src/services/SearchResults.h | 6 ++ src/ui/viewmodels/PointerFinderViewModel.cpp | 67 ++++++++-------- 7 files changed, 130 insertions(+), 67 deletions(-) diff --git a/src/devkit/data/CapturedMemoryBlock.cpp b/src/devkit/data/CapturedMemoryBlock.cpp index 2f00b39d..57587e07 100644 --- a/src/devkit/data/CapturedMemoryBlock.cpp +++ b/src/devkit/data/CapturedMemoryBlock.cpp @@ -335,46 +335,67 @@ ra::data::ByteAddress CapturedMemoryBlock::GetMatchingAddress(gsl::index nIndex) if (AreAllAddressesMatching()) return m_nFirstAddress + gsl::narrow_cast(nIndex); - const auto nAddressesSize = (m_nAddressCount + 7) / 8; - const uint8_t* pAddresses = (nAddressesSize > sizeof(m_vAddresses)) ? m_pAddresses : &m_vAddresses[0]; + ra::data::ByteAddress nAddress = 0; + EnumerateMatchingAddressesInternal([&nAddress, &nIndex](ra::data::ByteAddress nScanAddress) noexcept { + if (nIndex == 0) + { + nAddress = nScanAddress; + return false; + } + + --nIndex; + return true; + }); + + return nAddress; +} + +bool CapturedMemoryBlock::EnumerateMatchingAddresses(std::function fCallback) const +{ + if (!AreAllAddressesMatching()) + return EnumerateMatchingAddressesInternal(fCallback); + + for (auto nAddress = m_nFirstAddress; nAddress < m_nFirstAddress + m_nAddressCount; ++nAddress) + { + if (!fCallback(nAddress)) + return false; + } + + return true; +} + +bool CapturedMemoryBlock::EnumerateMatchingAddressesInternal(std::function fCallback) const +{ + const uint8_t* pAddresses = GetMatchingAddressPointer(); + if (GSL_UNLIKELY(pAddresses == nullptr)) + return false; + ra::data::ByteAddress nAddress = m_nFirstAddress; - const ra::data::ByteAddress nStop = m_nFirstAddress + m_nAddressCount; - uint8_t nMask = 0x01; + const ra::data::ByteAddress nStop = nAddress + m_nAddressCount; - if (pAddresses != nullptr) + do { - do + uint8_t nValue = *pAddresses++; + if (nValue) { - if (*pAddresses & nMask) + ra::data::ByteAddress nScanAddress = nAddress; + do { - if (nIndex-- == 0) - return nAddress; - } - - if (nMask == 0x80) - { - nMask = 0x01; - pAddresses++; - - while (!*pAddresses) + if (nValue & 1) { - nAddress += 8; - if (nAddress >= nStop) - break; - - pAddresses++; + if (!fCallback(nScanAddress)) + return false; } - } - else - { - nMask <<= 1; - } - nAddress++; - } while (nAddress < nStop); - } + ++nScanAddress; + nValue >>= 1; + } while (nValue); + } + + nAddress += 8; + } while (nAddress < nStop); - return 0; + return true; } } // namespace data diff --git a/src/devkit/data/CapturedMemoryBlock.hh b/src/devkit/data/CapturedMemoryBlock.hh index 31c86634..c97d9716 100644 --- a/src/devkit/data/CapturedMemoryBlock.hh +++ b/src/devkit/data/CapturedMemoryBlock.hh @@ -166,6 +166,12 @@ public: return (pMatchingAddresses[nIndex >> 3] & nBit); } + /// + /// Calls the provided callback for each matching address. + /// + /// Callback to call for each element, returns true to continue iterating, or false to stop. + bool EnumerateMatchingAddresses(std::function fCallback) const; + /// /// Attempts to minimize memory allocations by detecting and sharing large blocks of repeated data. /// @@ -175,6 +181,8 @@ public: private: bool IsBytesAllocated() const noexcept { return GetBytesSize() > sizeof(m_vBytes); } + bool EnumerateMatchingAddressesInternal(std::function fCallback) const; + void ShareMemory(const std::vector& vBlocks, uint32_t nHash) noexcept; //void SetRepeat(uint32_t nCount, uint32_t nValue) noexcept; diff --git a/src/services/Search/SearchImpl.cpp b/src/services/Search/SearchImpl.cpp index b7a47525..671dd195 100644 --- a/src/services/Search/SearchImpl.cpp +++ b/src/services/Search/SearchImpl.cpp @@ -198,6 +198,29 @@ bool SearchImpl::GetMatchingAddress(const SearchResults& srResults, gsl::index n return false; } +void SearchImpl::EnumerateMatches(const SearchResults& srResults, + std::function fCallback) const +{ + SearchResult result; + result.nSize = GetMemSize(); + + for (const auto& pBlock : srResults.m_vBlocks) + { + const auto nRealFirstAddress = ConvertToRealAddress(pBlock.GetFirstAddress()); + const auto bResult = pBlock.EnumerateMatchingAddresses( + [this, &result, &pBlock, nRealFirstAddress, fCallback](ra::data::ByteAddress nAddress) { + result.nAddress = ConvertToRealAddress(nAddress); + + const uint32_t nOffset = result.nAddress - nRealFirstAddress; + result.nValue = BuildValue(pBlock.GetBytes() + nOffset); + return fCallback(result); + }); + + if (!bResult) + break; + } +} + size_t SearchImpl::GetIndexOfBlockForVirtualAddress(const SearchResults& srResults, uint32_t nAddress) { size_t nIndexLow = 0; diff --git a/src/services/Search/SearchImpl.hh b/src/services/Search/SearchImpl.hh index 99c17e4a..123fd72e 100644 --- a/src/services/Search/SearchImpl.hh +++ b/src/services/Search/SearchImpl.hh @@ -76,6 +76,10 @@ public: bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResult& result) const noexcept; + // calls the provided callback for each matching address + void EnumerateMatches(const SearchResults& srResults, + std::function fCallback) 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 aafc907d..eac33f49 100644 --- a/src/services/SearchResults.cpp +++ b/src/services/SearchResults.cpp @@ -384,6 +384,12 @@ bool SearchResults::ExcludeResult(const SearchResult& pResult) return false; } +void SearchResults::EnumerateMatches(std::function fCallback) const +{ + if (m_pImpl != nullptr) + m_pImpl->EnumerateMatches(*this, fCallback); +} + bool SearchResults::GetMatchingAddress(gsl::index nIndex, _Out_ SearchResult& result) const noexcept { if (m_pImpl == nullptr) diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h index 44c46283..b6541a17 100644 --- a/src/services/SearchResults.h +++ b/src/services/SearchResults.h @@ -126,6 +126,12 @@ class SearchResults /// Type of search to initialize. void Initialize(_In_ const std::vector& srResults, _In_ SearchType nSearchType); + /// + /// Calls the provided callback for each matching address. + /// + /// Callback to call for each element, returns true to continue iterating, or false to stop. + void EnumerateMatches(std::function fCallback) const; + /// /// Gets the nIndex'th matching address. /// diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 2b3058d2..f480a63c 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -253,26 +253,23 @@ void PointerFinderViewModel::FindBestChains(std::vector& void PointerFinderViewModel::GetPointerAddresses(std::vector& vPointerAddresses, const ra::services::SearchResults& pResults) { const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults.MatchingAddressCount()); nIndex++) - { - ra::services::SearchResult pResult; - if (!pResults.GetMatchingAddress(nIndex, pResult)) - continue; - + pResults.EnumerateMatches([&vPointerAddresses, &pConsoleContext](const ra::services::SearchResult& pResult) { // ignore null values if (pResult.nValue == 0) - continue; + return true; // ignore non-pointer values const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); if (nPointerAddress == 0xFFFFFFFF) - continue; + return true; // add to the list if not already there const auto pInsertIter = std::lower_bound(vPointerAddresses.begin(), vPointerAddresses.end(), nPointerAddress); if (pInsertIter == vPointerAddresses.end() || *pInsertIter != nPointerAddress) vPointerAddresses.insert(pInsertIter, nPointerAddress); - } + + return true; + }); } PointerFinderViewModel::PointerAddressRange PointerFinderViewModel::NarrowSearch( @@ -310,24 +307,19 @@ void PointerFinderViewModel::FindPointers(std::vector& vP return; const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults.MatchingAddressCount()); nIndex++) - { - ra::services::SearchResult pResult; - if (!pResults.GetMatchingAddress(nIndex, pResult)) - continue; - + pResults.EnumerateMatches([&pConsoleContext, &vPotentialPointers, pRange, nSearchAddress, nStateIndex](const ra::services::SearchResult& pResult) { // ignore null values if (pResult.nValue == 0) - continue; + return true; // ignore non-pointer values const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); if (nPointerAddress == 0xFFFFFFFF) - continue; + return true; // ignore pointers not in the search range if (!std::binary_search(pRange.first, pRange.second, nPointerAddress)) - continue; + return true; auto& pPointerChain = vPotentialPointers.emplace_back(); auto& pPointer = pPointerChain.vNodes.emplace_back(); @@ -335,7 +327,9 @@ void PointerFinderViewModel::FindPointers(std::vector& vP pPointer.nAddress = pResult.nAddress; pPointer.nOffset = ra::to_signed(nSearchAddress) - nPointerAddress; pPointer.nValue.at(nStateIndex) = pResult.nValue; - } + + return true; + }); } void PointerFinderViewModel::FindMatches(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex) @@ -344,6 +338,7 @@ void PointerFinderViewModel::FindMatches(std::vector& vPo return; auto pPotentialPointer = vPotentialPointers.begin(); + auto pMatchedPointer = vPotentialPointers.begin(); auto nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; const auto& pConsoleContext = ra::services::ServiceLocator::Get(); @@ -351,43 +346,43 @@ void PointerFinderViewModel::FindMatches(std::vector& vPo Expects(pResults != nullptr); const auto nSearchAddress = pState.Viewer().GetAddress(); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pResults->MatchingAddressCount()); nIndex++) + pResults->EnumerateMatches([&pConsoleContext, &vPotentialPointers, + &pPotentialPointer, &pMatchedPointer, &nNextAddress, nSearchAddress, + nStateIndex](const ra::services::SearchResult& pResult) { - ra::services::SearchResult pResult; - if (!pResults->GetMatchingAddress(nIndex, pResult)) - continue; - if (pResult.nAddress < nNextAddress) - continue; + return true; - bool bIsMatch = false; - if (pResult.nAddress == nNextAddress && pResult.nValue != 0) + if (pResult.nAddress == nNextAddress && // found pointer + pResult.nValue != 0) // ignore null values { const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); - if (nPointerAddress != 0xFFFFFFFF) + if (nPointerAddress != 0xFFFFFFFF) // ignore non-pointer values { auto& pNode = *pPotentialPointer->vNodes.begin(); if (nPointerAddress + pNode.nOffset == nSearchAddress) { + // pointer value + offset is target address, keep it! pNode.nValue.at(nStateIndex) = pResult.nValue; - bIsMatch = true; + + if (pPotentialPointer != pMatchedPointer) + std::iter_swap(pMatchedPointer, pPotentialPointer); + ++pMatchedPointer; } } } - pPotentialPointer->bPrune = !bIsMatch; ++pPotentialPointer; if (pPotentialPointer == vPotentialPointers.end()) - break; + return false; nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; - } + return true; + }); - vPotentialPointers.erase(std::remove_if( - vPotentialPointers.begin(), vPotentialPointers.end(), - [](const PotentialPointerChain& pChain) { return pChain.bPrune; } - ), vPotentialPointers.end()); + if (pPotentialPointer != pMatchedPointer) + vPotentialPointers.erase(pMatchedPointer, pPotentialPointer); } void PointerFinderViewModel::BookmarkSelected() From 877a7f6580ba933e234b0c2e1400de94a8851178 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Mon, 17 Aug 2026 06:42:41 -0600 Subject: [PATCH 4/9] extract logic to PointerFinder service --- src/RA_Integration.vcxproj | 2 + src/RA_Integration.vcxproj.filters | 6 + src/services/PointerFinder.cpp | 185 +++++++++++++ src/services/PointerFinder.hh | 82 ++++++ src/ui/viewmodels/PointerFinderViewModel.cpp | 259 +++++------------- src/ui/viewmodels/PointerFinderViewModel.hh | 31 +-- tests/RA_Integration.Tests.vcxproj | 1 + tests/RA_Integration.Tests.vcxproj.filters | 3 + .../PointerFinderViewModel_Tests.cpp | 70 +++-- 9 files changed, 403 insertions(+), 236 deletions(-) create mode 100644 src/services/PointerFinder.cpp create mode 100644 src/services/PointerFinder.hh diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj index 99863849..0dae967e 100644 --- a/src/RA_Integration.vcxproj +++ b/src/RA_Integration.vcxproj @@ -101,6 +101,7 @@ + @@ -237,6 +238,7 @@ + diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters index ad69bf40..da122679 100644 --- a/src/RA_Integration.vcxproj.filters +++ b/src/RA_Integration.vcxproj.filters @@ -381,6 +381,9 @@ UI\Win32 + + Services + @@ -932,6 +935,9 @@ UI\Win32 + + Services + diff --git a/src/services/PointerFinder.cpp b/src/services/PointerFinder.cpp new file mode 100644 index 00000000..6833db28 --- /dev/null +++ b/src/services/PointerFinder.cpp @@ -0,0 +1,185 @@ +#include "PointerFinder.hh" + +#include "RA_Defs.h" + +#include "context\IConsoleContext.hh" + +#include "services\ServiceLocator.hh" + +#include "util\Strings.hh" + +#include "ui\IDesktop.hh" +#include "ui\viewmodels\MessageBoxViewModel.hh" +#include "ui\viewmodels\WindowManager.hh" + +namespace ra { +namespace services { + +constexpr uint32_t MAX_OFFSET = 1024; + +void PointerFinder::Capture::Initialize(const SearchResults& pSearchResults) +{ + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + m_vPointerValues.reserve(pSearchResults.MatchingAddressCount()); + m_vPointerValues.clear(); + + pSearchResults.EnumerateMatches( + [&pConsoleContext, &vPointerValues = m_vPointerValues] + (const ra::services::SearchResult& pResult) + { + if (pResult.nValue != 0) // ignore null values + { + const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); + if (nPointerAddress != 0xFFFFFFFF) // ignore non-pointer values + vPointerValues.emplace_back(pResult.nAddress, static_cast(nPointerAddress)); + } + + return true; + }); +} + +uint32_t PointerFinder::Capture::GetValue(ra::data::ByteAddress nAddress) const +{ + const auto pLowerBound = std::lower_bound(m_vPointerValues.begin(), m_vPointerValues.end(), nAddress, + [](const PointerValue& pPointerValue, uint32_t nAddress) + { + return pPointerValue.nAddress < nAddress; + }); + return (pLowerBound < m_vPointerValues.end() && pLowerBound->nAddress == nAddress) ? pLowerBound->nValue : 0; +} + +PointerFinder::PointerAddressRange PointerFinder::Capture::NarrowSearch(ra::data::ByteAddress nTargetAddress) const +{ + if (m_vPointerValues.empty()) + return PointerAddressRange(m_vPointerValues.data(), m_vPointerValues.data()); + + const auto pSearchIter = std::lower_bound(m_vPointerValues.begin(), m_vPointerValues.end(), nTargetAddress, + [](const PointerValue& pPointerValue, uint32_t nAddress) + { + return pPointerValue.nAddress < nAddress; + }); + auto pEnd = pSearchIter; + auto pStart = pSearchIter; + + uint32_t nMaxOffset = MAX_OFFSET; + for (int i = 0; i < 4; ++i, nMaxOffset *= 2) + { + while (pEnd < m_vPointerValues.end() && pEnd->nAddress - nTargetAddress <= nMaxOffset) + ++pEnd; + + while (pStart > m_vPointerValues.begin() && nTargetAddress - (pStart - 1)->nAddress <= nMaxOffset) + --pStart; + + const auto nMatches = gsl::narrow_cast(pEnd - pStart); + if (nMatches >= 8 || nMatches == m_vPointerValues.size()) + break; + } + + return PointerAddressRange(m_vPointerValues.data() + (pStart - m_vPointerValues.begin()), + m_vPointerValues.data() + (pEnd - m_vPointerValues.begin())); +} + +void PointerFinder::AddCapture(const Capture& pCapture, ra::data::ByteAddress nTargetAddress) +{ + m_vCaptures.emplace_back(pCapture, nTargetAddress); +} + +void PointerFinder::Analyze(std::vector& vResults) +{ + if (m_vCaptures.empty()) + return; + + std::vector vRootNodes; + GetRootNodes(vRootNodes); + + GetDirectPointers(vResults, vRootNodes); + + std::sort(vResults.begin(), vResults.end(), + [](const PotentialPointer& a, const PotentialPointer& b) + { + if (a.nScore == b.nScore) + return a.nRootAddress < b.nRootAddress; + + return a.nScore < b.nScore; + }); +} + +void PointerFinder::GetRootNodes(std::vector& vRootNodes) const +{ + const auto& pFirstCapture = m_vCaptures.front().first; + + vRootNodes.reserve(pFirstCapture.m_vPointerValues.size()); + for (const auto& pPointerValue : pFirstCapture.m_vPointerValues) + vRootNodes.emplace_back(pPointerValue.nAddress); + + for (size_t i = 1; i < m_vCaptures.size(); ++i) + { + auto pRootNodeAddress = vRootNodes.begin(); + const auto& vCapturePointers = m_vCaptures.at(i).first.m_vPointerValues; + for (const auto& pCapturePointer : vCapturePointers) + { + if (*pRootNodeAddress < pCapturePointer.nAddress) + { + auto pNodeIterStart = pRootNodeAddress; + do + { + ++pRootNodeAddress; + } while (pRootNodeAddress < vRootNodes.end() && *pRootNodeAddress < pCapturePointer.nAddress); + + pRootNodeAddress = vRootNodes.erase(pNodeIterStart, pRootNodeAddress); + if (pRootNodeAddress == vRootNodes.end()) + break; + } + + if (*pRootNodeAddress == pCapturePointer.nAddress) + { + ++pRootNodeAddress; + if (pRootNodeAddress == vRootNodes.end()) + break; + } + } + } +} + +void PointerFinder::GetDirectPointers(std::vector& vDirectNodes, const std::vector& vRootNodes) const +{ + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + for (const auto pRootNodeAddress : vRootNodes) + { + int32_t nFirstOffset = 0; + bool bFirst = true; + bool bMatch = true; + for (const auto& pCapture : m_vCaptures) + { + const auto nValue = pCapture.first.GetValue(pRootNodeAddress); + const auto nPointedAtAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); + const auto nOffset = ra::to_signed(pCapture.second) - ra::to_signed(nPointedAtAddress); + + if (bFirst) + { + nFirstOffset = nOffset; + bFirst = false; + } + else if (nOffset != nFirstOffset) + { + bMatch = false; + break; + } + } + + if (bMatch) + { + auto& pDirectNode = vDirectNodes.emplace_back(); + pDirectNode.nRootAddress = pRootNodeAddress; + pDirectNode.vOffsets.push_back(nFirstOffset); + + // treat negative offsets as being farther away + pDirectNode.nScore = ra::to_unsigned((nFirstOffset < 0) ? (-nFirstOffset * 8) : nFirstOffset); + } + } +} + + + +} // namespace services +} // namespace ra diff --git a/src/services/PointerFinder.hh b/src/services/PointerFinder.hh new file mode 100644 index 00000000..7f24f8bf --- /dev/null +++ b/src/services/PointerFinder.hh @@ -0,0 +1,82 @@ +#ifndef RA_SERVICES_POINTERFINDER_HH +#define RA_SERVICES_POINTERFINDER_HH +#pragma once + +#include "data\Types.hh" + +#include "util\GSL.hh" + +#include "SearchResults.h" + +namespace ra { +namespace services { + +class PointerFinder +{ +public: + GSL_SUPPRESS_F6 PointerFinder() = default; + virtual ~PointerFinder() = default; + + PointerFinder(const PointerFinder&) noexcept = delete; + PointerFinder& operator=(const PointerFinder&) noexcept = delete; + PointerFinder(PointerFinder&&) noexcept = delete; + PointerFinder& operator=(PointerFinder&&) noexcept = delete; + +private: + typedef struct PointerValue + { + PointerValue(ra::data::ByteAddress nAddress, uint32_t nValue) + : nAddress(nAddress), nValue(nValue) + { + } + + ra::data::ByteAddress nAddress; + uint32_t nValue; + } PointerValue; + typedef std::pair PointerAddressRange; + +public: + class Capture + { + public: + void Initialize(const SearchResults& pSearchResults); + + uint32_t GetValue(ra::data::ByteAddress nAddress) const; + + private: + friend class PointerFinder; + PointerAddressRange NarrowSearch(ra::data::ByteAddress nTargetAddress) const; + + std::vector m_vPointerValues; + }; + + void AddCapture(const Capture& pCapture, ra::data::ByteAddress nTargetAddress); + + typedef struct PotentialPointer + { + ra::data::ByteAddress nRootAddress = 0; + uint32_t nScore = 0; + std::vector vOffsets; + } PotentialPointer; + + void Analyze(std::vector& vResults); + +private: + typedef struct Node + { + ra::data::ByteAddress nAddress = 0; // the address of the pointer + int32_t nOffset = 0; // the offset to add to the pointer to reach the value + uint32_t nScore = 0; // the composite distance to reach the final value + ra::data::ByteAddress nNextAddress = 0; // the address of the next pointer node + } Node; + + void GetRootNodes(std::vector& vRootNodes) const; + void GetDirectPointers(std::vector& vDirectNodes, const std::vector& vRootNodes) const; + + std::vector> m_vCaptures; +}; + +} // namespace services +} // namespace ra + +#endif // !RA_SERVICES_FRAME_EVENT_QUEUE_HH diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index f480a63c..67e04789 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -95,22 +95,13 @@ void PointerFinderViewModel::StateViewModel::Capture() } DispatchMemoryRead([this]() { - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); const auto& pMemoryContext = ra::services::ServiceLocator::Get(); const auto nMemorySize = gsl::narrow(pMemoryContext.TotalMemorySize()); ra::services::SearchResults pInitialResults; pInitialResults.Initialize(0, nMemorySize, m_pOwner->GetSearchType()); - m_pCapture.reset(new ra::services::SearchResults()); - - m_pCapture->Initialize(pInitialResults, - [&pConsoleContext](const ra::services::SearchResult& pSearchResult) { - if (pSearchResult.nValue == 0) - return false; - - return (pConsoleContext.ByteAddressFromRealAddress(pSearchResult.nValue) != 0xFFFFFFFF); - }); + m_pCapture.Initialize(pInitialResults); }); SetValue(CaptureButtonTextProperty, L"Release"); @@ -119,7 +110,8 @@ void PointerFinderViewModel::StateViewModel::Capture() void PointerFinderViewModel::StateViewModel::ClearCapture() { - m_pCapture.reset(); + ra::services::SearchResults pEmptyResults; + m_pCapture.Initialize(pEmptyResults); SetValue(CaptureButtonTextProperty, CaptureButtonTextProperty.GetDefaultValue()); SetValue(CanCaptureProperty, true); @@ -169,54 +161,82 @@ void PointerFinderViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& void PointerFinderViewModel::Find() { - std::vector vPotentialPointers; - ra::data::Memory::Size nSize = ra::data::Memory::Size::Unknown; - bool bPerformedSearch = false; - // TODO: capture/restore selected address (find may be clicked again after changing captures) m_vResults.BeginUpdate(); m_vResults.Clear(); + ra::services::PointerFinder pPointerFinder; + int nUniqueAddresses = 0; + ra::data::ByteAddress nPreviousAddress = 0xFFFFFFFF; for (size_t i = 0; i < m_vStates.size(); i++) { const auto& pStateI = m_vStates.at(i); if (pStateI.CanCapture()) continue; - if (vPotentialPointers.empty()) - { - FindBestChains(vPotentialPointers, pStateI, i); - nSize = pStateI.CapturedMemory()->GetSize(); - } - else + const auto nAddress = pStateI.Viewer().GetAddress(); + pPointerFinder.AddCapture(pStateI.CapturedMemory(), nAddress); + if (nAddress != nPreviousAddress) { - FindMatches(vPotentialPointers, pStateI, i); - bPerformedSearch = true; + nPreviousAddress = nAddress; + ++nUniqueAddresses; } - - if (vPotentialPointers.empty()) - break; } + std::vector vResults; + pPointerFinder.Analyze(vResults); + + const auto nSize = GetSearchSize(); + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); const auto& pMemoryContext = ra::services::ServiceLocator::Get(); - for (const auto& pPotentialPointer : vPotentialPointers) + for (const auto& pPotentialPointer : vResults) { auto& pPointer = m_vResults.Add(); - const auto& pNode = pPotentialPointer.vNodes.at(0); - pPointer.m_nAddress = pNode.nAddress; + pPointer.m_nAddress = pPotentialPointer.nRootAddress; pPointer.SetPointerAddress(pMemoryContext.FormatAddress(pPointer.m_nAddress)); - pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", pNode.nOffset)); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vStates.size()); ++nIndex) + auto nOffset = pPotentialPointer.vOffsets.front(); + pPointer.m_nOffset = nOffset; + pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); + + std::array vPointerAddress = {}; + for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) { - const auto nValue = pNode.nValue.at(nIndex); - if (nValue) - pPointer.SetPointerValue(nIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); + const auto& pState = m_vStates.at(nStateIndex); + if (!pState.CanCapture()) + { + const auto nValue = pState.CapturedMemory().GetValue(pPointer.m_nAddress); + pPointer.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); + + const auto nAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); + vPointerAddress.at(nStateIndex) = nAddress + nOffset; + } + } + + for (gsl::index nOffsetIndex = 1; nOffsetIndex < gsl::narrow_cast(pPotentialPointer.vOffsets.size()); ++nOffsetIndex) + { + auto& pOffset = m_vResults.Add(); + nOffset = pPotentialPointer.vOffsets.at(nOffsetIndex); + pOffset.m_nOffset = nOffset; + pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); + + for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) + { + auto nPointerAddress = vPointerAddress.at(nStateIndex); + if (nPointerAddress) + { + const auto nValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(nPointerAddress); + pOffset.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); + + const auto nAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); + vPointerAddress.at(nStateIndex) = nAddress + nOffset; + } + } } } - if (m_vResults.Count() == 0 && bPerformedSearch) + if (m_vResults.Count() == 0 && nUniqueAddresses >= 2) { auto* pPointer = &m_vResults.Add(); pPointer->SetPointerAddress(L"No pointers found."); @@ -224,165 +244,28 @@ void PointerFinderViewModel::Find() } else { - SetValue(ResultCountTextProperty, std::to_wstring(m_vResults.Count())); + SetValue(ResultCountTextProperty, std::to_wstring(vResults.size())); } m_vResults.EndUpdate(); - if (!bPerformedSearch) + if (nUniqueAddresses < 2) ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Cannot find.", L"At least two unique addresses must be captured before potential pointers can be located."); } -void PointerFinderViewModel::FindBestChains(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex) -{ - // extract all pointers from the search results - const auto* pResults = pState.CapturedMemory(); - Expects(pResults != nullptr); - - std::vector vPointerAddresses; - GetPointerAddresses(vPointerAddresses, *pResults); - - // limit the results to addresses within MAX_OFFSET of the pointer value - const auto nSearchAddress = pState.Viewer().GetAddress(); - const auto pRange = NarrowSearch(vPointerAddresses, nSearchAddress); - - // get things pointing to the filtered addresses - FindPointers(vPotentialPointers, *pResults, nStateIndex, vPointerAddresses, pRange, nSearchAddress); -} - -void PointerFinderViewModel::GetPointerAddresses(std::vector& vPointerAddresses, const ra::services::SearchResults& pResults) +ra::data::Memory::Size PointerFinderViewModel::GetSearchSize() const { - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - pResults.EnumerateMatches([&vPointerAddresses, &pConsoleContext](const ra::services::SearchResult& pResult) { - // ignore null values - if (pResult.nValue == 0) - return true; - - // ignore non-pointer values - const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); - if (nPointerAddress == 0xFFFFFFFF) - return true; - - // add to the list if not already there - const auto pInsertIter = std::lower_bound(vPointerAddresses.begin(), vPointerAddresses.end(), nPointerAddress); - if (pInsertIter == vPointerAddresses.end() || *pInsertIter != nPointerAddress) - vPointerAddresses.insert(pInsertIter, nPointerAddress); - - return true; - }); -} - -PointerFinderViewModel::PointerAddressRange PointerFinderViewModel::NarrowSearch( - const std::vector& vPointerAddresses, ra::data::ByteAddress nSearchAddress) -{ - if (vPointerAddresses.size() == 0) - return PointerAddressRange(vPointerAddresses.data(), vPointerAddresses.data()); - - const auto pSearchIter = std::lower_bound(vPointerAddresses.begin(), vPointerAddresses.end(), nSearchAddress); - auto pEnd = pSearchIter; - auto pStart = pSearchIter; - - uint32_t nMaxOffset = MAX_OFFSET; - for (int i = 0; i < 4; ++i, nMaxOffset *= 2) + auto nSize = ra::data::Memory::Size::ThirtyTwoBit; + switch (GetSearchType()) { - while (pEnd < vPointerAddresses.end() && *pEnd - nSearchAddress <= nMaxOffset) - ++pEnd; - - while (pStart > vPointerAddresses.begin() && nSearchAddress - *(pStart - 1) <= nMaxOffset) - --pStart; - - const auto nMatches = gsl::narrow_cast(pEnd - pStart); - if (nMatches >= 8 || nMatches == vPointerAddresses.size()) + case ra::services::SearchType::SixteenBit: + case ra::services::SearchType::SixteenBitAligned: + case ra::services::SearchType::SixteenBitBigEndian: + nSize = ra::data::Memory::Size::SixteenBit; break; } - return PointerAddressRange(vPointerAddresses.data() + (pStart - vPointerAddresses.begin()), - vPointerAddresses.data() + (pEnd - vPointerAddresses.begin())); -} - -void PointerFinderViewModel::FindPointers(std::vector& vPotentialPointers, const ra::services::SearchResults& pResults, size_t nStateIndex, - const std::vector& vPointerAddresses, PointerFinderViewModel::PointerAddressRange pRange, ra::data::ByteAddress nSearchAddress) -{ - if (pRange.first == pRange.second) - return; - - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - pResults.EnumerateMatches([&pConsoleContext, &vPotentialPointers, pRange, nSearchAddress, nStateIndex](const ra::services::SearchResult& pResult) { - // ignore null values - if (pResult.nValue == 0) - return true; - - // ignore non-pointer values - const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); - if (nPointerAddress == 0xFFFFFFFF) - return true; - - // ignore pointers not in the search range - if (!std::binary_search(pRange.first, pRange.second, nPointerAddress)) - return true; - - auto& pPointerChain = vPotentialPointers.emplace_back(); - auto& pPointer = pPointerChain.vNodes.emplace_back(); - memset(&pPointer, 0, sizeof(pPointer)); - pPointer.nAddress = pResult.nAddress; - pPointer.nOffset = ra::to_signed(nSearchAddress) - nPointerAddress; - pPointer.nValue.at(nStateIndex) = pResult.nValue; - - return true; - }); -} - -void PointerFinderViewModel::FindMatches(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex) -{ - if (vPotentialPointers.empty()) - return; - - auto pPotentialPointer = vPotentialPointers.begin(); - auto pMatchedPointer = vPotentialPointers.begin(); - auto nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - - const auto* pResults = pState.CapturedMemory(); - Expects(pResults != nullptr); - const auto nSearchAddress = pState.Viewer().GetAddress(); - - pResults->EnumerateMatches([&pConsoleContext, &vPotentialPointers, - &pPotentialPointer, &pMatchedPointer, &nNextAddress, nSearchAddress, - nStateIndex](const ra::services::SearchResult& pResult) - { - if (pResult.nAddress < nNextAddress) - return true; - - if (pResult.nAddress == nNextAddress && // found pointer - pResult.nValue != 0) // ignore null values - { - const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); - if (nPointerAddress != 0xFFFFFFFF) // ignore non-pointer values - { - auto& pNode = *pPotentialPointer->vNodes.begin(); - if (nPointerAddress + pNode.nOffset == nSearchAddress) - { - // pointer value + offset is target address, keep it! - pNode.nValue.at(nStateIndex) = pResult.nValue; - - if (pPotentialPointer != pMatchedPointer) - std::iter_swap(pMatchedPointer, pPotentialPointer); - ++pMatchedPointer; - } - } - } - - ++pPotentialPointer; - - if (pPotentialPointer == vPotentialPointers.end()) - return false; - - nNextAddress = pPotentialPointer->vNodes.begin()->nAddress; - return true; - }); - - if (pPotentialPointer != pMatchedPointer) - vPotentialPointers.erase(pMatchedPointer, pPotentialPointer); + return nSize; } void PointerFinderViewModel::BookmarkSelected() @@ -395,17 +278,7 @@ void PointerFinderViewModel::BookmarkSelected() { if (pItem.IsSelected()) { - auto nSize = ra::data::Memory::Size::ThirtyTwoBit; - switch (GetSearchType()) - { - case ra::services::SearchType::SixteenBit: - case ra::services::SearchType::SixteenBitAligned: - case ra::services::SearchType::SixteenBitBigEndian: - nSize = ra::data::Memory::Size::SixteenBit; - break; - } - - vmBookmarks.AddBookmark(pItem.m_nAddress, nSize); + vmBookmarks.AddBookmark(pItem.m_nAddress, GetSearchSize()); break; } } diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 97d66176..0b4379c3 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.hh +++ b/src/ui/viewmodels/PointerFinderViewModel.hh @@ -2,6 +2,7 @@ #define RA_UI_POINTERFINDERVIEWMODEL_H #pragma once +#include "services/PointerFinder.hh" #include "services/SearchResults.h" #include "ui/WindowViewModelBase.hh" @@ -108,7 +109,7 @@ public: void ToggleCapture(); - const ra::services::SearchResults* CapturedMemory() const noexcept { return m_pCapture.get(); } + const ra::services::PointerFinder::Capture& CapturedMemory() const noexcept { return m_pCapture; } MemoryViewerViewModel& Viewer() noexcept { return m_pViewer; } const MemoryViewerViewModel& Viewer() const noexcept { return m_pViewer; } @@ -126,7 +127,7 @@ public: PointerFinderViewModel* m_pOwner = nullptr; MemoryViewerViewModel m_pViewer; - std::unique_ptr m_pCapture; + ra::services::PointerFinder::Capture m_pCapture; }; std::array& States() noexcept { return m_vStates; } @@ -284,29 +285,11 @@ public: protected: void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; -private: - std::array m_vStates; + ra::data::Memory::Size GetSearchSize() const; - struct PotentialPointerNode - { - ra::data::ByteAddress nAddress = 0; - int32_t nOffset = 0; - std::array nValue {}; - }; - struct PotentialPointerChain - { - std::vector vNodes; - int nScore = 0; - bool bPrune = false; - }; - typedef std::pair PointerAddressRange; - void FindBestChains(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex); - void FindPointers(std::vector& vPotentialPointers, const ra::services::SearchResults& pResults, size_t nStateIndex, - const std::vector& vPointerAddresses, PointerAddressRange pRange, ra::data::ByteAddress nSearchAddress); - void FindMatches(std::vector& vPotentialPointers, const StateViewModel& pState, size_t nStateIndex); - - static void GetPointerAddresses(std::vector& vPointerAddresses, const ra::services::SearchResults& pResults); - static PointerAddressRange NarrowSearch(const std::vector& vPointerAddresses, ra::data::ByteAddress nSearchAddress); +private: + static constexpr size_t NUM_STATES = 4; + std::array m_vStates; ra::ui::ViewModelCollection m_vResults; LookupItemViewModelCollection m_vSearchTypes; diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj index 02f1f0ec..2500f5a2 100644 --- a/tests/RA_Integration.Tests.vcxproj +++ b/tests/RA_Integration.Tests.vcxproj @@ -94,6 +94,7 @@ + diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters index 6a44d291..60471cd0 100644 --- a/tests/RA_Integration.Tests.vcxproj.filters +++ b/tests/RA_Integration.Tests.vcxproj.filters @@ -399,6 +399,9 @@ Code + + Code + diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index 653d96df..e63ec006 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -176,7 +176,6 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsTrue(pState.CanCapture()); Assert::AreEqual(std::wstring(L"0x1234"), pState.GetAddress()); Assert::AreEqual(std::wstring(L"Capture"), pState.GetCaptureButtonText()); - Assert::IsNull(pState.CapturedMemory()); pState.ToggleCapture(); @@ -185,7 +184,6 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsFalse(pState.CanCapture()); Assert::AreEqual(std::wstring(L"0x1234"), pState.GetAddress()); Assert::AreEqual(std::wstring(L"Release"), pState.GetCaptureButtonText()); - Assert::IsNotNull(pState.CapturedMemory()); pState.ToggleCapture(); @@ -194,7 +192,6 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsTrue(pState.CanCapture()); Assert::AreEqual(std::wstring(L"0x1234"), pState.GetAddress()); Assert::AreEqual(std::wstring(L"Capture"), pState.GetCaptureButtonText()); - Assert::IsNull(pState.CapturedMemory()); } } @@ -387,9 +384,9 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); Assert::AreEqual({ 3U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(1, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(2, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(0, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + vmPointerFinder.AssertRow(2, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 Assert::AreEqual(std::wstring(L"3"), vmPointerFinder.GetResultCountText()); } @@ -428,8 +425,8 @@ TEST_CLASS(PointerFinderViewModel_Tests) vmPointerFinder.Find(); Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); Assert::AreEqual({ 2U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"001c", L"0034", L"0034"); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(1, L"0x00a4", L"+0x00", L"0020", L"0020", L"0038", L"0038"); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(0, L"0x00a4", L"+0x00", L"0020", L"0020", L"0038", L"0038"); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"001c", L"0034", L"0034"); // 1c+04=>20, 34+04=>38 Assert::AreEqual(std::wstring(L"2"), vmPointerFinder.GetResultCountText()); } @@ -458,6 +455,41 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::AreEqual(std::wstring(L"0"), vmPointerFinder.GetResultCountText()); } + TEST_METHOD(TestFindOffsetNested) + { + PointerFinderViewModelHarness vmPointerFinder; + vmPointerFinder.mockGameContext.SetGameId(1U); + vmPointerFinder.SetSearchType(ra::services::SearchType::SixteenBitAligned); + + // 0008 + 4 + 12 + std::array pMemory{}; + pMemory.at(0x08) = 0x1c; // $0000 = 001C + 4 => 0020 + pMemory.at(0x20) = 0x44; // $0020 = 0044 + 12 => 0x50 + pMemory.at(0x21) = 0x00; + pMemory.at(0x50) = 0x55; // $0050 + pMemory.at(0x51) = 0x46; + vmPointerFinder.MockMemory(pMemory); + + vmPointerFinder.States().at(0).SetAddress(L"0x50"); + vmPointerFinder.States().at(0).ToggleCapture(); + + pMemory.at(0x08) = 0x34; // $0000 = 0034 + 4 => 0038 + pMemory.at(0x38) = 0x68; // $0038 = 0068 + 12 => 0074 + pMemory.at(0x39) = 0x00; + pMemory.at(0x74) = 0x55; + pMemory.at(0x75) = 0x46; + + vmPointerFinder.States().at(1).SetAddress(L"0x74"); + vmPointerFinder.States().at(1).ToggleCapture(); + vmPointerFinder.Find(); + + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + Assert::AreEqual({ 2U }, vmPointerFinder.PotentialPointers().Count()); + vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + vmPointerFinder.AssertRow(1, L"", L"+0x0C", L"0044", L"0068", L"", L""); // 44+0C=>50, 68+0C=>74 + Assert::AreEqual(std::wstring(L"1"), vmPointerFinder.GetResultCountText()); + } + TEST_METHOD(TestBookmarkSelected) { PointerFinderViewModelHarness vmPointerFinder; @@ -492,9 +524,9 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); Assert::AreEqual({ 3U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(1, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(2, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(0, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + vmPointerFinder.AssertRow(2, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 // no selection vmPointerFinder.BookmarkSelected(); @@ -504,7 +536,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); vmPointerFinder.BookmarkSelected(); Assert::AreEqual({ 1U }, pBookmarks.Items().Count()); - Assert::AreEqual({ 0x0070U }, pBookmarks.Items().GetItemAt(0)->GetAddress()); + Assert::AreEqual({ 0x0008U }, pBookmarks.Items().GetItemAt(0)->GetAddress()); } TEST_METHOD(TestExportResults) @@ -533,9 +565,9 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); Assert::AreEqual({ 3U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(1, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(2, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(0, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + vmPointerFinder.AssertRow(2, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 bool bDialogSeen = false; vmPointerFinder.mockDesktop.ExpectWindow( @@ -556,7 +588,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsTrue(bDialogSeen); const std::string& sContents = vmPointerFinder.mockFileSystem.GetFileContents(L"E:\\Data\\3-Pointers.csv"); - Assert::AreEqual(std::string("Address,Offset,State1,State2\n0x0008,+0x04,001c,0034\n0x0070,+0x04,001c,0034\n0x009c,+0x00,0020,0038\n"), + Assert::AreEqual(std::string("Address,Offset,State1,State2\n0x009c,+0x00,0020,0038\n0x0008,+0x04,001c,0034\n0x0070,+0x04,001c,0034\n"), sContents); } @@ -586,9 +618,9 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); Assert::AreEqual({ 3U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(1, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(2, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(0, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 + vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + vmPointerFinder.AssertRow(2, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 bool bDialogSeen = false; vmPointerFinder.mockDesktop.ExpectWindow( From 516b006533e88bf7cfda4e97e04c9312fad24d5f Mon Sep 17 00:00:00 2001 From: Jamiras Date: Mon, 17 Aug 2026 14:53:40 -0600 Subject: [PATCH 5/9] support for nested pointers --- src/services/PointerFinder.cpp | 302 ++++++++++++++---- src/services/PointerFinder.hh | 44 ++- src/ui/viewmodels/PointerFinderViewModel.cpp | 51 ++- src/ui/viewmodels/PointerFinderViewModel.hh | 2 + .../PointerFinderViewModel_Tests.cpp | 1 + 5 files changed, 322 insertions(+), 78 deletions(-) diff --git a/src/services/PointerFinder.cpp b/src/services/PointerFinder.cpp index 6833db28..29234d8f 100644 --- a/src/services/PointerFinder.cpp +++ b/src/services/PointerFinder.cpp @@ -15,7 +15,8 @@ namespace ra { namespace services { -constexpr uint32_t MAX_OFFSET = 1024; +constexpr int32_t MAX_OFFSET = 1024; +constexpr uint32_t MAX_DEPTH = 4; void PointerFinder::Capture::Initialize(const SearchResults& pSearchResults) { @@ -31,21 +32,31 @@ void PointerFinder::Capture::Initialize(const SearchResults& pSearchResults) { const auto nPointerAddress = pConsoleContext.ByteAddressFromRealAddress(pResult.nValue); if (nPointerAddress != 0xFFFFFFFF) // ignore non-pointer values - vPointerValues.emplace_back(pResult.nAddress, static_cast(nPointerAddress)); + vPointerValues.emplace_back(pResult.nAddress, pResult.nValue, nPointerAddress); } return true; }); } -uint32_t PointerFinder::Capture::GetValue(ra::data::ByteAddress nAddress) const +const PointerFinder::PointerValue* PointerFinder::Capture::GetValue(ra::data::ByteAddress nAddress) const { const auto pLowerBound = std::lower_bound(m_vPointerValues.begin(), m_vPointerValues.end(), nAddress, [](const PointerValue& pPointerValue, uint32_t nAddress) { return pPointerValue.nAddress < nAddress; }); - return (pLowerBound < m_vPointerValues.end() && pLowerBound->nAddress == nAddress) ? pLowerBound->nValue : 0; + return (pLowerBound < m_vPointerValues.end() && pLowerBound->nAddress == nAddress) ? &*pLowerBound : nullptr; +} + +ra::data::ByteAddress PointerFinder::Capture::GetValueAsAddress(ra::data::ByteAddress nAddress) const +{ + const auto pLowerBound = std::lower_bound(m_vPointerValues.begin(), m_vPointerValues.end(), nAddress, + [](const PointerValue& pPointerValue, uint32_t nAddress) + { + return pPointerValue.nAddress < nAddress; + }); + return (pLowerBound < m_vPointerValues.end() && pLowerBound->nAddress == nAddress) ? pLowerBound->nValueAsAddress : 0; } PointerFinder::PointerAddressRange PointerFinder::Capture::NarrowSearch(ra::data::ByteAddress nTargetAddress) const @@ -84,17 +95,19 @@ void PointerFinder::AddCapture(const Capture& pCapture, ra::data::ByteAddress nT m_vCaptures.emplace_back(pCapture, nTargetAddress); } -void PointerFinder::Analyze(std::vector& vResults) +void PointerFinder::Analyze(std::vector& vResults, std::function fProgress) { if (m_vCaptures.empty()) return; - std::vector vRootNodes; - GetRootNodes(vRootNodes); + GetPointers(vResults, fProgress); - GetDirectPointers(vResults, vRootNodes); + SortPointers(vResults); +} - std::sort(vResults.begin(), vResults.end(), +void PointerFinder::SortPointers(std::vector& vPointers) +{ + std::sort(vPointers.begin(), vPointers.end(), [](const PotentialPointer& a, const PotentialPointer& b) { if (a.nScore == b.nScore) @@ -104,82 +117,257 @@ void PointerFinder::Analyze(std::vector& vResults) }); } -void PointerFinder::GetRootNodes(std::vector& vRootNodes) const +static uint32_t CalculateScore(int32_t nOffset) noexcept { - const auto& pFirstCapture = m_vCaptures.front().first; + // treat negative offsets as being farther away + return ra::to_unsigned((nOffset < 0) ? (-nOffset * 8) : nOffset); +} - vRootNodes.reserve(pFirstCapture.m_vPointerValues.size()); - for (const auto& pPointerValue : pFirstCapture.m_vPointerValues) - vRootNodes.emplace_back(pPointerValue.nAddress); +void PointerFinder::InitializeBestRoutes(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddress) +{ + vPointers.reserve(pCapture.m_vPointerValues.size()); - for (size_t i = 1; i < m_vCaptures.size(); ++i) + for (const auto& pValue : pCapture.m_vPointerValues) { - auto pRootNodeAddress = vRootNodes.begin(); - const auto& vCapturePointers = m_vCaptures.at(i).first.m_vPointerValues; - for (const auto& pCapturePointer : vCapturePointers) + const int32_t nOffset = ra::to_signed(nTargetAddress) - ra::to_signed(pValue.nValueAsAddress); + if (nOffset < MAX_OFFSET && nOffset > -MAX_OFFSET) { - if (*pRootNodeAddress < pCapturePointer.nAddress) - { - auto pNodeIterStart = pRootNodeAddress; - do - { - ++pRootNodeAddress; - } while (pRootNodeAddress < vRootNodes.end() && *pRootNodeAddress < pCapturePointer.nAddress); + auto& pPointer = vPointers.emplace_back(); + pPointer.nRootAddress = pValue.nAddress; - pRootNodeAddress = vRootNodes.erase(pNodeIterStart, pRootNodeAddress); - if (pRootNodeAddress == vRootNodes.end()) - break; - } + pPointer.vOffsets.push_back(nOffset); + pPointer.nScore = CalculateScore(nOffset); + } + } - if (*pRootNodeAddress == pCapturePointer.nAddress) - { - ++pRootNodeAddress; - if (pRootNodeAddress == vRootNodes.end()) - break; - } + std::sort(vPointers.begin(), vPointers.end(), + [](const PotentialPointer& a, const PotentialPointer& b) + { + return a.vOffsets.front() < b.vOffsets.front(); + }); +} + +void PointerFinder::GetPointers(std::vector& vIndirectNodes, std::function fProgress) const +{ + AnalysisState pAnalysisState; + pAnalysisState.fProgress = fProgress; + pAnalysisState.vResults = &vIndirectNodes; + + for (const auto& pCapture : m_vCaptures) + { + auto& pCaptureMetrics = pAnalysisState.vCaptureMetrics.emplace_back(); + pCaptureMetrics.pCapture = &pCapture.first; + InitializeBestRoutes(pCaptureMetrics.vBestRoutes, pCapture.first, pCapture.second); + } + + if (pAnalysisState.fProgress) + { + pAnalysisState.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vBestRoutes.size(); + pAnalysisState.nMaxProgress = pAnalysisState.nRootProgressSize * (pAnalysisState.nRootProgressSize + 1); + pAnalysisState.fProgress(0, pAnalysisState.nMaxProgress); + } + + GetPointers(vIndirectNodes, pAnalysisState); +} + +void PointerFinder::GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState) +{ + for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) + pCaptureMetrics.pIterator = pCaptureMetrics.vBestRoutes.cbegin(); + + const size_t nProgressStart = pAnalysisState.nProgress; + do + { + if (!FindSharedOffset(pAnalysisState.vCaptureMetrics)) + break; + + ProcessSharedOffset(vIndirectNodes, pAnalysisState); + + for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) + pCaptureMetrics.pIterator = pCaptureMetrics.pStopIterator; + + if (pAnalysisState.fProgress && pAnalysisState.vOffsets.size() < 2) + { + if (pAnalysisState.vOffsets.size() == 1) + pAnalysisState.nProgress = nProgressStart + (pAnalysisState.vCaptureMetrics.front().pIterator - pAnalysisState.vCaptureMetrics.front().vBestRoutes.cbegin()); + else + pAnalysisState.nProgress++; + + pAnalysisState.fProgress(pAnalysisState.nProgress, pAnalysisState.nMaxProgress); } + } while (true); + + if (pAnalysisState.fProgress && pAnalysisState.vOffsets.size() < 2) + { + if (pAnalysisState.vOffsets.size() == 1) + pAnalysisState.nProgress = nProgressStart + pAnalysisState.nRootProgressSize; + else + pAnalysisState.nProgress++; + + pAnalysisState.fProgress(pAnalysisState.nProgress, pAnalysisState.nMaxProgress); } } -void PointerFinder::GetDirectPointers(std::vector& vDirectNodes, const std::vector& vRootNodes) const +bool PointerFinder::FindSharedOffset(std::vector& vCaptureMetrics) noexcept { - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - for (const auto pRootNodeAddress : vRootNodes) + int32_t nMaxOffset = std::numeric_limits::min(); + for (auto& pCaptureMetric : vCaptureMetrics) + { + if (pCaptureMetric.pIterator == pCaptureMetric.vBestRoutes.cend()) + return false; + + nMaxOffset = std::max(nMaxOffset, pCaptureMetric.pIterator->vOffsets.front()); + } + + bool bRepeat; + do { - int32_t nFirstOffset = 0; - bool bFirst = true; - bool bMatch = true; - for (const auto& pCapture : m_vCaptures) + bRepeat = false; + for (auto& pCaptureMetrics : vCaptureMetrics) { - const auto nValue = pCapture.first.GetValue(pRootNodeAddress); - const auto nPointedAtAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); - const auto nOffset = ra::to_signed(pCapture.second) - ra::to_signed(nPointedAtAddress); + const auto pStop = pCaptureMetrics.vBestRoutes.cend(); - if (bFirst) + auto pIter = pCaptureMetrics.pIterator; + if (pIter->vOffsets.front() < nMaxOffset) { - nFirstOffset = nOffset; - bFirst = false; + do { + if (++pIter == pStop) + return false; + } while (pIter->vOffsets.front() < nMaxOffset); } - else if (nOffset != nFirstOffset) + + if (pIter->vOffsets.front() > nMaxOffset) { - bMatch = false; + nMaxOffset = pIter->vOffsets.front(); + bRepeat = true; break; } + + pCaptureMetrics.pIterator = pIter; + + do { + ++pIter; + } while (pIter < pStop && pIter->vOffsets.front() == nMaxOffset); + + pCaptureMetrics.pStopIterator = pIter; } + } while (bRepeat); - if (bMatch) - { - auto& pDirectNode = vDirectNodes.emplace_back(); - pDirectNode.nRootAddress = pRootNodeAddress; - pDirectNode.vOffsets.push_back(nFirstOffset); + return true; +} - // treat negative offsets as being farther away - pDirectNode.nScore = ra::to_unsigned((nFirstOffset < 0) ? (-nFirstOffset * 8) : nFirstOffset); +void PointerFinder::ProcessSharedOffset(std::vector& vIndirectNodes, AnalysisState& pAnalysisState) +{ + std::vector::const_iterator> vIterators; + vIterators.reserve(pAnalysisState.vCaptureMetrics.size()); + for (const auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) + vIterators.push_back(pCaptureMetrics.pIterator); + + const auto nOffset = vIterators.front()->vOffsets.front(); + + AnalysisState pNewAnalysisState; + pNewAnalysisState.nScore = pAnalysisState.nScore + CalculateScore(nOffset); + + pNewAnalysisState.vOffsets.push_back(nOffset); + for (const auto nChildOffset : pAnalysisState.vOffsets) + pNewAnalysisState.vOffsets.push_back(nChildOffset); + + if (pNewAnalysisState.vOffsets.size() < MAX_DEPTH) + { + pNewAnalysisState.fProgress = pAnalysisState.fProgress; + pNewAnalysisState.nRootProgressSize = pAnalysisState.nRootProgressSize; + pNewAnalysisState.nMaxProgress = pAnalysisState.nMaxProgress; + pNewAnalysisState.vResults = pAnalysisState.vResults; + + gsl::index nIndex = 0; + for (const auto& pCaptureMetric : pAnalysisState.vCaptureMetrics) + { + auto& pNewCaptureMetric = pNewAnalysisState.vCaptureMetrics.emplace_back(); + pNewCaptureMetric.pCapture = pCaptureMetric.pCapture; + ++nIndex; } } + + do + { + if (PointersMatch(vIterators)) + { + const auto nRootAddress = vIterators.front()->nRootAddress; + bool bFound = false; + for (auto& pPointer : vIndirectNodes) + { + if (pPointer.nRootAddress == nRootAddress) + { + // this root pointer was already captured. only keep this + // route if it's more efficient (lower score). + if (pNewAnalysisState.nScore < pPointer.nScore) + { + pPointer.vOffsets = pNewAnalysisState.vOffsets; + pPointer.nScore = pNewAnalysisState.nScore; + } + + bFound = true; + break; + } + } + if (!bFound) + { + auto& pPointer = vIndirectNodes.emplace_back(); + pPointer.nRootAddress = nRootAddress; + pPointer.vOffsets = pNewAnalysisState.vOffsets; + pPointer.nScore = pNewAnalysisState.nScore; + } + } + else if (!pNewAnalysisState.vCaptureMetrics.empty()) + { + gsl::index nIndex = 0; + for (auto& pCaptureMetric : pNewAnalysisState.vCaptureMetrics) + { + pCaptureMetric.vBestRoutes.clear(); + InitializeBestRoutes(pCaptureMetric.vBestRoutes, *pCaptureMetric.pCapture, vIterators.at(nIndex)->nRootAddress); + ++nIndex; + } + + pNewAnalysisState.nProgress = pAnalysisState.nProgress; + GetPointers(vIndirectNodes, pNewAnalysisState); + pAnalysisState.nProgress = pNewAnalysisState.nProgress; + } + + // advance to the next combination + gsl::index nIndex = 0; + do + { + const auto& pCaptureMetrics = pAnalysisState.vCaptureMetrics.at(nIndex); + auto pIter = vIterators.at(nIndex); + if (++pIter < pCaptureMetrics.pStopIterator) + { + vIterators.at(nIndex) = pIter; + break; + } + + vIterators.at(nIndex) = pCaptureMetrics.pIterator; + if (++nIndex == gsl::narrow_cast(vIterators.size())) + return; + + } while (true); + } while (true); } +bool PointerFinder::PointersMatch(std::vector::const_iterator>& vIterators) noexcept +{ + auto pIter = vIterators.begin(); + const ra::data::ByteAddress nAddress = (*pIter)->nRootAddress; + ++pIter; + for (; pIter < vIterators.end(); ++pIter) + { + const ra::data::ByteAddress nOtherAddress = (*pIter)->nRootAddress; + if (nOtherAddress != nAddress) + return false; + } + + return true; +} } // namespace services } // namespace ra diff --git a/src/services/PointerFinder.hh b/src/services/PointerFinder.hh index 7f24f8bf..fa610b5d 100644 --- a/src/services/PointerFinder.hh +++ b/src/services/PointerFinder.hh @@ -22,17 +22,19 @@ public: PointerFinder(PointerFinder&&) noexcept = delete; PointerFinder& operator=(PointerFinder&&) noexcept = delete; -private: typedef struct PointerValue { - PointerValue(ra::data::ByteAddress nAddress, uint32_t nValue) - : nAddress(nAddress), nValue(nValue) + PointerValue(ra::data::ByteAddress nAddress, uint32_t nValue, ra::data::ByteAddress nValueAsAddress) noexcept + : nAddress(nAddress), nValue(nValue), nValueAsAddress(nValueAsAddress) { } - + ra::data::ByteAddress nAddress; uint32_t nValue; + ra::data::ByteAddress nValueAsAddress; } PointerValue; + +private: typedef std::pair PointerAddressRange; public: @@ -41,7 +43,8 @@ public: public: void Initialize(const SearchResults& pSearchResults); - uint32_t GetValue(ra::data::ByteAddress nAddress) const; + const PointerValue* GetValue(ra::data::ByteAddress nAddress) const; + ra::data::ByteAddress GetValueAsAddress(ra::data::ByteAddress nAddress) const; private: friend class PointerFinder; @@ -59,7 +62,7 @@ public: std::vector vOffsets; } PotentialPointer; - void Analyze(std::vector& vResults); + void Analyze(std::vector& vResults, std::function fProgress); private: typedef struct Node @@ -70,8 +73,33 @@ private: ra::data::ByteAddress nNextAddress = 0; // the address of the next pointer node } Node; - void GetRootNodes(std::vector& vRootNodes) const; - void GetDirectPointers(std::vector& vDirectNodes, const std::vector& vRootNodes) const; + typedef struct CaptureMetrics + { + std::vector vBestRoutes; + std::vector::const_iterator pIterator; + std::vector::const_iterator pStopIterator; + const Capture* pCapture = nullptr; + } CaptureMetrics; + + typedef struct AnalysisState + { + std::vector vCaptureMetrics; + std::vector vOffsets; + uint32_t nScore = 0; + size_t nProgress = 0; + size_t nRootProgressSize = 0; + size_t nMaxProgress = 0; + std::function fProgress; + std::vector* vResults = nullptr; + } AnalysisState; + + void GetPointers(std::vector& vIndirectNodes, std::function fProgress) const; + static void GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); + static void SortPointers(std::vector& vPointers); + static void InitializeBestRoutes(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddres); + static bool FindSharedOffset(std::vector& vCaptureMetrics) noexcept; + static void ProcessSharedOffset(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); + static bool PointersMatch(std::vector::const_iterator>& vIterators) noexcept; std::vector> m_vCaptures; }; diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 67e04789..029f2274 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -13,6 +13,7 @@ #include "ui/viewmodels/FileDialogViewModel.hh" #include "ui/viewmodels/MessageBoxViewModel.hh" +#include "ui/viewmodels/ProgressViewModel.hh" #include "ui/viewmodels/WindowManager.hh" namespace ra { @@ -21,6 +22,7 @@ namespace viewmodels { const StringModelProperty PointerFinderViewModel::ResultCountTextProperty("PointerFinderViewModel", "ResultCountText", L"0"); const IntModelProperty PointerFinderViewModel::SearchTypeProperty("PointerFinderViewModel", "SearchType", ra::etoi(ra::services::SearchType::ThirtyTwoBitAligned)); +const BoolModelProperty PointerFinderViewModel::ShowProgressDialogProperty("PointerFinderViewModel", "ShowProgressDialog", true); const StringModelProperty PointerFinderViewModel::StateViewModel::AddressProperty("StateViewModel", "Address", L""); const StringModelProperty PointerFinderViewModel::StateViewModel::CaptureButtonTextProperty("StateViewModel", "CaptureButtonText", L"Capture"); @@ -185,10 +187,31 @@ void PointerFinderViewModel::Find() } std::vector vResults; - pPointerFinder.Analyze(vResults); + + if (GetValue(ShowProgressDialogProperty)) + { + ProgressViewModel vmProgress; + vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing potential pointers")); + vmProgress.QueueTask([&pPointerFinder, &vResults, &vmProgress]() { + size_t nLastTotal = 0; + pPointerFinder.Analyze(vResults, [&nLastTotal, &vmProgress](size_t nProgress, size_t nTotal) + { + if (nTotal != nLastTotal) + { + vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing %u potential pointers", gsl::narrow_cast(nTotal))); + nLastTotal = nTotal; + } + vmProgress.SetProgress(gsl::narrow_cast(nProgress * 100 / nTotal)); + }); + }); + vmProgress.ShowModal(); + } + else + { + pPointerFinder.Analyze(vResults, nullptr); + } const auto nSize = GetSearchSize(); - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); const auto& pMemoryContext = ra::services::ServiceLocator::Get(); for (const auto& pPotentialPointer : vResults) { @@ -206,11 +229,12 @@ void PointerFinderViewModel::Find() const auto& pState = m_vStates.at(nStateIndex); if (!pState.CanCapture()) { - const auto nValue = pState.CapturedMemory().GetValue(pPointer.m_nAddress); - pPointer.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); - - const auto nAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); - vPointerAddress.at(nStateIndex) = nAddress + nOffset; + const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(pPointer.m_nAddress); + if (pValue) + { + pPointer.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); + vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; + } } } @@ -219,18 +243,19 @@ void PointerFinderViewModel::Find() auto& pOffset = m_vResults.Add(); nOffset = pPotentialPointer.vOffsets.at(nOffsetIndex); pOffset.m_nOffset = nOffset; - pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); + pOffset.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) { auto nPointerAddress = vPointerAddress.at(nStateIndex); if (nPointerAddress) { - const auto nValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(nPointerAddress); - pOffset.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(nValue, nSize, ra::data::Memory::Format::Hex)); - - const auto nAddress = pConsoleContext.ByteAddressFromRealAddress(nValue); - vPointerAddress.at(nStateIndex) = nAddress + nOffset; + const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(nPointerAddress); + if (pValue) + { + pOffset.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); + vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; + } } } } diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 0b4379c3..41695201 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.hh +++ b/src/ui/viewmodels/PointerFinderViewModel.hh @@ -283,6 +283,8 @@ public: } protected: + static const BoolModelProperty ShowProgressDialogProperty; + void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; ra::data::Memory::Size GetSearchSize() const; diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index e63ec006..b2233ff7 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -38,6 +38,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) GSL_SUPPRESS_F6 PointerFinderViewModelHarness() : PointerFinderViewModel() { + SetValue(ShowProgressDialogProperty, false); } ~PointerFinderViewModelHarness() From 231c5dbc9bc4ddfe67d1f0cd07eb306ab9444732 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 18 Aug 2026 18:22:57 -0600 Subject: [PATCH 6/9] remove unshared offsets before processing --- src/services/PointerFinder.cpp | 180 +++++++++++------- src/services/PointerFinder.hh | 16 +- src/ui/viewmodels/PointerFinderViewModel.cpp | 15 +- .../PointerFinderViewModel_Tests.cpp | 4 +- 4 files changed, 135 insertions(+), 80 deletions(-) diff --git a/src/services/PointerFinder.cpp b/src/services/PointerFinder.cpp index 29234d8f..7466e1e9 100644 --- a/src/services/PointerFinder.cpp +++ b/src/services/PointerFinder.cpp @@ -16,7 +16,6 @@ namespace ra { namespace services { constexpr int32_t MAX_OFFSET = 1024; -constexpr uint32_t MAX_DEPTH = 4; void PointerFinder::Capture::Initialize(const SearchResults& pSearchResults) { @@ -123,7 +122,7 @@ static uint32_t CalculateScore(int32_t nOffset) noexcept return ra::to_unsigned((nOffset < 0) ? (-nOffset * 8) : nOffset); } -void PointerFinder::InitializeBestRoutes(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddress) +void PointerFinder::InitializePotentialPointers(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddress) { vPointers.reserve(pCapture.m_vPointerValues.size()); @@ -135,7 +134,7 @@ void PointerFinder::InitializeBestRoutes(std::vector& vPointer auto& pPointer = vPointers.emplace_back(); pPointer.nRootAddress = pValue.nAddress; - pPointer.vOffsets.push_back(nOffset); + pPointer.vOffsets.at(pPointer.nOffsetLength++) = nOffset; pPointer.nScore = CalculateScore(nOffset); } } @@ -147,6 +146,58 @@ void PointerFinder::InitializeBestRoutes(std::vector& vPointer }); } +void PointerFinder::RemoveUnsharedOffsets(std::vector& vCaptureMetrics) +{ + // determine which offsets are shared across all captures + std::vector vSharedOffsets; + + int32_t nPrevOffset = std::numeric_limits::min(); + for (const auto& pPointer : vCaptureMetrics.front().vPotentialPointers) + { + const auto nOffset = pPointer.vOffsets.front(); + if (nOffset != nPrevOffset) + { + vSharedOffsets.push_back(nOffset); + nPrevOffset = nOffset; + } + } + + for (auto pCaptureMetrics = vCaptureMetrics.begin() + 1; pCaptureMetrics < vCaptureMetrics.end(); ++pCaptureMetrics) + { + vSharedOffsets.erase(std::remove_if(vSharedOffsets.begin(), vSharedOffsets.end(), + [&pCaptureMetrics](const int32_t nOffset) noexcept + { + for (const auto& pPointer : pCaptureMetrics->vPotentialPointers) + { + if (pPointer.vOffsets.front() == nOffset) + return false; + } + + return true; + }), + vSharedOffsets.end()); + } + + // remove any potential pointers at offsets not shared by all captures + if (vSharedOffsets.empty()) + { + for (auto& pCaptureMetrics : vCaptureMetrics) + pCaptureMetrics.vPotentialPointers.clear(); + } + else + { + for (auto& pCaptureMetrics : vCaptureMetrics) + { + pCaptureMetrics.vPotentialPointers.erase(std::remove_if(pCaptureMetrics.vPotentialPointers.begin(), pCaptureMetrics.vPotentialPointers.end(), + [&vSharedOffsets](const PotentialPointer& a) + { + return !std::binary_search(vSharedOffsets.begin(), vSharedOffsets.end(), a.vOffsets.front()); + }), + pCaptureMetrics.vPotentialPointers.end()); + } + } +} + void PointerFinder::GetPointers(std::vector& vIndirectNodes, std::function fProgress) const { AnalysisState pAnalysisState; @@ -157,13 +208,16 @@ void PointerFinder::GetPointers(std::vector& vIndirectNodes, s { auto& pCaptureMetrics = pAnalysisState.vCaptureMetrics.emplace_back(); pCaptureMetrics.pCapture = &pCapture.first; - InitializeBestRoutes(pCaptureMetrics.vBestRoutes, pCapture.first, pCapture.second); + InitializePotentialPointers(pCaptureMetrics.vPotentialPointers, pCapture.first, pCapture.second); } + RemoveUnsharedOffsets(pAnalysisState.vCaptureMetrics); + if (pAnalysisState.fProgress) { - pAnalysisState.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vBestRoutes.size(); - pAnalysisState.nMaxProgress = pAnalysisState.nRootProgressSize * (pAnalysisState.nRootProgressSize + 1); + constexpr size_t nMinProgress = 1; // std::max cannot deduce type of literal + pAnalysisState.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vPotentialPointers.size(); + pAnalysisState.nMaxProgress = std::max(nMinProgress, pAnalysisState.nRootProgressSize * (pAnalysisState.nRootProgressSize + 1)); pAnalysisState.fProgress(0, pAnalysisState.nMaxProgress); } @@ -173,7 +227,7 @@ void PointerFinder::GetPointers(std::vector& vIndirectNodes, s void PointerFinder::GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState) { for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) - pCaptureMetrics.pIterator = pCaptureMetrics.vBestRoutes.cbegin(); + pCaptureMetrics.pIterator = pCaptureMetrics.vPotentialPointers.cbegin(); const size_t nProgressStart = pAnalysisState.nProgress; do @@ -186,20 +240,21 @@ void PointerFinder::GetPointers(std::vector& vIndirectNodes, A for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) pCaptureMetrics.pIterator = pCaptureMetrics.pStopIterator; - if (pAnalysisState.fProgress && pAnalysisState.vOffsets.size() < 2) + if (pAnalysisState.fProgress && pAnalysisState.nOffsetLength < 2) { - if (pAnalysisState.vOffsets.size() == 1) - pAnalysisState.nProgress = nProgressStart + (pAnalysisState.vCaptureMetrics.front().pIterator - pAnalysisState.vCaptureMetrics.front().vBestRoutes.cbegin()); + const auto nItemsProcessed = (pAnalysisState.vCaptureMetrics.front().pIterator - pAnalysisState.vCaptureMetrics.front().vPotentialPointers.cbegin()); + if (pAnalysisState.nOffsetLength == 1) + pAnalysisState.nProgress = nProgressStart + nItemsProcessed; else - pAnalysisState.nProgress++; + pAnalysisState.nProgress = nProgressStart + nItemsProcessed * (pAnalysisState.nRootProgressSize + 1); pAnalysisState.fProgress(pAnalysisState.nProgress, pAnalysisState.nMaxProgress); } } while (true); - if (pAnalysisState.fProgress && pAnalysisState.vOffsets.size() < 2) + if (pAnalysisState.fProgress && pAnalysisState.nOffsetLength < 2) { - if (pAnalysisState.vOffsets.size() == 1) + if (pAnalysisState.nOffsetLength == 1) pAnalysisState.nProgress = nProgressStart + pAnalysisState.nRootProgressSize; else pAnalysisState.nProgress++; @@ -210,48 +265,21 @@ void PointerFinder::GetPointers(std::vector& vIndirectNodes, A bool PointerFinder::FindSharedOffset(std::vector& vCaptureMetrics) noexcept { - int32_t nMaxOffset = std::numeric_limits::min(); - for (auto& pCaptureMetric : vCaptureMetrics) - { - if (pCaptureMetric.pIterator == pCaptureMetric.vBestRoutes.cend()) - return false; + // assert: metrics arrays only contain shared offsets per RemoveUnsharedOffsets + // and they're in incremental order, so all we have to do is update the + // stop iterators. + if (vCaptureMetrics.front().pIterator == vCaptureMetrics.front().vPotentialPointers.cend()) + return false; - nMaxOffset = std::max(nMaxOffset, pCaptureMetric.pIterator->vOffsets.front()); - } + const auto nOffset = vCaptureMetrics.front().pIterator->vOffsets.front(); - bool bRepeat; - do + for (auto& pCaptureMetric : vCaptureMetrics) { - bRepeat = false; - for (auto& pCaptureMetrics : vCaptureMetrics) - { - const auto pStop = pCaptureMetrics.vBestRoutes.cend(); - - auto pIter = pCaptureMetrics.pIterator; - if (pIter->vOffsets.front() < nMaxOffset) - { - do { - if (++pIter == pStop) - return false; - } while (pIter->vOffsets.front() < nMaxOffset); - } - - if (pIter->vOffsets.front() > nMaxOffset) - { - nMaxOffset = pIter->vOffsets.front(); - bRepeat = true; - break; - } - - pCaptureMetrics.pIterator = pIter; - - do { - ++pIter; - } while (pIter < pStop && pIter->vOffsets.front() == nMaxOffset); - - pCaptureMetrics.pStopIterator = pIter; - } - } while (bRepeat); + auto pIter = pCaptureMetric.pIterator; + while (pIter < pCaptureMetric.vPotentialPointers.end() && pIter->vOffsets.front() == nOffset) + ++pIter; + pCaptureMetric.pStopIterator = pIter; + } return true; } @@ -268,11 +296,11 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect AnalysisState pNewAnalysisState; pNewAnalysisState.nScore = pAnalysisState.nScore + CalculateScore(nOffset); - pNewAnalysisState.vOffsets.push_back(nOffset); - for (const auto nChildOffset : pAnalysisState.vOffsets) - pNewAnalysisState.vOffsets.push_back(nChildOffset); + pNewAnalysisState.vOffsets.at(0) = nOffset; + memcpy(&pNewAnalysisState.vOffsets.at(1), &pAnalysisState.vOffsets.at(0), pAnalysisState.nOffsetLength * sizeof(pAnalysisState.vOffsets.at(0))); + pNewAnalysisState.nOffsetLength = pAnalysisState.nOffsetLength + 1; - if (pNewAnalysisState.vOffsets.size() < MAX_DEPTH) + if (pNewAnalysisState.nOffsetLength < MAX_DEPTH) { pNewAnalysisState.fProgress = pAnalysisState.fProgress; pNewAnalysisState.nRootProgressSize = pAnalysisState.nRootProgressSize; @@ -303,6 +331,7 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect if (pNewAnalysisState.nScore < pPointer.nScore) { pPointer.vOffsets = pNewAnalysisState.vOffsets; + pPointer.nOffsetLength = pNewAnalysisState.nOffsetLength; pPointer.nScore = pNewAnalysisState.nScore; } @@ -315,22 +344,43 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect auto& pPointer = vIndirectNodes.emplace_back(); pPointer.nRootAddress = nRootAddress; pPointer.vOffsets = pNewAnalysisState.vOffsets; + pPointer.nOffsetLength = pNewAnalysisState.nOffsetLength; pPointer.nScore = pNewAnalysisState.nScore; } } else if (!pNewAnalysisState.vCaptureMetrics.empty()) { - gsl::index nIndex = 0; - for (auto& pCaptureMetric : pNewAnalysisState.vCaptureMetrics) + const auto nRootAddress = vIterators.front()->nRootAddress; + if (!std::binary_search(pAnalysisState.vDeadEndAddresses.begin(), pAnalysisState.vDeadEndAddresses.end(), nRootAddress)) { - pCaptureMetric.vBestRoutes.clear(); - InitializeBestRoutes(pCaptureMetric.vBestRoutes, *pCaptureMetric.pCapture, vIterators.at(nIndex)->nRootAddress); - ++nIndex; - } + gsl::index nIndex = 0; + for (auto& pCaptureMetric : pNewAnalysisState.vCaptureMetrics) + { + pCaptureMetric.vPotentialPointers.clear(); + InitializePotentialPointers(pCaptureMetric.vPotentialPointers, *pCaptureMetric.pCapture, vIterators.at(nIndex)->nRootAddress); + ++nIndex; + } + + RemoveUnsharedOffsets(pNewAnalysisState.vCaptureMetrics); + + const auto nFoundPointers = pAnalysisState.vResults->size(); - pNewAnalysisState.nProgress = pAnalysisState.nProgress; - GetPointers(vIndirectNodes, pNewAnalysisState); - pAnalysisState.nProgress = pNewAnalysisState.nProgress; + pNewAnalysisState.nProgress = pAnalysisState.nProgress; + GetPointers(vIndirectNodes, pNewAnalysisState); + pAnalysisState.nProgress = pNewAnalysisState.nProgress; + + if (pAnalysisState.vResults->size() == nFoundPointers) + { + const auto pInsertAt = std::lower_bound(pAnalysisState.vDeadEndAddresses.begin(), pAnalysisState.vDeadEndAddresses.end(), nRootAddress); + if (pInsertAt == pAnalysisState.vDeadEndAddresses.end() || *pInsertAt != nRootAddress) + pAnalysisState.vDeadEndAddresses.insert(pInsertAt, nRootAddress); + } + } + else + { + static int nUnused = 0; + nUnused++; + } } // advance to the next combination diff --git a/src/services/PointerFinder.hh b/src/services/PointerFinder.hh index fa610b5d..7a11cb1c 100644 --- a/src/services/PointerFinder.hh +++ b/src/services/PointerFinder.hh @@ -42,6 +42,7 @@ public: { public: void Initialize(const SearchResults& pSearchResults); + size_t GetCapturedPointerCount() const noexcept { return m_vPointerValues.size(); } const PointerValue* GetValue(ra::data::ByteAddress nAddress) const; ra::data::ByteAddress GetValueAsAddress(ra::data::ByteAddress nAddress) const; @@ -55,11 +56,14 @@ public: void AddCapture(const Capture& pCapture, ra::data::ByteAddress nTargetAddress); + static constexpr uint32_t MAX_DEPTH = 4; + typedef struct PotentialPointer { ra::data::ByteAddress nRootAddress = 0; uint32_t nScore = 0; - std::vector vOffsets; + uint32_t nOffsetLength = 0; + std::array vOffsets{}; } PotentialPointer; void Analyze(std::vector& vResults, std::function fProgress); @@ -75,7 +79,7 @@ private: typedef struct CaptureMetrics { - std::vector vBestRoutes; + std::vector vPotentialPointers; std::vector::const_iterator pIterator; std::vector::const_iterator pStopIterator; const Capture* pCapture = nullptr; @@ -84,8 +88,11 @@ private: typedef struct AnalysisState { std::vector vCaptureMetrics; - std::vector vOffsets; uint32_t nScore = 0; + uint32_t nOffsetLength = 0; + std::array vOffsets{}; + + std::vector vDeadEndAddresses; size_t nProgress = 0; size_t nRootProgressSize = 0; size_t nMaxProgress = 0; @@ -96,7 +103,8 @@ private: void GetPointers(std::vector& vIndirectNodes, std::function fProgress) const; static void GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); static void SortPointers(std::vector& vPointers); - static void InitializeBestRoutes(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddres); + static void InitializePotentialPointers(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddres); + static void RemoveUnsharedOffsets(std::vector& vCaptureMetrics); static bool FindSharedOffset(std::vector& vCaptureMetrics) noexcept; static void ProcessSharedOffset(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); static bool PointersMatch(std::vector::const_iterator>& vIterators) noexcept; diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 029f2274..ed2343b0 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -170,6 +170,7 @@ void PointerFinderViewModel::Find() ra::services::PointerFinder pPointerFinder; int nUniqueAddresses = 0; + size_t nPointerCount = 0; ra::data::ByteAddress nPreviousAddress = 0xFFFFFFFF; for (size_t i = 0; i < m_vStates.size(); i++) { @@ -184,6 +185,8 @@ void PointerFinderViewModel::Find() nPreviousAddress = nAddress; ++nUniqueAddresses; } + + nPointerCount += pStateI.CapturedMemory().GetCapturedPointerCount(); } std::vector vResults; @@ -191,16 +194,10 @@ void PointerFinderViewModel::Find() if (GetValue(ShowProgressDialogProperty)) { ProgressViewModel vmProgress; - vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing potential pointers")); + vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing %u potential pointers", gsl::narrow_cast(nPointerCount))); vmProgress.QueueTask([&pPointerFinder, &vResults, &vmProgress]() { - size_t nLastTotal = 0; - pPointerFinder.Analyze(vResults, [&nLastTotal, &vmProgress](size_t nProgress, size_t nTotal) + pPointerFinder.Analyze(vResults, [&vmProgress](size_t nProgress, size_t nTotal) { - if (nTotal != nLastTotal) - { - vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing %u potential pointers", gsl::narrow_cast(nTotal))); - nLastTotal = nTotal; - } vmProgress.SetProgress(gsl::narrow_cast(nProgress * 100 / nTotal)); }); }); @@ -238,7 +235,7 @@ void PointerFinderViewModel::Find() } } - for (gsl::index nOffsetIndex = 1; nOffsetIndex < gsl::narrow_cast(pPotentialPointer.vOffsets.size()); ++nOffsetIndex) + for (gsl::index nOffsetIndex = 1; nOffsetIndex < gsl::narrow_cast(pPotentialPointer.nOffsetLength); ++nOffsetIndex) { auto& pOffset = m_vResults.Add(); nOffset = pPotentialPointer.vOffsets.at(nOffsetIndex); diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index b2233ff7..0897d732 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -464,7 +464,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) // 0008 + 4 + 12 std::array pMemory{}; - pMemory.at(0x08) = 0x1c; // $0000 = 001C + 4 => 0020 + pMemory.at(0x08) = 0x1c; // $0008 = 001C + 4 => 0020 pMemory.at(0x20) = 0x44; // $0020 = 0044 + 12 => 0x50 pMemory.at(0x21) = 0x00; pMemory.at(0x50) = 0x55; // $0050 @@ -474,7 +474,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) vmPointerFinder.States().at(0).SetAddress(L"0x50"); vmPointerFinder.States().at(0).ToggleCapture(); - pMemory.at(0x08) = 0x34; // $0000 = 0034 + 4 => 0038 + pMemory.at(0x08) = 0x34; // $0008 = 0034 + 4 => 0038 pMemory.at(0x38) = 0x68; // $0038 = 0068 + 12 => 0074 pMemory.at(0x39) = 0x00; pMemory.at(0x74) = 0x55; From 9448b01ae663c6650d2b05edc5daeabd51cab358 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 19 Aug 2026 17:24:56 -0600 Subject: [PATCH 7/9] hook up cancel button --- src/services/PointerFinder.cpp | 196 +++++++++++++------ src/services/PointerFinder.hh | 41 ++-- src/ui/viewmodels/PointerFinderViewModel.cpp | 15 +- 3 files changed, 171 insertions(+), 81 deletions(-) diff --git a/src/services/PointerFinder.cpp b/src/services/PointerFinder.cpp index 7466e1e9..18f830d9 100644 --- a/src/services/PointerFinder.cpp +++ b/src/services/PointerFinder.cpp @@ -12,10 +12,30 @@ #include "ui\viewmodels\MessageBoxViewModel.hh" #include "ui\viewmodels\WindowManager.hh" +/* General algorithm: + * - For each of 2 or more snapshots, identify any memory that looks like a pointer + * - For a given target address: + * - For each snapshot: + * - Examine the memory at the address pointed to by each of the captured pointers + * and calculate the offset from that address to the target address. + * - Keep any pointers that point to an address within MAX_OFFSET bytes of the target address. + * - Cross-reference the snapshots to find an offset to the target address that is + * present in all snapshots. + * - Examine all pointers that point to an address that is exactly that many bytes + * away from the target address. + * - If all snapshots contain a match at the same address, a pointer was successfully found. + * - Otherwise, repeat the process looking for the target address from the first snapshot. + * - If one is found, check the other snapshots to see if that address points to the + * target addresses from those snapshots. + * - If they do match, a pointer chain was found. + * - This process can be repeated recursively up to MAX_DEPTH + * - This process can be repeated for any other offsets that are present in all snapshots. + */ + namespace ra { namespace services { -constexpr int32_t MAX_OFFSET = 1024; +constexpr int32_t MAX_OFFSET = 8192; void PointerFinder::Capture::Initialize(const SearchResults& pSearchResults) { @@ -94,7 +114,7 @@ void PointerFinder::AddCapture(const Capture& pCapture, ra::data::ByteAddress nT m_vCaptures.emplace_back(pCapture, nTargetAddress); } -void PointerFinder::Analyze(std::vector& vResults, std::function fProgress) +void PointerFinder::Analyze(std::vector& vResults, std::function fProgress) { if (m_vCaptures.empty()) return; @@ -198,11 +218,14 @@ void PointerFinder::RemoveUnsharedOffsets(std::vector& vCaptureM } } -void PointerFinder::GetPointers(std::vector& vIndirectNodes, std::function fProgress) const +void PointerFinder::GetPointers(std::vector& vIndirectNodes, std::function fProgress) const { + AnalysisProgress pProgress; + pProgress.fProgress = fProgress; + pProgress.vResults = &vIndirectNodes; + AnalysisState pAnalysisState; - pAnalysisState.fProgress = fProgress; - pAnalysisState.vResults = &vIndirectNodes; + pAnalysisState.pProgress = &pProgress; for (const auto& pCapture : m_vCaptures) { @@ -213,53 +236,68 @@ void PointerFinder::GetPointers(std::vector& vIndirectNodes, s RemoveUnsharedOffsets(pAnalysisState.vCaptureMetrics); - if (pAnalysisState.fProgress) + if (pProgress.fProgress) { constexpr size_t nMinProgress = 1; // std::max cannot deduce type of literal - pAnalysisState.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vPotentialPointers.size(); - pAnalysisState.nMaxProgress = std::max(nMinProgress, pAnalysisState.nRootProgressSize * (pAnalysisState.nRootProgressSize + 1)); - pAnalysisState.fProgress(0, pAnalysisState.nMaxProgress); + pProgress.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vPotentialPointers.size(); + pProgress.nMaxProgress = std::max(nMinProgress, pProgress.nRootProgressSize * (pProgress.nRootProgressSize + 1)); + pProgress.fProgress(0, pProgress.nMaxProgress); } - GetPointers(vIndirectNodes, pAnalysisState); + AnalyzeState(pAnalysisState); } -void PointerFinder::GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState) +void PointerFinder::AnalyzeState(AnalysisState& pAnalysisState) { for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) pCaptureMetrics.pIterator = pCaptureMetrics.vPotentialPointers.cbegin(); - const size_t nProgressStart = pAnalysisState.nProgress; + const size_t nProgressStart = pAnalysisState.pProgress->nProgress; do { if (!FindSharedOffset(pAnalysisState.vCaptureMetrics)) break; - ProcessSharedOffset(vIndirectNodes, pAnalysisState); + ProcessSharedOffset(pAnalysisState); + if (pAnalysisState.pProgress->bAborted) + return; for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) pCaptureMetrics.pIterator = pCaptureMetrics.pStopIterator; - if (pAnalysisState.fProgress && pAnalysisState.nOffsetLength < 2) + if (pAnalysisState.pProgress->fProgress) { - const auto nItemsProcessed = (pAnalysisState.vCaptureMetrics.front().pIterator - pAnalysisState.vCaptureMetrics.front().vPotentialPointers.cbegin()); - if (pAnalysisState.nOffsetLength == 1) - pAnalysisState.nProgress = nProgressStart + nItemsProcessed; - else - pAnalysisState.nProgress = nProgressStart + nItemsProcessed * (pAnalysisState.nRootProgressSize + 1); + if (pAnalysisState.nDepth < 2) + { + const auto nItemsProcessed = (pAnalysisState.vCaptureMetrics.front().pIterator - pAnalysisState.vCaptureMetrics.front().vPotentialPointers.cbegin()); + if (pAnalysisState.nDepth == 1) + pAnalysisState.pProgress->nProgress = nProgressStart + nItemsProcessed; + else + pAnalysisState.pProgress->nProgress = nProgressStart + nItemsProcessed * (pAnalysisState.pProgress->nRootProgressSize + 1); + } + + if (++pAnalysisState.pProgress->nChecksSinceLastProgressUpdated > 500) + { + pAnalysisState.pProgress->nChecksSinceLastProgressUpdated = 0; - pAnalysisState.fProgress(pAnalysisState.nProgress, pAnalysisState.nMaxProgress); + if (!pAnalysisState.pProgress->fProgress(pAnalysisState.pProgress->nProgress, pAnalysisState.pProgress->nMaxProgress)) + { + pAnalysisState.pProgress->bAborted = true; + return; + } + } } } while (true); - if (pAnalysisState.fProgress && pAnalysisState.nOffsetLength < 2) + if (pAnalysisState.pProgress->fProgress && pAnalysisState.nDepth < 2) { - if (pAnalysisState.nOffsetLength == 1) - pAnalysisState.nProgress = nProgressStart + pAnalysisState.nRootProgressSize; + if (pAnalysisState.nDepth == 1) + pAnalysisState.pProgress->nProgress = nProgressStart + pAnalysisState.pProgress->nRootProgressSize; else - pAnalysisState.nProgress++; + pAnalysisState.pProgress->nProgress++; - pAnalysisState.fProgress(pAnalysisState.nProgress, pAnalysisState.nMaxProgress); + if (!pAnalysisState.pProgress->fProgress(pAnalysisState.pProgress->nProgress, pAnalysisState.pProgress->nMaxProgress)) + pAnalysisState.pProgress->bAborted = true; } } @@ -284,7 +322,7 @@ bool PointerFinder::FindSharedOffset(std::vector& vCaptureMetric return true; } -void PointerFinder::ProcessSharedOffset(std::vector& vIndirectNodes, AnalysisState& pAnalysisState) +void PointerFinder::ProcessSharedOffset(AnalysisState& pAnalysisState) { std::vector::const_iterator> vIterators; vIterators.reserve(pAnalysisState.vCaptureMetrics.size()); @@ -292,20 +330,17 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect vIterators.push_back(pCaptureMetrics.pIterator); const auto nOffset = vIterators.front()->vOffsets.front(); + const auto nDepth = pAnalysisState.nDepth + 1; + auto* nOffsetFront = &pAnalysisState.pProgress->vOffsets.at(MAX_DEPTH - nDepth); + *nOffsetFront = nOffset; AnalysisState pNewAnalysisState; pNewAnalysisState.nScore = pAnalysisState.nScore + CalculateScore(nOffset); - pNewAnalysisState.vOffsets.at(0) = nOffset; - memcpy(&pNewAnalysisState.vOffsets.at(1), &pAnalysisState.vOffsets.at(0), pAnalysisState.nOffsetLength * sizeof(pAnalysisState.vOffsets.at(0))); - pNewAnalysisState.nOffsetLength = pAnalysisState.nOffsetLength + 1; - - if (pNewAnalysisState.nOffsetLength < MAX_DEPTH) + if (nDepth < MAX_DEPTH) { - pNewAnalysisState.fProgress = pAnalysisState.fProgress; - pNewAnalysisState.nRootProgressSize = pAnalysisState.nRootProgressSize; - pNewAnalysisState.nMaxProgress = pAnalysisState.nMaxProgress; - pNewAnalysisState.vResults = pAnalysisState.vResults; + pNewAnalysisState.nDepth = nDepth; + pNewAnalysisState.pProgress = pAnalysisState.pProgress; gsl::index nIndex = 0; for (const auto& pCaptureMetric : pAnalysisState.vCaptureMetrics) @@ -320,9 +355,10 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect { if (PointersMatch(vIterators)) { + // found a route. check to see if another route exists const auto nRootAddress = vIterators.front()->nRootAddress; bool bFound = false; - for (auto& pPointer : vIndirectNodes) + for (auto& pPointer : *pAnalysisState.pProgress->vResults) { if (pPointer.nRootAddress == nRootAddress) { @@ -330,59 +366,89 @@ void PointerFinder::ProcessSharedOffset(std::vector& vIndirect // route if it's more efficient (lower score). if (pNewAnalysisState.nScore < pPointer.nScore) { - pPointer.vOffsets = pNewAnalysisState.vOffsets; - pPointer.nOffsetLength = pNewAnalysisState.nOffsetLength; pPointer.nScore = pNewAnalysisState.nScore; + memcpy(&pPointer.vOffsets.front(), nOffsetFront, nDepth * sizeof(pPointer.vOffsets[0])); + pPointer.nOffsetLength = nDepth; } bFound = true; break; } } + + // no route exists for this address, add one. if (!bFound) { - auto& pPointer = vIndirectNodes.emplace_back(); + auto& pPointer = pAnalysisState.pProgress->vResults->emplace_back(); pPointer.nRootAddress = nRootAddress; - pPointer.vOffsets = pNewAnalysisState.vOffsets; - pPointer.nOffsetLength = pNewAnalysisState.nOffsetLength; + memcpy(&pPointer.vOffsets.front(), nOffsetFront, nDepth * sizeof(pPointer.vOffsets[0])); + pPointer.nOffsetLength = nDepth; pPointer.nScore = pNewAnalysisState.nScore; } } - else if (!pNewAnalysisState.vCaptureMetrics.empty()) + else if (nDepth < MAX_DEPTH) { + // not a direct reference. check to see if there's a valid chain pointing at the current pointers const auto nRootAddress = vIterators.front()->nRootAddress; - if (!std::binary_search(pAnalysisState.vDeadEndAddresses.begin(), pAnalysisState.vDeadEndAddresses.end(), nRootAddress)) + + auto& vUnreachableAddresses = pAnalysisState.pProgress->vUnreachableAddresses; + if (std::binary_search(vUnreachableAddresses.begin(), vUnreachableAddresses.end(), nRootAddress)) + { + // this pointer was previously examined and did not share an offset with any other pointers + // so it cannot be a valid link in the route. + } + else { - gsl::index nIndex = 0; - for (auto& pCaptureMetric : pNewAnalysisState.vCaptureMetrics) + auto& vDeadEndAddresses = pAnalysisState.pProgress->vDeadEndAddresses.at(nDepth); + if (std::binary_search(vDeadEndAddresses.begin(), vDeadEndAddresses.end(), nRootAddress)) { - pCaptureMetric.vPotentialPointers.clear(); - InitializePotentialPointers(pCaptureMetric.vPotentialPointers, *pCaptureMetric.pCapture, vIterators.at(nIndex)->nRootAddress); - ++nIndex; + // this pointer was previously exhaustively examined at this depth + // and no routes were found so it is not a valid link in the route. } + else + { + // find any pointers pointing to within MAX_OFFSET bytes of the target address + gsl::index nIndex = 0; + for (auto& pCaptureMetric : pNewAnalysisState.vCaptureMetrics) + { + pCaptureMetric.vPotentialPointers.clear(); + InitializePotentialPointers(pCaptureMetric.vPotentialPointers, *pCaptureMetric.pCapture, vIterators.at(nIndex)->nRootAddress); + ++nIndex; + } - RemoveUnsharedOffsets(pNewAnalysisState.vCaptureMetrics); - - const auto nFoundPointers = pAnalysisState.vResults->size(); - - pNewAnalysisState.nProgress = pAnalysisState.nProgress; - GetPointers(vIndirectNodes, pNewAnalysisState); - pAnalysisState.nProgress = pNewAnalysisState.nProgress; + // narrow the list down to offsets that occur in all captures. + RemoveUnsharedOffsets(pNewAnalysisState.vCaptureMetrics); - if (pAnalysisState.vResults->size() == nFoundPointers) - { - const auto pInsertAt = std::lower_bound(pAnalysisState.vDeadEndAddresses.begin(), pAnalysisState.vDeadEndAddresses.end(), nRootAddress); - if (pInsertAt == pAnalysisState.vDeadEndAddresses.end() || *pInsertAt != nRootAddress) - pAnalysisState.vDeadEndAddresses.insert(pInsertAt, nRootAddress); + if (pNewAnalysisState.vCaptureMetrics.front().vPotentialPointers.empty()) + { + // if all offsets were unique, this address cannot be reached from any + // captured pointers. mark it as unreachable so we don't try again in the future + const auto pInsertAt = std::lower_bound(vUnreachableAddresses.begin(), vUnreachableAddresses.end(), nRootAddress); + if (pInsertAt == vUnreachableAddresses.end() || *pInsertAt != nRootAddress) + vUnreachableAddresses.insert(pInsertAt, nRootAddress); + } + else + { + const auto nFoundPointers = pAnalysisState.pProgress->vResults->size(); + + AnalyzeState(pNewAnalysisState); + + if (pAnalysisState.pProgress->vResults->size() == nFoundPointers) + { + // no path to the target address was found, mark it as a dead end + // at this depth so we don't try to process it again. + const auto pInsertAt = std::lower_bound(vDeadEndAddresses.begin(), vDeadEndAddresses.end(), nRootAddress); + if (pInsertAt == vDeadEndAddresses.end() || *pInsertAt != nRootAddress) + vDeadEndAddresses.insert(pInsertAt, nRootAddress); + } + } } } - else - { - static int nUnused = 0; - nUnused++; - } } + if (pAnalysisState.pProgress->bAborted) + break; + // advance to the next combination gsl::index nIndex = 0; do diff --git a/src/services/PointerFinder.hh b/src/services/PointerFinder.hh index 7a11cb1c..046c37e9 100644 --- a/src/services/PointerFinder.hh +++ b/src/services/PointerFinder.hh @@ -56,7 +56,7 @@ public: void AddCapture(const Capture& pCapture, ra::data::ByteAddress nTargetAddress); - static constexpr uint32_t MAX_DEPTH = 4; + static constexpr uint32_t MAX_DEPTH = 16; typedef struct PotentialPointer { @@ -66,7 +66,7 @@ public: std::array vOffsets{}; } PotentialPointer; - void Analyze(std::vector& vResults, std::function fProgress); + void Analyze(std::vector& vResults, std::function fProgress); private: typedef struct Node @@ -85,28 +85,43 @@ private: const Capture* pCapture = nullptr; } CaptureMetrics; - typedef struct AnalysisState + typedef struct AnalysisProgress { - std::vector vCaptureMetrics; - uint32_t nScore = 0; - uint32_t nOffsetLength = 0; - std::array vOffsets{}; - - std::vector vDeadEndAddresses; size_t nProgress = 0; size_t nRootProgressSize = 0; size_t nMaxProgress = 0; - std::function fProgress; + size_t nChecksSinceLastProgressUpdated = 0; + bool bAborted = false; + std::function fProgress; + std::vector* vResults = nullptr; + + std::array vOffsets{}; + + // addresses that cannot be reached by a common offset from any pointers across captures. + std::vector vUnreachableAddresses; + + // addresses that could not be found within MAX_DEPTH traversals. + // index of outer array is the depth at which the address could not be resolved. + std::array, MAX_DEPTH> vDeadEndAddresses; + } AnalysisProgress; + + typedef struct AnalysisState + { + std::vector vCaptureMetrics; + uint32_t nScore = 0; + uint32_t nDepth = 0; + + AnalysisProgress* pProgress = nullptr; } AnalysisState; - void GetPointers(std::vector& vIndirectNodes, std::function fProgress) const; - static void GetPointers(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); + void GetPointers(std::vector& vIndirectNodes, std::function fProgress) const; + static void AnalyzeState(AnalysisState& pAnalysisState); static void SortPointers(std::vector& vPointers); static void InitializePotentialPointers(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddres); static void RemoveUnsharedOffsets(std::vector& vCaptureMetrics); static bool FindSharedOffset(std::vector& vCaptureMetrics) noexcept; - static void ProcessSharedOffset(std::vector& vIndirectNodes, AnalysisState& pAnalysisState); + static void ProcessSharedOffset(AnalysisState& pAnalysisState); static bool PointersMatch(std::vector::const_iterator>& vIterators) noexcept; std::vector> m_vCaptures; diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index ed2343b0..4f233ba7 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -194,11 +194,20 @@ void PointerFinderViewModel::Find() if (GetValue(ShowProgressDialogProperty)) { ProgressViewModel vmProgress; - vmProgress.SetMessage(ra::util::String::Printf(L"Analyzing %u potential pointers", gsl::narrow_cast(nPointerCount))); - vmProgress.QueueTask([&pPointerFinder, &vResults, &vmProgress]() { - pPointerFinder.Analyze(vResults, [&vmProgress](size_t nProgress, size_t nTotal) + std::wstring sMessage = ra::util::String::Printf(L"Analyzing %u potential pointers", gsl::narrow_cast(nPointerCount)); + vmProgress.SetMessage(sMessage); + vmProgress.QueueTask([&pPointerFinder, &vResults, &vmProgress, &sMessage]() { + uint32_t nCount = 0; + pPointerFinder.Analyze(vResults, [&vmProgress, &sMessage, &nCount](size_t nProgress, size_t nTotal) { + if (vmProgress.GetDialogResult() != DialogResult::None) + return false; + + static const std::array sProgressTicker = { L"", L".", L"..", L"...", L" ..", L" ." }; + nCount = (nCount + 1) % sProgressTicker.size(); + vmProgress.SetMessage(sMessage + sProgressTicker.at(nCount)); vmProgress.SetProgress(gsl::narrow_cast(nProgress * 100 / nTotal)); + return true; }); }); vmProgress.ShowModal(); From 75bed8ba22a77f62adbfaab84267ef46e75fc6eb Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 19 Aug 2026 21:19:46 -0600 Subject: [PATCH 8/9] update BookmarkSelected --- src/ui/viewmodels/PointerFinderViewModel.cpp | 179 +++++++++++++----- src/ui/viewmodels/PointerFinderViewModel.hh | 2 + .../PointerFinderViewModel_Tests.cpp | 80 +++++--- 3 files changed, 187 insertions(+), 74 deletions(-) diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 4f233ba7..543394f8 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -218,54 +218,8 @@ void PointerFinderViewModel::Find() } const auto nSize = GetSearchSize(); - const auto& pMemoryContext = ra::services::ServiceLocator::Get(); for (const auto& pPotentialPointer : vResults) - { - auto& pPointer = m_vResults.Add(); - pPointer.m_nAddress = pPotentialPointer.nRootAddress; - pPointer.SetPointerAddress(pMemoryContext.FormatAddress(pPointer.m_nAddress)); - - auto nOffset = pPotentialPointer.vOffsets.front(); - pPointer.m_nOffset = nOffset; - pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); - - std::array vPointerAddress = {}; - for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) - { - const auto& pState = m_vStates.at(nStateIndex); - if (!pState.CanCapture()) - { - const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(pPointer.m_nAddress); - if (pValue) - { - pPointer.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); - vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; - } - } - } - - for (gsl::index nOffsetIndex = 1; nOffsetIndex < gsl::narrow_cast(pPotentialPointer.nOffsetLength); ++nOffsetIndex) - { - auto& pOffset = m_vResults.Add(); - nOffset = pPotentialPointer.vOffsets.at(nOffsetIndex); - pOffset.m_nOffset = nOffset; - pOffset.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); - - for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) - { - auto nPointerAddress = vPointerAddress.at(nStateIndex); - if (nPointerAddress) - { - const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(nPointerAddress); - if (pValue) - { - pOffset.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); - vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; - } - } - } - } - } + AddPotentialPointer(pPotentialPointer, nSize); if (m_vResults.Count() == 0 && nUniqueAddresses >= 2) { @@ -284,6 +238,57 @@ void PointerFinderViewModel::Find() ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Cannot find.", L"At least two unique addresses must be captured before potential pointers can be located."); } +void PointerFinderViewModel::AddPotentialPointer(const ra::services::PointerFinder::PotentialPointer& pPotentialPointer, ra::data::Memory::Size nSize) +{ + const auto& pMemoryContext = ra::services::ServiceLocator::Get(); + + auto& pPointer = m_vResults.Add(); + pPointer.m_nAddress = pPotentialPointer.nRootAddress; + pPointer.SetPointerAddress(pMemoryContext.FormatAddress(pPointer.m_nAddress)); + + auto nOffset = pPotentialPointer.vOffsets.front(); + pPointer.m_nOffset = nOffset; + pPointer.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); + + std::array vPointerAddress = {}; + for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) + { + const auto& pState = m_vStates.at(nStateIndex); + if (!pState.CanCapture()) + { + const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(pPointer.m_nAddress); + if (pValue) + { + pPointer.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); + vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; + } + } + } + + for (gsl::index nOffsetIndex = 1; nOffsetIndex < gsl::narrow_cast(pPotentialPointer.nOffsetLength); ++nOffsetIndex) + { + auto& pOffset = m_vResults.Add(); + nOffset = pPotentialPointer.vOffsets.at(nOffsetIndex); + pOffset.m_bIsChild = true; + pOffset.m_nOffset = nOffset; + pOffset.SetOffset(ra::util::String::Printf(L"+0x%02X", nOffset)); + + for (gsl::index nStateIndex = 0; nStateIndex < NUM_STATES; ++nStateIndex) + { + auto nPointerAddress = vPointerAddress.at(nStateIndex); + if (nPointerAddress) + { + const auto* pValue = m_vStates.at(nStateIndex).CapturedMemory().GetValue(nPointerAddress); + if (pValue) + { + pOffset.SetPointerValue(nStateIndex, ra::data::Memory::FormatValue(pValue->nValue, nSize, ra::data::Memory::Format::Hex)); + vPointerAddress.at(nStateIndex) = pValue->nValueAsAddress + nOffset; + } + } + } + } +} + ra::data::Memory::Size PointerFinderViewModel::GetSearchSize() const { auto nSize = ra::data::Memory::Size::ThirtyTwoBit; @@ -305,14 +310,86 @@ void PointerFinderViewModel::BookmarkSelected() if (!vmBookmarks.IsVisible()) vmBookmarks.Show(); - for (const auto& pItem : m_vResults) + std::vector vSelectedItems; + gsl::index nStartIndex = 0; + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vResults.Count()); ++nIndex) { - if (pItem.IsSelected()) + const auto* pItem = m_vResults.GetItemAt(nIndex); + Expects(pItem != nullptr); + + if (!pItem->m_bIsChild) + nStartIndex = nIndex; + + if (pItem->IsSelected()) { - vmBookmarks.AddBookmark(pItem.m_nAddress, GetSearchSize()); - break; + if (vSelectedItems.empty() || vSelectedItems.back() != nStartIndex) + vSelectedItems.push_back(nStartIndex); } } + + if (vSelectedItems.empty()) + return; + + auto nSize = ra::data::Memory::Size::ThirtyTwoBit; + uint32_t nMask = 0xFFFFFFFF; + uint32_t nOffset = 0; + + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + if (!pConsoleContext.GetRealAddressConversion(&nSize, &nMask, &nOffset)) + { + nSize = GetSearchSize(); + nMask = 0xFFFFFFFF; + nOffset = pConsoleContext.RealAddressFromByteAddress(0); + if (nOffset == 0xFFFFFFFF) + nOffset = 0; + } + else if (nMask != 0xFFFFFFFF) + { + const auto nBitsMask = ra::to_unsigned((1 << ra::data::Memory::SizeBits(nSize)) - 1); + if (nBitsMask == nMask) + nMask = 0xFFFFFFFF; // indicate masking is not needed + } + + for (auto nIndex : vSelectedItems) + { + const auto* pItem = m_vResults.GetItemAt(nIndex); + Expects(pItem != nullptr); + + uint32_t nAddress = gsl::narrow_cast(pItem->m_nAddress); + std::string sBuffer; + do + { + ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::AddAddress); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, nSize, nAddress); + + if (nOffset != 0) + { + ra::services::AchievementLogicSerializer::AppendOperator(sBuffer, ra::services::TriggerOperatorType::Subtract); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Value, ra::data::Memory::Size::ThirtyTwoBit, nOffset); + } + else if (nMask != 0xFFFFFFFF) + { + ra::services::AchievementLogicSerializer::AppendOperator(sBuffer, ra::services::TriggerOperatorType::BitwiseAnd); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Value, ra::data::Memory::Size::ThirtyTwoBit, nMask); + } + + ra::services::AchievementLogicSerializer::AppendConditionSeparator(sBuffer); + + const PotentialPointerViewModel* pNextItem = m_vResults.GetItemAt(++nIndex); + if (pNextItem == nullptr || !pNextItem->m_bIsChild) + { + ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::Measured); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, + nSize, ra::to_unsigned(pItem->m_nOffset)); + break; + } + + nAddress = pItem->m_nOffset; + pItem = pNextItem; + } while (true); + + vmBookmarks.AddBookmark(sBuffer); + } } void PointerFinderViewModel::ExportResults() const diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 41695201..9f2f6998 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.hh +++ b/src/ui/viewmodels/PointerFinderViewModel.hh @@ -269,6 +269,7 @@ public: friend class PointerFinderViewModel; ra::data::ByteAddress m_nAddress = 0; int32_t m_nOffset = 0; + bool m_bIsChild = false; bool m_bMatched = false; }; @@ -285,6 +286,7 @@ public: protected: static const BoolModelProperty ShowProgressDialogProperty; + void AddPotentialPointer(const ra::services::PointerFinder::PotentialPointer& pPotentialPointer, ra::data::Memory::Size nSize); void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; ra::data::Memory::Size GetSearchSize() const; diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index 0897d732..3b6fea54 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -9,6 +9,7 @@ #include "tests\devkit\context\mocks\MockConsoleContext.hh" #include "tests\devkit\context\mocks\MockEmulatorMemoryContext.hh" +#include "tests\devkit\context\mocks\MockRcClient.hh" #include "tests\devkit\services\mocks\MockFileSystem.hh" #include "tests\mocks\MockConfiguration.hh" #include "tests\mocks\MockGameContext.hh" @@ -72,6 +73,8 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::AreEqual(sPointerValue3, pPointer->GetPointerValue3()); Assert::AreEqual(sPointerValue4, pPointer->GetPointerValue4()); } + + using PointerFinderViewModel::AddPotentialPointer; }; public: @@ -489,11 +492,14 @@ TEST_CLASS(PointerFinderViewModel_Tests) vmPointerFinder.AssertRow(0, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 vmPointerFinder.AssertRow(1, L"", L"+0x0C", L"0044", L"0068", L"", L""); // 44+0C=>50, 68+0C=>74 Assert::AreEqual(std::wstring(L"1"), vmPointerFinder.GetResultCountText()); + + } TEST_METHOD(TestBookmarkSelected) { PointerFinderViewModelHarness vmPointerFinder; + ra::context::mocks::MockRcClient mockRcClient; vmPointerFinder.mockGameContext.SetGameId(1U); vmPointerFinder.SetSearchType(ra::services::SearchType::SixteenBitAligned); @@ -506,28 +512,28 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::AreEqual({ 0U }, pBookmarks.Items().Count()); // initialize results - std::array pMemory{}; - pMemory.at(0x08) = 0x1c; - pMemory.at(0x70) = 0x1c; - pMemory.at(0x9c) = 0x20; - vmPointerFinder.MockMemory(pMemory); - - vmPointerFinder.States().at(0).SetAddress(L"0x20"); - vmPointerFinder.States().at(0).ToggleCapture(); - - pMemory.at(0x08) = 0x34; - pMemory.at(0x70) = 0x34; - pMemory.at(0x9c) = 0x38; - - vmPointerFinder.States().at(1).SetAddress(L"0x38"); - vmPointerFinder.States().at(1).ToggleCapture(); - vmPointerFinder.Find(); - - Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); - Assert::AreEqual({ 3U }, vmPointerFinder.PotentialPointers().Count()); - vmPointerFinder.AssertRow(0, L"0x009c", L"+0x00", L"0020", L"0038", L"", L""); // 20+00=>20, 38+00=>38 - vmPointerFinder.AssertRow(1, L"0x0008", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 - vmPointerFinder.AssertRow(2, L"0x0070", L"+0x04", L"001c", L"0034", L"", L""); // 1c+04=>20, 34+04=>38 + ra::services::PointerFinder::PotentialPointer pPointer; + pPointer.nRootAddress = 0x009c; + pPointer.vOffsets.at(0) = 0; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0008; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0020; + pPointer.vOffsets.at(0) = 8; + pPointer.vOffsets.at(1) = 0; + pPointer.vOffsets.at(2) = 4; + pPointer.nOffsetLength = 3; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0070; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); // no selection vmPointerFinder.BookmarkSelected(); @@ -537,7 +543,35 @@ TEST_CLASS(PointerFinderViewModel_Tests) vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); vmPointerFinder.BookmarkSelected(); Assert::AreEqual({ 1U }, pBookmarks.Items().Count()); - Assert::AreEqual({ 0x0008U }, pBookmarks.Items().GetItemAt(0)->GetAddress()); + Assert::AreEqual(std::string("I:0x 0008_M:0x 0004"), pBookmarks.Items().GetItemAt(0)->GetIndirectAddress()); + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(false); + + // first part of chain + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(true); + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 2U }, pBookmarks.Items().Count()); + Assert::AreEqual(std::string("I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), pBookmarks.Items().GetItemAt(1)->GetIndirectAddress()); + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(false); + + // middle part of chain + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(true); + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 3U }, pBookmarks.Items().Count()); + Assert::AreEqual(std::string("I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), pBookmarks.Items().GetItemAt(2)->GetIndirectAddress()); + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(false); + + // end part of chain + vmPointerFinder.PotentialPointers().GetItemAt(4)->SetSelected(true); + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 4U }, pBookmarks.Items().Count()); + Assert::AreEqual(std::string("I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), pBookmarks.Items().GetItemAt(3)->GetIndirectAddress()); + + // entire chain + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(false); + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(false); + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 5U }, pBookmarks.Items().Count()); + Assert::AreEqual(std::string("I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), pBookmarks.Items().GetItemAt(4)->GetIndirectAddress()); } TEST_METHOD(TestExportResults) From 65df428bead4f9244594c756fa572284301a6b21 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 20 Aug 2026 08:07:37 -0600 Subject: [PATCH 9/9] add Copy Chain button --- src/RA_Shared.rc | 2 +- src/ui/viewmodels/PointerFinderViewModel.cpp | 57 ++++++-- src/ui/viewmodels/PointerFinderViewModel.hh | 10 +- src/ui/win32/PointerFinderDialog.cpp | 12 +- .../PointerFinderViewModel_Tests.cpp | 138 ++++++++++++++++++ 5 files changed, 204 insertions(+), 15 deletions(-) diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc index ff4fefc4..d26a8ad0 100644 --- a/src/RA_Shared.rc +++ b/src/RA_Shared.rc @@ -387,7 +387,7 @@ BEGIN COMBOBOX IDC_RA_SEARCHTYPE,7,19,71,13,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP PUSHBUTTON "&Find",IDC_RA_RESET_FILTER,6,33,72,13 PUSHBUTTON "Book&mark Selected",IDC_RA_RESULTS_BOOKMARK,6,65,72,13 - PUSHBUTTON "E&xport",IDC_RA_RESULTS_EXPORT,6,78,72,13 + PUSHBUTTON "&Copy Chain",IDC_RA_COPY_ALL,6,78,72,13 CONTROL "",IDC_RA_RESULTS,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | WS_BORDER | WS_VSCROLL | WS_TABSTOP,80,9,312,81 GROUPBOX "State 1",IDC_RA_GBX_STATE_1,4,94,392,59 LTEXT "Address:",IDC_RA_LBL_ADDRESS_1,8,103,32,9 diff --git a/src/ui/viewmodels/PointerFinderViewModel.cpp b/src/ui/viewmodels/PointerFinderViewModel.cpp index 543394f8..4c80cc88 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -8,6 +8,7 @@ #include "data/context/EmulatorContext.hh" #include "data/context/GameContext.hh" +#include "services/IClipboard.hh" #include "services/IFileSystem.hh" #include "services/ServiceLocator.hh" @@ -204,7 +205,7 @@ void PointerFinderViewModel::Find() return false; static const std::array sProgressTicker = { L"", L".", L"..", L"...", L" ..", L" ." }; - nCount = (nCount + 1) % sProgressTicker.size(); + nCount = (nCount + 1) % gsl::narrow_cast(sProgressTicker.size()); vmProgress.SetMessage(sMessage + sProgressTicker.at(nCount)); vmProgress.SetProgress(gsl::narrow_cast(nProgress * 100 / nTotal)); return true; @@ -304,13 +305,47 @@ ra::data::Memory::Size PointerFinderViewModel::GetSearchSize() const return nSize; } -void PointerFinderViewModel::BookmarkSelected() +void PointerFinderViewModel::BookmarkSelected() const { auto& vmBookmarks = ra::services::ServiceLocator::GetMutable().MemoryBookmarks; if (!vmBookmarks.IsVisible()) vmBookmarks.Show(); std::vector vSelectedItems; + GetSelectedItems(vSelectedItems); + if (vSelectedItems.empty()) + return; + + ConvertResultsToAchievementLogic(vSelectedItems, + [&vmBookmarks](const std::string& sSerialized) + { + vmBookmarks.AddBookmark(sSerialized); + }); +} + +void PointerFinderViewModel::CopySelectedToClipboard() const +{ + std::vector vSelectedItems; + GetSelectedItems(vSelectedItems); + if (vSelectedItems.empty()) + return; + + if (vSelectedItems.size() > 1) + { + MessageBoxViewModel::ShowErrorMessage(L"Multiple items selected", L"Only one item can be copied at a time."); + return; + } + + ConvertResultsToAchievementLogic(vSelectedItems, + [](const std::string& sSerialized) + { + auto& pClipboard = ra::services::ServiceLocator::Get(); + pClipboard.SetText(ra::util::String::Widen(sSerialized)); + }); +} + +void PointerFinderViewModel::GetSelectedItems(std::vector& nIndices) const +{ gsl::index nStartIndex = 0; for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vResults.Count()); ++nIndex) { @@ -322,14 +357,14 @@ void PointerFinderViewModel::BookmarkSelected() if (pItem->IsSelected()) { - if (vSelectedItems.empty() || vSelectedItems.back() != nStartIndex) - vSelectedItems.push_back(nStartIndex); + if (nIndices.empty() || nIndices.back() != nStartIndex) + nIndices.push_back(nStartIndex); } } +} - if (vSelectedItems.empty()) - return; - +void PointerFinderViewModel::ConvertResultsToAchievementLogic(const std::vector& nIndices, std::function fCallback) const +{ auto nSize = ra::data::Memory::Size::ThirtyTwoBit; uint32_t nMask = 0xFFFFFFFF; uint32_t nOffset = 0; @@ -350,7 +385,7 @@ void PointerFinderViewModel::BookmarkSelected() nMask = 0xFFFFFFFF; // indicate masking is not needed } - for (auto nIndex : vSelectedItems) + for (auto nIndex : nIndices) { const auto* pItem = m_vResults.GetItemAt(nIndex); Expects(pItem != nullptr); @@ -360,7 +395,7 @@ void PointerFinderViewModel::BookmarkSelected() do { ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::AddAddress); - ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, nSize, nAddress); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, nSize, nAddress); if (nOffset != 0) { @@ -380,7 +415,7 @@ void PointerFinderViewModel::BookmarkSelected() { ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::Measured); ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, - nSize, ra::to_unsigned(pItem->m_nOffset)); + nSize, ra::to_unsigned(pItem->m_nOffset)); break; } @@ -388,7 +423,7 @@ void PointerFinderViewModel::BookmarkSelected() pItem = pNextItem; } while (true); - vmBookmarks.AddBookmark(sBuffer); + fCallback(sBuffer); } } diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 9f2f6998..62a789fc 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.hh +++ b/src/ui/viewmodels/PointerFinderViewModel.hh @@ -66,7 +66,12 @@ public: /// /// Bookmarks the currently item from the search results. /// - void BookmarkSelected(); + void BookmarkSelected() const; + + /// + /// Copies the currently item from the search results as achievemnet logic. + /// + void CopySelectedToClipboard() const; class StateViewModel : public ViewModelBase, protected ra::data::context::EmulatorContext::DispatchesReadMemory @@ -289,6 +294,9 @@ protected: void AddPotentialPointer(const ra::services::PointerFinder::PotentialPointer& pPotentialPointer, ra::data::Memory::Size nSize); void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; + void GetSelectedItems(std::vector& nIndices) const; + void ConvertResultsToAchievementLogic(const std::vector& nIndices, std::function fCallback) const; + ra::data::Memory::Size GetSearchSize() const; private: diff --git a/src/ui/win32/PointerFinderDialog.cpp b/src/ui/win32/PointerFinderDialog.cpp index 3e788dd2..a7b7a36d 100644 --- a/src/ui/win32/PointerFinderDialog.cpp +++ b/src/ui/win32/PointerFinderDialog.cpp @@ -164,7 +164,7 @@ PointerFinderDialog::PointerFinderDialog(PointerFinderViewModel& vmPointerFinder SetAnchor(IDC_RA_RESET_FILTER, Anchor::Top | Anchor::Left); SetAnchor(IDC_RA_APPLY_FILTER, Anchor::Top | Anchor::Left); SetAnchor(IDC_RA_RESULTS_BOOKMARK, Anchor::Left | Anchor::Bottom); - SetAnchor(IDC_RA_RESULTS_EXPORT, Anchor::Left | Anchor::Bottom); + SetAnchor(IDC_RA_COPY_ALL, Anchor::Left | Anchor::Bottom); SetAnchor(IDC_RA_RESULTS, Anchor::Top | Anchor::Left | Anchor::Bottom | Anchor::Right); SetAnchor(IDC_RA_GBX_STATE_1, Anchor::Left | Anchor::Bottom | Anchor::Right); @@ -226,13 +226,21 @@ BOOL PointerFinderDialog::OnCommand(WORD nCommand) } case IDC_RA_RESULTS_BOOKMARK: { - auto* vmPointerFinder = dynamic_cast(&m_vmWindow); + const auto* vmPointerFinder = dynamic_cast(&m_vmWindow); if (vmPointerFinder) vmPointerFinder->BookmarkSelected(); return TRUE; } + case IDC_RA_COPY_ALL: { + const auto* vmPointerFinder = dynamic_cast(&m_vmWindow); + if (vmPointerFinder) + vmPointerFinder->CopySelectedToClipboard(); + + return TRUE; + } + case IDC_RA_RESULTS_EXPORT: { const auto* vmPointerFinder = dynamic_cast(&m_vmWindow); if (vmPointerFinder) diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index 3b6fea54..fd3d1e81 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -11,6 +11,8 @@ #include "tests\devkit\context\mocks\MockEmulatorMemoryContext.hh" #include "tests\devkit\context\mocks\MockRcClient.hh" #include "tests\devkit\services\mocks\MockFileSystem.hh" + +#include "tests\mocks\MockClipboard.hh" #include "tests\mocks\MockConfiguration.hh" #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockDesktop.hh" @@ -574,6 +576,142 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::AreEqual(std::string("I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), pBookmarks.Items().GetItemAt(4)->GetIndirectAddress()); } + TEST_METHOD(TestCopySelectedToClipboard) + { + PointerFinderViewModelHarness vmPointerFinder; + ra::services::mocks::MockClipboard mockClipboard; + vmPointerFinder.mockGameContext.SetGameId(1U); + vmPointerFinder.SetSearchType(ra::services::SearchType::SixteenBitAligned); + + // no results - nothing copied, no dialog shown + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + + // initialize results + ra::services::PointerFinder::PotentialPointer pPointer; + pPointer.nRootAddress = 0x009c; + pPointer.vOffsets.at(0) = 0; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0008; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0020; + pPointer.vOffsets.at(0) = 8; + pPointer.vOffsets.at(1) = 0; + pPointer.vOffsets.at(2) = 4; + pPointer.nOffsetLength = 3; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0070; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + // no selection + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + + // selection + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L"I:0x 0008_M:0x 0004"), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(false); + + // first part of chain + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(true); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L"I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(false); + mockClipboard.SetText(L""); + + // middle part of chain + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(true); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L"I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(false); + mockClipboard.SetText(L""); + + // end part of chain + vmPointerFinder.PotentialPointers().GetItemAt(4)->SetSelected(true); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L"I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + mockClipboard.SetText(L""); + + // entire chain + vmPointerFinder.PotentialPointers().GetItemAt(3)->SetSelected(false); + vmPointerFinder.PotentialPointers().GetItemAt(2)->SetSelected(false); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L"I:0x 0020_I:0x 0008_I:0x 0000_M:0x 0004"), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); + } + + TEST_METHOD(TestCopySelectedToClipboardMultipleSelections) + { + PointerFinderViewModelHarness vmPointerFinder; + ra::services::mocks::MockClipboard mockClipboard; + vmPointerFinder.mockGameContext.SetGameId(1U); + vmPointerFinder.SetSearchType(ra::services::SearchType::SixteenBitAligned); + + // initialize results + ra::services::PointerFinder::PotentialPointer pPointer; + pPointer.nRootAddress = 0x009c; + pPointer.vOffsets.at(0) = 0; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0008; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0020; + pPointer.vOffsets.at(0) = 8; + pPointer.vOffsets.at(1) = 0; + pPointer.vOffsets.at(2) = 4; + pPointer.nOffsetLength = 3; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + pPointer.nRootAddress = 0x0070; + pPointer.vOffsets.at(0) = 4; + pPointer.nOffsetLength = 1; + vmPointerFinder.AddPotentialPointer(pPointer, ra::data::Memory::Size::SixteenBit); + + // selection + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); + vmPointerFinder.PotentialPointers().GetItemAt(4)->SetSelected(true); + + bool bDialogSeen = false; + vmPointerFinder.mockDesktop.ExpectWindow( + [&bDialogSeen](const ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { + Assert::AreEqual(std::wstring(L"Multiple items selected"), vmMessageBox.GetHeader()); + Assert::AreEqual(std::wstring(L"Only one item can be copied at a time."), vmMessageBox.GetMessage()); + + bDialogSeen = true; + return ra::ui::DialogResult::OK; + } + ); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(L""), mockClipboard.GetText()); + Assert::IsTrue(bDialogSeen); + } + TEST_METHOD(TestExportResults) { PointerFinderViewModelHarness vmPointerFinder;