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(