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/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/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/PointerFinder.cpp b/src/services/PointerFinder.cpp new file mode 100644 index 00000000..18f830d9 --- /dev/null +++ b/src/services/PointerFinder.cpp @@ -0,0 +1,489 @@ +#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" + +/* 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 = 8192; + +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, pResult.nValue, nPointerAddress); + } + + return true; + }); +} + +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 : 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 +{ + 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, std::function fProgress) +{ + if (m_vCaptures.empty()) + return; + + GetPointers(vResults, fProgress); + + SortPointers(vResults); +} + +void PointerFinder::SortPointers(std::vector& vPointers) +{ + std::sort(vPointers.begin(), vPointers.end(), + [](const PotentialPointer& a, const PotentialPointer& b) + { + if (a.nScore == b.nScore) + return a.nRootAddress < b.nRootAddress; + + return a.nScore < b.nScore; + }); +} + +static uint32_t CalculateScore(int32_t nOffset) noexcept +{ + // treat negative offsets as being farther away + return ra::to_unsigned((nOffset < 0) ? (-nOffset * 8) : nOffset); +} + +void PointerFinder::InitializePotentialPointers(std::vector& vPointers, const Capture& pCapture, ra::data::ByteAddress nTargetAddress) +{ + vPointers.reserve(pCapture.m_vPointerValues.size()); + + for (const auto& pValue : pCapture.m_vPointerValues) + { + const int32_t nOffset = ra::to_signed(nTargetAddress) - ra::to_signed(pValue.nValueAsAddress); + if (nOffset < MAX_OFFSET && nOffset > -MAX_OFFSET) + { + auto& pPointer = vPointers.emplace_back(); + pPointer.nRootAddress = pValue.nAddress; + + pPointer.vOffsets.at(pPointer.nOffsetLength++) = nOffset; + pPointer.nScore = CalculateScore(nOffset); + } + } + + std::sort(vPointers.begin(), vPointers.end(), + [](const PotentialPointer& a, const PotentialPointer& b) + { + return a.vOffsets.front() < b.vOffsets.front(); + }); +} + +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 +{ + AnalysisProgress pProgress; + pProgress.fProgress = fProgress; + pProgress.vResults = &vIndirectNodes; + + AnalysisState pAnalysisState; + pAnalysisState.pProgress = &pProgress; + + for (const auto& pCapture : m_vCaptures) + { + auto& pCaptureMetrics = pAnalysisState.vCaptureMetrics.emplace_back(); + pCaptureMetrics.pCapture = &pCapture.first; + InitializePotentialPointers(pCaptureMetrics.vPotentialPointers, pCapture.first, pCapture.second); + } + + RemoveUnsharedOffsets(pAnalysisState.vCaptureMetrics); + + if (pProgress.fProgress) + { + constexpr size_t nMinProgress = 1; // std::max cannot deduce type of literal + pProgress.nRootProgressSize = pAnalysisState.vCaptureMetrics.front().vPotentialPointers.size(); + pProgress.nMaxProgress = std::max(nMinProgress, pProgress.nRootProgressSize * (pProgress.nRootProgressSize + 1)); + pProgress.fProgress(0, pProgress.nMaxProgress); + } + + AnalyzeState(pAnalysisState); +} + +void PointerFinder::AnalyzeState(AnalysisState& pAnalysisState) +{ + for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) + pCaptureMetrics.pIterator = pCaptureMetrics.vPotentialPointers.cbegin(); + + const size_t nProgressStart = pAnalysisState.pProgress->nProgress; + do + { + if (!FindSharedOffset(pAnalysisState.vCaptureMetrics)) + break; + + ProcessSharedOffset(pAnalysisState); + if (pAnalysisState.pProgress->bAborted) + return; + + for (auto& pCaptureMetrics : pAnalysisState.vCaptureMetrics) + pCaptureMetrics.pIterator = pCaptureMetrics.pStopIterator; + + if (pAnalysisState.pProgress->fProgress) + { + 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; + + if (!pAnalysisState.pProgress->fProgress(pAnalysisState.pProgress->nProgress, pAnalysisState.pProgress->nMaxProgress)) + { + pAnalysisState.pProgress->bAborted = true; + return; + } + } + } + } while (true); + + if (pAnalysisState.pProgress->fProgress && pAnalysisState.nDepth < 2) + { + if (pAnalysisState.nDepth == 1) + pAnalysisState.pProgress->nProgress = nProgressStart + pAnalysisState.pProgress->nRootProgressSize; + else + pAnalysisState.pProgress->nProgress++; + + if (!pAnalysisState.pProgress->fProgress(pAnalysisState.pProgress->nProgress, pAnalysisState.pProgress->nMaxProgress)) + pAnalysisState.pProgress->bAborted = true; + } +} + +bool PointerFinder::FindSharedOffset(std::vector& vCaptureMetrics) noexcept +{ + // 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; + + const auto nOffset = vCaptureMetrics.front().pIterator->vOffsets.front(); + + for (auto& pCaptureMetric : vCaptureMetrics) + { + auto pIter = pCaptureMetric.pIterator; + while (pIter < pCaptureMetric.vPotentialPointers.end() && pIter->vOffsets.front() == nOffset) + ++pIter; + pCaptureMetric.pStopIterator = pIter; + } + + return true; +} + +void PointerFinder::ProcessSharedOffset(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(); + 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); + + if (nDepth < MAX_DEPTH) + { + pNewAnalysisState.nDepth = nDepth; + pNewAnalysisState.pProgress = pAnalysisState.pProgress; + + 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)) + { + // found a route. check to see if another route exists + const auto nRootAddress = vIterators.front()->nRootAddress; + bool bFound = false; + for (auto& pPointer : *pAnalysisState.pProgress->vResults) + { + 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.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 = pAnalysisState.pProgress->vResults->emplace_back(); + pPointer.nRootAddress = nRootAddress; + memcpy(&pPointer.vOffsets.front(), nOffsetFront, nDepth * sizeof(pPointer.vOffsets[0])); + pPointer.nOffsetLength = nDepth; + pPointer.nScore = pNewAnalysisState.nScore; + } + } + 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; + + 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 + { + auto& vDeadEndAddresses = pAnalysisState.pProgress->vDeadEndAddresses.at(nDepth); + if (std::binary_search(vDeadEndAddresses.begin(), vDeadEndAddresses.end(), nRootAddress)) + { + // 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; + } + + // narrow the list down to offsets that occur in all captures. + RemoveUnsharedOffsets(pNewAnalysisState.vCaptureMetrics); + + 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); + } + } + } + } + } + + if (pAnalysisState.pProgress->bAborted) + break; + + // 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 new file mode 100644 index 00000000..046c37e9 --- /dev/null +++ b/src/services/PointerFinder.hh @@ -0,0 +1,133 @@ +#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; + + typedef struct PointerValue + { + 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: + class Capture + { + 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; + + private: + friend class PointerFinder; + PointerAddressRange NarrowSearch(ra::data::ByteAddress nTargetAddress) const; + + std::vector m_vPointerValues; + }; + + void AddCapture(const Capture& pCapture, ra::data::ByteAddress nTargetAddress); + + static constexpr uint32_t MAX_DEPTH = 16; + + typedef struct PotentialPointer + { + ra::data::ByteAddress nRootAddress = 0; + uint32_t nScore = 0; + uint32_t nOffsetLength = 0; + std::array vOffsets{}; + } PotentialPointer; + + void Analyze(std::vector& vResults, std::function fProgress); + +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; + + typedef struct CaptureMetrics + { + std::vector vPotentialPointers; + std::vector::const_iterator pIterator; + std::vector::const_iterator pStopIterator; + const Capture* pCapture = nullptr; + } CaptureMetrics; + + typedef struct AnalysisProgress + { + size_t nProgress = 0; + size_t nRootProgressSize = 0; + size_t nMaxProgress = 0; + 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 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(AnalysisState& pAnalysisState); + static bool PointersMatch(std::vector::const_iterator>& vIterators) noexcept; + + std::vector> m_vCaptures; +}; + +} // namespace services +} // namespace ra + +#endif // !RA_SERVICES_FRAME_EVENT_QUEUE_HH diff --git a/src/services/Search/SearchImpl.cpp b/src/services/Search/SearchImpl.cpp index 34ae0b91..671dd195 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(); @@ -165,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 cc25a226..123fd72e 100644 --- a/src/services/Search/SearchImpl.hh +++ b/src/services/Search/SearchImpl.hh @@ -68,10 +68,18 @@ 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; + // 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 e64fd7aa..eac33f49 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) @@ -368,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 ebae438f..b6541a17 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. /// @@ -117,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. /// @@ -125,7 +140,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..4c80cc88 100644 --- a/src/ui/viewmodels/PointerFinderViewModel.cpp +++ b/src/ui/viewmodels/PointerFinderViewModel.cpp @@ -3,14 +3,18 @@ #include "RA_Defs.h" #include "util\Strings.hh" +#include "context/IConsoleContext.hh" + #include "data/context/EmulatorContext.hh" #include "data/context/GameContext.hh" +#include "services/IClipboard.hh" #include "services/IFileSystem.hh" #include "services/ServiceLocator.hh" #include "ui/viewmodels/FileDialogViewModel.hh" #include "ui/viewmodels/MessageBoxViewModel.hh" +#include "ui/viewmodels/ProgressViewModel.hh" #include "ui/viewmodels/WindowManager.hh" namespace ra { @@ -19,6 +23,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"); @@ -32,6 +37,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()) @@ -94,8 +101,10 @@ void PointerFinderViewModel::StateViewModel::Capture() const auto& pMemoryContext = ra::services::ServiceLocator::Get(); const auto nMemorySize = gsl::narrow(pMemoryContext.TotalMemorySize()); - m_pCapture.reset(new ra::services::SearchResults()); - m_pCapture->Initialize(0, nMemorySize, m_pOwner->GetSearchType()); + ra::services::SearchResults pInitialResults; + pInitialResults.Initialize(0, nMemorySize, m_pOwner->GetSearchType()); + + m_pCapture.Initialize(pInitialResults); }); SetValue(CaptureButtonTextProperty, L"Release"); @@ -104,7 +113,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); @@ -152,118 +162,67 @@ 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() { - 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(); + 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++) { const auto& pStateI = m_vStates.at(i); if (pStateI.CanCapture()) continue; - for (size_t j = i + 1; j < m_vStates.size(); j++) + const auto nAddress = pStateI.Viewer().GetAddress(); + pPointerFinder.AddCapture(pStateI.CapturedMemory(), nAddress); + if (nAddress != nPreviousAddress) { - 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; - } - } + nPreviousAddress = nAddress; + ++nUniqueAddresses; + } - // 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); - } - } + nPointerCount += pStateI.CapturedMemory().GetCapturedPointerCount(); + } - bPerformedSearch = true; - } + std::vector vResults; + + if (GetValue(ShowProgressDialogProperty)) + { + ProgressViewModel vmProgress; + 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) % gsl::narrow_cast(sProgressTicker.size()); + vmProgress.SetMessage(sMessage + sProgressTicker.at(nCount)); + vmProgress.SetProgress(gsl::narrow_cast(nProgress * 100 / nTotal)); + return true; + }); + }); + vmProgress.ShowModal(); + } + else + { + pPointerFinder.Analyze(vResults, nullptr); } - if (m_vResults.Count() == 0 && bPerformedSearch) + const auto nSize = GetSearchSize(); + for (const auto& pPotentialPointer : vResults) + AddPotentialPointer(pPotentialPointer, nSize); + + if (m_vResults.Count() == 0 && nUniqueAddresses >= 2) { auto* pPointer = &m_vResults.Add(); pPointer->SetPointerAddress(L"No pointers found."); @@ -271,39 +230,200 @@ 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::BookmarkSelected() +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; + switch (GetSearchType()) + { + case ra::services::SearchType::SixteenBit: + case ra::services::SearchType::SixteenBitAligned: + case ra::services::SearchType::SixteenBitBigEndian: + nSize = ra::data::Memory::Size::SixteenBit; + break; + } + + return nSize; +} + +void PointerFinderViewModel::BookmarkSelected() const { auto& vmBookmarks = ra::services::ServiceLocator::GetMutable().MemoryBookmarks; if (!vmBookmarks.IsVisible()) vmBookmarks.Show(); - for (const auto& pItem : m_vResults) + 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) + { + const auto* pItem = m_vResults.GetItemAt(nIndex); + Expects(pItem != nullptr); + + if (!pItem->m_bIsChild) + nStartIndex = nIndex; + + if (pItem->IsSelected()) + { + if (nIndices.empty() || nIndices.back() != nStartIndex) + nIndices.push_back(nStartIndex); + } + } +} + +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; + + 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 : nIndices) { - if (pItem.IsSelected()) + const auto* pItem = m_vResults.GetItemAt(nIndex); + Expects(pItem != nullptr); + + uint32_t nAddress = gsl::narrow_cast(pItem->m_nAddress); + std::string sBuffer; + do { - auto nSize = ra::data::Memory::Size::ThirtyTwoBit; - switch (GetSearchType()) + ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::AddAddress); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, nSize, nAddress); + + if (nOffset != 0) { - case ra::services::SearchType::SixteenBit: - case ra::services::SearchType::SixteenBitAligned: - case ra::services::SearchType::SixteenBitBigEndian: - nSize = ra::data::Memory::Size::SixteenBit; - break; + 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); } - vmBookmarks.AddBookmark(pItem.m_nAddress, nSize); - break; - } + 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); + + fCallback(sBuffer); } } diff --git a/src/ui/viewmodels/PointerFinderViewModel.hh b/src/ui/viewmodels/PointerFinderViewModel.hh index 0a2158e6..62a789fc 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" @@ -65,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 @@ -108,7 +114,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 +132,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; } @@ -267,6 +273,8 @@ public: private: friend class PointerFinderViewModel; ra::data::ByteAddress m_nAddress = 0; + int32_t m_nOffset = 0; + bool m_bIsChild = false; bool m_bMatched = false; }; @@ -281,10 +289,19 @@ 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; + void GetSelectedItems(std::vector& nIndices) const; + void ConvertResultsToAchievementLogic(const std::vector& nIndices, std::function fCallback) const; + + ra::data::Memory::Size GetSearchSize() const; + private: - std::array m_vStates; + static constexpr size_t NUM_STATES = 4; + std::array m_vStates; ra::ui::ViewModelCollection m_vResults; LookupItemViewModelCollection m_vSearchTypes; diff --git a/src/ui/win32/PointerFinderDialog.cpp b/src/ui/win32/PointerFinderDialog.cpp index 2224b99e..a7b7a36d 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( @@ -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/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/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/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 diff --git a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp index 261e8b3a..fd3d1e81 100644 --- a/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp +++ b/tests/ui/viewmodels/PointerFinderViewModel_Tests.cpp @@ -7,8 +7,12 @@ #include "tests\RA_UnitTestHelpers.h" +#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\MockClipboard.hh" #include "tests\mocks\MockConfiguration.hh" #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockDesktop.hh" @@ -27,6 +31,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 +41,7 @@ TEST_CLASS(PointerFinderViewModel_Tests) GSL_SUPPRESS_F6 PointerFinderViewModelHarness() : PointerFinderViewModel() { - + SetValue(ShowProgressDialogProperty, false); } ~PointerFinderViewModelHarness() @@ -48,6 +53,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) @@ -62,6 +75,8 @@ TEST_CLASS(PointerFinderViewModel_Tests) Assert::AreEqual(sPointerValue3, pPointer->GetPointerValue3()); Assert::AreEqual(sPointerValue4, pPointer->GetPointerValue4()); } + + using PointerFinderViewModel::AddPotentialPointer; }; public: @@ -102,7 +117,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) { @@ -167,7 +182,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(); @@ -176,7 +190,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(); @@ -185,7 +198,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()); } } @@ -215,7 +227,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 +252,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 +284,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 +313,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 +342,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 +373,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(); @@ -378,9 +390,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()); } @@ -395,7 +407,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(); @@ -419,8 +431,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()); } @@ -432,7 +444,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(); @@ -449,9 +461,47 @@ 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; // $0008 = 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; // $0008 = 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; + ra::context::mocks::MockRcClient mockRcClient; vmPointerFinder.mockGameContext.SetGameId(1U); vmPointerFinder.SetSearchType(ra::services::SearchType::SixteenBitAligned); @@ -464,38 +514,202 @@ 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.mockEmulatorMemoryContext.MockMemory(pMemory); + 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); - vmPointerFinder.States().at(0).SetAddress(L"0x20"); - vmPointerFinder.States().at(0).ToggleCapture(); + // no selection + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 0U }, pBookmarks.Items().Count()); - pMemory.at(0x08) = 0x34; - pMemory.at(0x70) = 0x34; - pMemory.at(0x9c) = 0x38; + // selection + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); + vmPointerFinder.BookmarkSelected(); + Assert::AreEqual({ 1U }, pBookmarks.Items().Count()); + Assert::AreEqual(std::string("I:0x 0008_M:0x 0004"), pBookmarks.Items().GetItemAt(0)->GetIndirectAddress()); + vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(false); - vmPointerFinder.States().at(1).SetAddress(L"0x38"); - vmPointerFinder.States().at(1).ToggleCapture(); - vmPointerFinder.Find(); + // 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(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()); - 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.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.BookmarkSelected(); - Assert::AreEqual({ 0U }, pBookmarks.Items().Count()); + vmPointerFinder.CopySelectedToClipboard(); + Assert::AreEqual(std::wstring(), mockClipboard.GetText()); + Assert::IsFalse(vmPointerFinder.mockDesktop.WasDialogShown()); + vmPointerFinder.mockDesktop.ResetExpectedWindows(); // selection vmPointerFinder.PotentialPointers().GetItemAt(1)->SetSelected(true); - vmPointerFinder.BookmarkSelected(); - Assert::AreEqual({ 1U }, pBookmarks.Items().Count()); - Assert::AreEqual({ 0x0070U }, pBookmarks.Items().GetItemAt(0)->GetAddress()); + 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) @@ -509,7 +723,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(); @@ -524,9 +738,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( @@ -547,7 +761,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); } @@ -562,7 +776,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(); @@ -577,9 +791,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(