diff --git a/src/Exports.cpp b/src/Exports.cpp index 3d93f7cb..74595a3e 100644 --- a/src/Exports.cpp +++ b/src/Exports.cpp @@ -514,8 +514,12 @@ static void UpdateUIForFrameChange() TALLY_PERFORMANCE(PerformanceCheckpoint::AssetEditorDoFrame); pWindowManager.AssetEditor.DoFrame(); + TALLY_PERFORMANCE(PerformanceCheckpoint::PointerFinderDoFrame); pWindowManager.PointerFinder.DoFrame(); + TALLY_PERFORMANCE(PerformanceCheckpoint::PointerInspectorDoFrame); + pWindowManager.PointerInspector.DoFrame(); + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); pFrameEventQueue.DoFrame(); } diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj index 5d083dfb..32d2431f 100644 --- a/src/RA_Integration.vcxproj +++ b/src/RA_Integration.vcxproj @@ -134,6 +134,7 @@ + @@ -169,6 +170,7 @@ + @@ -309,6 +311,7 @@ + @@ -327,6 +330,8 @@ + + @@ -356,6 +361,7 @@ + diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters index e1c34658..b853e24e 100644 --- a/src/RA_Integration.vcxproj.filters +++ b/src/RA_Integration.vcxproj.filters @@ -411,6 +411,12 @@ Data\Models + + UI\ViewModels + + + UI\Win32 + Services @@ -980,6 +986,18 @@ Data\Models + + UI\ViewModels + + + UI\Win32 + + + UI\Win32\Bindings + + + UI\Win32\Bindings + Services diff --git a/src/RA_Resource.h b/src/RA_Resource.h index a10a9f1e..161a698d 100644 --- a/src/RA_Resource.h +++ b/src/RA_Resource.h @@ -164,6 +164,7 @@ #define IDD_RA_PROGRESS 1512 #define IDD_RA_NEWASSET 1513 #define IDD_RA_POINTERFINDER 1514 +#define IDD_RA_POINTERINSPECTOR 1515 #define IDC_RA_PASSWORD 1535 #define IDC_RA_SAVEPASSWORD 1536 #define IDC_RA_USERNAME 1549 @@ -209,6 +210,7 @@ #define IDM_RA_NON_HARDCORE_WARNING 1718 #define IDM_RA_FILES_OPENALL 1719 #define IDM_RA_FILES_POINTERFINDER 1720 +#define IDM_RA_FILES_POINTERINSPECTOR 1721 #define IDM_RA_MENUEND 1739 // Next default values for new objects diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc index c11b26a7..23353008 100644 --- a/src/RA_Shared.rc +++ b/src/RA_Shared.rc @@ -263,6 +263,25 @@ BEGIN PUSHBUTTON "Move Down",IDC_RA_MOVE_BOOKMARK_DOWN,290,137,50,14 END +IDD_RA_POINTERINSPECTOR DIALOGEX 0, 0, 341, 232 +STYLE DS_SETFONT | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Pointer Inspector" +FONT 8, "MS Shell Dlg", 400, 0, 0x0 +BEGIN + LTEXT "Root Address:",IDC_STATIC,6,5,63,11 + EDITTEXT IDC_RA_ADDRESS,55,3,54,12,ES_AUTOHSCROLL,WS_EX_RIGHT + LTEXT "Viewing Node:",IDC_STATIC,120,5,83,11 + COMBOBOX IDC_RA_FILTER_VALUE,170,3,168,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Description:",IDC_STATIC,6,20,63,11 + EDITTEXT IDC_RA_DESCRIPTION,55,18,283,12,ES_AUTOHSCROLL + CONTROL "",IDC_RA_LBX_GROUPS,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | WS_CLIPCHILDREN | WS_BORDER,4,32,334,60 + CONTROL "",IDC_RA_LBX_ADDRESSES,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | WS_CLIPCHILDREN | WS_BORDER,4,95,334,118 + PUSHBUTTON "Bookmark Selected",IDC_RA_ADDBOOKMARK,4,215,70,14,BS_MULTILINE + PUSHBUTTON "Copy Chain",IDC_RA_COPY_ALL,75,215,70,14 + PUSHBUTTON "Pause",IDC_RA_PAUSE,236,215,50,14,BS_MULTILINE + PUSHBUTTON "Freeze",IDC_RA_FREEZE,288,215,50,14,BS_MULTILINE +END + IDD_RA_CODENOTES DIALOGEX 0, 0, 287, 230 STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME CAPTION "Code Notes" diff --git a/src/RA_StringUtils.h b/src/RA_StringUtils.h index 4549e75f..785a01b1 100644 --- a/src/RA_StringUtils.h +++ b/src/RA_StringUtils.h @@ -46,7 +46,7 @@ std::wstring& NormalizeLineEndings(_Inout_ std::wstring& str); // ----- ToString ----- template -_NODISCARD inline const std::string ToString(_In_ const T& value) +_NODISCARD inline const std::string ToAString(_In_ const T& value) { if constexpr (std::is_arithmetic_v) { @@ -75,44 +75,44 @@ _NODISCARD inline const std::string ToString(_In_ const T& value) } template<> -_NODISCARD inline const std::string ToString(_In_ const std::string& value) +_NODISCARD inline const std::string ToAString(_In_ const std::string& value) { return value; } template<> -_NODISCARD inline const std::string ToString(_In_ const std::wstring& value) +_NODISCARD inline const std::string ToAString(_In_ const std::wstring& value) { return ra::Narrow(value); } template<> -_NODISCARD inline const std::string ToString(_In_ const wchar_t* const& value) +_NODISCARD inline const std::string ToAString(_In_ const wchar_t* const& value) { return ra::Narrow(value); } template<> -_NODISCARD inline const std::string ToString(_In_ const char* const& value) +_NODISCARD inline const std::string ToAString(_In_ const char* const& value) { return std::string(value); } template<> -_NODISCARD inline const std::string ToString(_In_ char* const& value) +_NODISCARD inline const std::string ToAString(_In_ char* const& value) { return std::string(value); } template<> -_NODISCARD inline const std::string ToString(_In_ const char& value) +_NODISCARD inline const std::string ToAString(_In_ const char& value) { return std::string(1, value); } // literal strings can't be passed by reference, so won't call the templated methods -_NODISCARD inline const std::string ToString(_In_ const char* value) { return std::string(value); } -_NODISCARD inline const std::string ToString(_In_ const wchar_t* value) { return ra::Narrow(value); } +_NODISCARD inline const std::string ToAString(_In_ const char* value) { return std::string(value); } +_NODISCARD inline const std::string ToAString(_In_ const wchar_t* value) { return ra::Narrow(value); } // ----- ToWString ----- @@ -201,7 +201,7 @@ class StringBuilder if (m_bPrepareWide) m_vPending.emplace_back(std::wstring{ra::ToWString(arg)}); else - m_vPending.emplace_back(std::string{ra::ToString(arg)}); + m_vPending.emplace_back(std::string{ra::ToAString(arg)}); } template<> @@ -442,7 +442,7 @@ class StringBuilder const char c = sFormat.back(); sFormat.pop_back(); // remove 's'/'x' sFormat.pop_back(); // remove '*' - sFormat.append(ra::ToString(value)); + sFormat.append(ra::ToAString(value)); sFormat.push_back(c); // replace 's'/'x' if constexpr (sizeof...(args) > 0) diff --git a/src/data/ModelCollectionBase.hh b/src/data/ModelCollectionBase.hh index 7ff6cd79..33b5f9de 100644 --- a/src/data/ModelCollectionBase.hh +++ b/src/data/ModelCollectionBase.hh @@ -180,6 +180,40 @@ public: return -1; } + /// + /// Finds the index of the first item where the specified property has the specified value. + /// + /// The property to query. + /// The value to find. + /// Index of the first matching item, -1 if not found. + gsl::index FindItemIndex(const StringModelProperty& pProperty, const std::wstring& sValue) const + { + for (gsl::index nIndex = 0; nIndex < gsl::narrow(m_nSize); ++nIndex) + { + if (m_vItems.at(nIndex)->GetValue(pProperty) == sValue) + return nIndex; + } + + return -1; + } + + /// + /// Finds the index of the first item where the specified property has the specified value. + /// + /// The property to query. + /// The value to find. + /// Index of the first matching item, -1 if not found. + gsl::index FindItemIndex(const BoolModelProperty& pProperty, bool bValue) const + { + for (gsl::index nIndex = 0; nIndex < gsl::narrow(m_nSize); ++nIndex) + { + if (m_vItems.at(nIndex)->GetValue(pProperty) == bValue) + return nIndex; + } + + return -1; + } + /// /// Calls the OnBeginModelCollectionUpdate method of any attached NotifyTargets. /// diff --git a/src/data/models/CodeNoteModel.cpp b/src/data/models/CodeNoteModel.cpp index 5005d64a..76d014a2 100644 --- a/src/data/models/CodeNoteModel.cpp +++ b/src/data/models/CodeNoteModel.cpp @@ -70,6 +70,11 @@ uint32_t CodeNoteModel::GetRawPointerValue() const noexcept return m_pPointerData != nullptr ? m_pPointerData->RawPointerValue : 0xFFFFFFFF; } +bool CodeNoteModel::HasNestedPointers() const noexcept +{ + return m_pPointerData != nullptr && m_pPointerData->HasPointers; +} + static ra::ByteAddress ConvertPointer(ra::ByteAddress nAddress) { const auto& pConsoleContext = ra::services::ServiceLocator::Get(); @@ -300,7 +305,19 @@ bool CodeNoteModel::GetNextAddress(ra::ByteAddress nAfterAddress, ra::ByteAddres return bResult; } -void CodeNoteModel::SetNote(const std::wstring& sNote) +std::wstring CodeNoteModel::GetPrimaryNote() const +{ + if (m_pPointerData != nullptr) + { + const auto nIndex = m_sNote.find(L"\n+"); + if (nIndex != std::wstring::npos) + return m_sNote.substr(0, nIndex); + } + + return m_sNote; +} + +void CodeNoteModel::SetNote(const std::wstring& sNote, bool bImpliedPointer) { if (m_sNote == sNote) return; @@ -314,28 +331,53 @@ void CodeNoteModel::SetNote(const std::wstring& sNote) const auto nNextIndex = sNote.find(L'\n', nIndex); sLine = (nNextIndex == std::string::npos) ? sNote.substr(nIndex) : sNote.substr(nIndex, nNextIndex - nIndex); - StringMakeLowercase(sLine); - ExtractSize(sLine); - if (sLine.find(L"pointer") != std::string::npos) + if (!sLine.empty()) { - if (m_nMemSize == MemSize::Unknown) + if (sLine.at(0) == '+' && bImpliedPointer) { - // pointer size not specified. assume 32-bit m_nMemSize = MemSize::ThirtyTwoBit; m_nBytes = 4; + + // found a line starting with a plus sign, bit no pointer annotation. bImpliedPointer + // must be true. assume the parent note is not described. pass -1 as the note size + // because we already skipped over the newline character + ProcessIndirectNotes(sNote, gsl::narrow_cast(-1)); + m_pPointerData->HeaderLength = 0; + break; } - // if there are any lines starting with a plus sign, extract the indirect code notes - nIndex = sNote.find(L"\n+", nIndex + 1); - if (nIndex != std::string::npos) - ProcessIndirectNotes(sNote, nIndex); + StringMakeLowercase(sLine); + ExtractSize(sLine); - break; - } + if (sLine.find(L"pointer") != std::string::npos) + { + if (m_nMemSize == MemSize::Unknown) + { + // pointer size not specified. assume 32-bit + m_nMemSize = MemSize::ThirtyTwoBit; + m_nBytes = 4; + } - if (m_nMemSize != MemSize::Unknown) // found a size. stop processing. - break; + // if there are any lines starting with a plus sign, extract the indirect code notes + nIndex = sNote.find(L"\n+", nIndex + 1); + if (nIndex != std::string::npos) + ProcessIndirectNotes(sNote, nIndex); + + // failed to find nested code notes. create a PointerData object so the note still + // gets treated as a pointer + if (!m_pPointerData) + { + m_pPointerData.reset(new PointerData()); + m_pPointerData->HeaderLength = gsl::narrow_cast(sNote.length()); + } + + break; + } + + if (m_nMemSize != MemSize::Unknown) // found a size. stop processing. + break; + } if (nNextIndex == std::string::npos) // end of string break; @@ -664,16 +706,27 @@ void CodeNoteModel::ProcessIndirectNotes(const std::wstring& sNote, size_t nInde // skip over [whitespace] [optional separator] [whitespace] const wchar_t* pStop = sNextNote.c_str() + sNextNote.length(); - while (pEnd < pStop && isspace(*pEnd)) - pEnd++; - if (pEnd < pStop && !isalnum(*pEnd)) + while (pEnd < pStop && isspace(*pEnd) && *pEnd != '\n') + ++pEnd; + + if (pEnd < pStop) { - pEnd++; - while (pEnd < pStop && isspace(*pEnd)) - pEnd++; + if (*pEnd == '\n') + { + // no separator. found an unannotated note + ++pEnd; + } + else if (!isalnum(*pEnd)) + { + // found a separator. skip it and any following whitespace + ++pEnd; + + while (pEnd < pStop && isspace(*pEnd)) + ++pEnd; + } } - offsetNote.SetNote(sNextNote.substr(pEnd - sNextNote.c_str())); + offsetNote.SetNote(sNextNote.substr(pEnd - sNextNote.c_str()), true); pointerData->HasPointers |= offsetNote.IsPointer(); offsetNote.SetAddress(gsl::narrow_cast(nOffset)); diff --git a/src/data/models/CodeNoteModel.hh b/src/data/models/CodeNoteModel.hh index 507bbb44..d017eb18 100644 --- a/src/data/models/CodeNoteModel.hh +++ b/src/data/models/CodeNoteModel.hh @@ -28,12 +28,13 @@ public: void SetAuthor(const std::string& sAuthor) { m_sAuthor = sAuthor; } void SetAddress(ra::ByteAddress nAddress) noexcept { m_nAddress = nAddress; } - void SetNote(const std::wstring& sNote); + void SetNote(const std::wstring& sNote, bool bImpliedPointer = false); bool IsPointer() const noexcept { return m_pPointerData != nullptr; } std::wstring GetPointerDescription() const; ra::ByteAddress GetPointerAddress() const noexcept; uint32_t GetRawPointerValue() const noexcept; + bool HasNestedPointers() const noexcept; const CodeNoteModel* GetPointerNoteAtOffset(int nOffset) const; std::pair GetPointerNoteAtAddress(ra::ByteAddress nAddress) const; @@ -45,6 +46,7 @@ public: bool GetPreviousAddress(ra::ByteAddress nBeforeAddress, ra::ByteAddress& nPreviousAddress) const; bool GetNextAddress(ra::ByteAddress nAfterAddress, ra::ByteAddress& nNextAddress) const; + std::wstring GetPrimaryNote() const; void EnumeratePointerNotes(ra::ByteAddress nPointerAddress, std::function fCallback) const; void EnumeratePointerNotes(std::function fCallback) const; diff --git a/src/services/AchievementRuntimeExports.cpp b/src/services/AchievementRuntimeExports.cpp index adb39745..b004092b 100644 --- a/src/services/AchievementRuntimeExports.cpp +++ b/src/services/AchievementRuntimeExports.cpp @@ -561,7 +561,7 @@ class AchievementRuntimeExports : private AchievementRuntime const auto* pItem = vmMenuItems.GetItemAt(nIndex); if (!pItem) continue; - + switch (pItem->GetId()) { case IDM_RA_FILES_LOGIN: diff --git a/src/services/PerformanceCounter.hh b/src/services/PerformanceCounter.hh index cb5668c1..90d335d0 100644 --- a/src/services/PerformanceCounter.hh +++ b/src/services/PerformanceCounter.hh @@ -15,6 +15,8 @@ enum class PerformanceCheckpoint MemoryInspectorDoFrame, AssetListDoFrame, AssetEditorDoFrame, + PointerFinderDoFrame, + PointerInspectorDoFrame, NUM_CHECKPOINTS }; diff --git a/src/ui/ViewModelCollection.hh b/src/ui/ViewModelCollection.hh index a58da04e..bb7056a7 100644 --- a/src/ui/ViewModelCollection.hh +++ b/src/ui/ViewModelCollection.hh @@ -128,6 +128,18 @@ public: return dynamic_cast(ModelCollectionBase::AddItem(std::move(pItem))); } + /// + /// Adds an item to the end of the collection. + /// + template + T2& Add(Args&&... args) + { + static_assert(std::is_base_of::value, "T2 not derived from base class T"); + + auto pItem = std::make_unique(std::forward(args)...); + return dynamic_cast(ModelCollectionBase::AddItem(std::move(pItem))); + } + /// /// Adds an item to the end of the collection. /// @@ -148,6 +160,18 @@ public: /// Gets the item at the specified index. /// const T* GetItemAt(gsl::index nIndex) const { return dynamic_cast(GetModelAt(nIndex)); } + + /// + /// Gets the item at the specified index. + /// + template + T2* GetItemAt(gsl::index nIndex) { return dynamic_cast(GetModelAt(nIndex)); } + + /// + /// Gets the item at the specified index. + /// + template + const T2* GetItemAt(gsl::index nIndex) const { return dynamic_cast(GetModelAt(nIndex)); } }; } // namespace ui diff --git a/src/ui/viewmodels/IntegrationMenuViewModel.cpp b/src/ui/viewmodels/IntegrationMenuViewModel.cpp index e6d31b03..47e39ac5 100644 --- a/src/ui/viewmodels/IntegrationMenuViewModel.cpp +++ b/src/ui/viewmodels/IntegrationMenuViewModel.cpp @@ -88,6 +88,7 @@ void IntegrationMenuViewModel::AddCommonMenuItems(LookupItemViewModelCollection& vmMenu.Add(IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); vmMenu.Add(0, L"-----"); vmMenu.Add(IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); + vmMenu.Add(IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); } void IntegrationMenuViewModel::ActivateMenuItem(int nMenuItemId) @@ -146,6 +147,10 @@ void IntegrationMenuViewModel::ActivateMenuItem(int nMenuItemId) ShowPointerFinder(); break; + case IDM_RA_FILES_POINTERINSPECTOR: + ShowPointerInspector(); + break; + case IDM_RA_FILES_CODENOTES: ShowCodeNotes(); break; @@ -321,6 +326,16 @@ void IntegrationMenuViewModel::ShowPointerFinder() } } +void IntegrationMenuViewModel::ShowPointerInspector() +{ + auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); + if (pEmulatorContext.WarnDisableHardcoreMode("inspect pointers")) + { + auto& pWindowManager = ra::services::ServiceLocator::GetMutable(); + pWindowManager.PointerInspector.Show(); + } +} + void IntegrationMenuViewModel::ShowCodeNotes() { auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); diff --git a/src/ui/viewmodels/IntegrationMenuViewModel.hh b/src/ui/viewmodels/IntegrationMenuViewModel.hh index 5cf82c75..23ca822b 100644 --- a/src/ui/viewmodels/IntegrationMenuViewModel.hh +++ b/src/ui/viewmodels/IntegrationMenuViewModel.hh @@ -34,6 +34,7 @@ private: static void ShowMemoryInspector(); static void ShowMemoryBookmarks(); static void ShowPointerFinder(); + static void ShowPointerInspector(); static void ShowCodeNotes(); static void ShowRichPresenceMonitor(); static void ShowAllEditors(); diff --git a/src/ui/viewmodels/LookupItemViewModel.hh b/src/ui/viewmodels/LookupItemViewModel.hh index 96bd494f..4032b704 100644 --- a/src/ui/viewmodels/LookupItemViewModel.hh +++ b/src/ui/viewmodels/LookupItemViewModel.hh @@ -83,6 +83,18 @@ public: return dynamic_cast(ViewModelCollectionBase::AddItem(std::move(pItem))); } + /// + /// Adds an item to the end of the collection. + /// + template + GSL_SUPPRESS_C128 T& Add(Args&&... args) + { + static_assert(std::is_base_of::value, "T not derived from base class LookupItemViewModel"); + + auto pItem = std::make_unique(std::forward(args)...); + return dynamic_cast(ViewModelCollectionBase::AddItem(std::move(pItem))); + } + /// /// Gets the item at the specified index. /// @@ -93,6 +105,18 @@ public: /// const LookupItemViewModel* GetItemAt(gsl::index nIndex) const { return dynamic_cast(GetModelAt(nIndex)); } + /// + /// Gets the item at the specified index. + /// + template + T* GetItemAt(gsl::index nIndex) { return dynamic_cast(GetModelAt(nIndex)); } + + /// + /// Gets the item at the specified index. + /// + template + const T* GetItemAt(gsl::index nIndex) const { return dynamic_cast(GetModelAt(nIndex)); } + /// /// Gets the label for item specified by the provided ID. /// diff --git a/src/ui/viewmodels/MemoryBookmarksViewModel.cpp b/src/ui/viewmodels/MemoryBookmarksViewModel.cpp index 413fa559..82ce3ed0 100644 --- a/src/ui/viewmodels/MemoryBookmarksViewModel.cpp +++ b/src/ui/viewmodels/MemoryBookmarksViewModel.cpp @@ -89,6 +89,17 @@ void MemoryBookmarksViewModel::InitializeNotifyTargets() pEmulatorContext.AddNotifyTarget(*this); } +void MemoryBookmarksViewModel::MemoryBookmarkViewModel::SetAddressWithoutUpdatingValue(ra::ByteAddress nNewAddress) +{ + // set m_bInitialized to false while updating the address to prevent synchronizing the value + const bool bInitialized = m_bInitialized; + m_bInitialized = false; + + SetAddress(nNewAddress); + + m_bInitialized = bInitialized; +} + GSL_SUPPRESS_F6 void MemoryBookmarksViewModel::MemoryBookmarkViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args) { @@ -586,55 +597,58 @@ void MemoryBookmarksViewModel::DoFrame() for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex) { auto& pBookmark = *m_vBookmarks.GetItemAt(nIndex); - if (pBookmark.MemoryChanged()) + UpdateBookmark(pBookmark); + } + + pEmulatorContext.AddNotifyTarget(*this); +} + +void MemoryBookmarksViewModel::UpdateBookmark(MemoryBookmarksViewModel::MemoryBookmarkViewModel& pBookmark) +{ + if (pBookmark.MemoryChanged()) + { + if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange) { - if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange) - { - pBookmark.SetRowColor(ra::ui::Color(0xFFFFC0C0)); + pBookmark.SetRowColor(ra::ui::Color(0xFFFFC0C0)); - const auto nSizeIndex = m_vSizes.FindItemIndex(LookupItemViewModel::IdProperty, ra::etoi(pBookmark.GetSize())); - Expects(nSizeIndex >= 0); + const auto nSizeIndex = m_vSizes.FindItemIndex(LookupItemViewModel::IdProperty, ra::etoi(pBookmark.GetSize())); + Expects(nSizeIndex >= 0); - auto sMessage = ra::StringPrintf(L"%s %s", - m_vSizes.GetItemAt(nSizeIndex)->GetLabel(), - ra::ByteAddressToString(pBookmark.GetAddress())); + auto sMessage = ra::StringPrintf(L"%s %s", m_vSizes.GetItemAt(nSizeIndex)->GetLabel(), ra::ByteAddressToString(pBookmark.GetAddress())); - // remove leading space of " 8-bit" - if (isspace(sMessage.at(0))) - sMessage.erase(0, 1); + // remove leading space of " 8-bit" + if (isspace(sMessage.at(0))) + sMessage.erase(0, 1); - const auto& pDescription = pBookmark.GetDescription(); - if (!pDescription.empty()) + const auto& pDescription = pBookmark.GetDescription(); + if (!pDescription.empty()) + { + auto nDescriptionLength = pDescription.find(L'\n'); + if (nDescriptionLength == std::string::npos) { - auto nDescriptionLength = pDescription.find(L'\n'); - if (nDescriptionLength == std::string::npos) + if (pDescription.length() < 40) { - if (pDescription.length() < 40) - { - nDescriptionLength = pDescription.length(); - } - else - { - nDescriptionLength = pDescription.find_last_of(L' ', 40); - if (nDescriptionLength == std::string::npos) - nDescriptionLength = 40; - } + nDescriptionLength = pDescription.length(); + } + else + { + nDescriptionLength = pDescription.find_last_of(L' ', 40); + if (nDescriptionLength == std::string::npos) + nDescriptionLength = 40; } - sMessage.append(L": "); - sMessage.append(pDescription, 0, nDescriptionLength); } - - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnChange(sMessage); + sMessage.append(L": "); + sMessage.append(pDescription, 0, nDescriptionLength); } - } - else if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange) - { - pBookmark.SetRowColor(ra::ui::Color(ra::to_unsigned(MemoryBookmarkViewModel::RowColorProperty.GetDefaultValue()))); + + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); + pFrameEventQueue.QueuePauseOnChange(sMessage); } } - - pEmulatorContext.AddNotifyTarget(*this); + else if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange) + { + pBookmark.SetRowColor(ra::ui::Color(ra::to_unsigned(MemoryBookmarkViewModel::RowColorProperty.GetDefaultValue()))); + } } bool MemoryBookmarksViewModel::HasBookmark(ra::ByteAddress nAddress) const diff --git a/src/ui/viewmodels/MemoryBookmarksViewModel.hh b/src/ui/viewmodels/MemoryBookmarksViewModel.hh index 26c0aefe..472c3a04 100644 --- a/src/ui/viewmodels/MemoryBookmarksViewModel.hh +++ b/src/ui/viewmodels/MemoryBookmarksViewModel.hh @@ -33,7 +33,7 @@ public: void InitializeNotifyTargets(); - void DoFrame(); + virtual void DoFrame(); enum class BookmarkBehavior { @@ -135,6 +135,11 @@ public: /// bool SetCurrentValue(const std::wstring& sValue, _Out_ std::wstring& sError); + /// + /// Gets the unformatted current value of the bookmarked address. + /// + uint32_t GetCurrentValueRaw() const noexcept { return m_nValue; } + /// /// The for the previous value of the bookmarked address. /// @@ -264,6 +269,8 @@ public: unsigned ReadValue() const; + void SetAddressWithoutUpdatingValue(ra::ByteAddress nNewAddress); + private: std::wstring BuildCurrentValue() const; @@ -399,6 +406,8 @@ protected: void OnViewModelIntValueChanged(gsl::index nIndex, const IntModelProperty::ChangeArgs& args) override; void OnEndViewModelCollectionUpdate() override; + void UpdateBookmark(MemoryBookmarkViewModel& pBookmark); + bool IsModified() const; size_t m_nUnmodifiedBookmarkCount = 0; diff --git a/src/ui/viewmodels/PointerInspectorViewModel.cpp b/src/ui/viewmodels/PointerInspectorViewModel.cpp new file mode 100644 index 00000000..51b749f0 --- /dev/null +++ b/src/ui/viewmodels/PointerInspectorViewModel.cpp @@ -0,0 +1,511 @@ +#include "PointerInspectorViewModel.hh" + +#include "RA_Defs.h" + +#include "data\context\ConsoleContext.hh" + +#include "services\AchievementLogicSerializer.hh" +#include "services\IClipboard.hh" +#include "services\ServiceLocator.hh" + +namespace ra { +namespace ui { +namespace viewmodels { + +const IntModelProperty PointerInspectorViewModel::CurrentAddressProperty("PointerInspectorViewModel", "CurrentAddress", 0); +const StringModelProperty PointerInspectorViewModel::CurrentAddressTextProperty("PointerInspectorViewModel", "CurrentAddressText", L"0x0000"); +const StringModelProperty PointerInspectorViewModel::CurrentAddressNoteProperty("PointerInspectorViewModel", "CurrentAddressNote", L""); +const StringModelProperty PointerInspectorViewModel::StructFieldViewModel::OffsetProperty("StructFieldViewModel", "Offset", L"+0000"); +const IntModelProperty PointerInspectorViewModel::SelectedNodeProperty("PointerInspectorViewModel", "SelectedNode", -2); + +PointerInspectorViewModel::PointerInspectorViewModel() + : MemoryBookmarksViewModel() +{ + SetWindowTitle(L"Pointer Inspector"); +} + +void PointerInspectorViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args) +{ + if (args.Property == CurrentAddressProperty && !m_bSyncingAddress) + { + const auto nAddress = static_cast(args.tNewValue); + + m_bSyncingAddress = true; + SetCurrentAddressText(ra::Widen(ra::ByteAddressToString(nAddress))); + m_bSyncingAddress = false; + + OnCurrentAddressChanged(nAddress); + } + else if (args.Property == SelectedNodeProperty && !m_bSyncingAddress) + { + OnSelectedNodeChanged(args.tNewValue); + } + + MemoryBookmarksViewModel::OnValueChanged(args); +} + +void PointerInspectorViewModel::OnValueChanged(const StringModelProperty::ChangeArgs& args) +{ + if (args.Property == CurrentAddressTextProperty && !m_bSyncingAddress) + { + const auto nAddress = ra::ByteAddressFromString(ra::Narrow(args.tNewValue)); + + // ignore change event for current address so text field is not modified + m_bSyncingAddress = true; + SetCurrentAddress(nAddress); + m_bSyncingAddress = false; + + OnCurrentAddressChanged(nAddress); + } + + MemoryBookmarksViewModel::OnValueChanged(args); +} + +void PointerInspectorViewModel::OnActiveGameChanged() +{ + const auto& pGameContext = ra::services::ServiceLocator::Get(); + if (pGameContext.GameId() == 0) + SetCurrentAddress(0); + else + OnCurrentAddressChanged(GetCurrentAddress()); +} + +void PointerInspectorViewModel::OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring&) +{ + if (nAddress == GetCurrentAddress()) + OnCurrentAddressChanged(nAddress); // not really, but causes the note to be reloaded +} + +void PointerInspectorViewModel::OnCurrentAddressChanged(ra::ByteAddress nNewAddress) +{ + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes(); + if (pCodeNotes != nullptr) + { + // select an invalid node to force LoadNodes to select the new root node after it's been updated + SetSelectedNode(-2); + + const auto* pNote = pCodeNotes->FindCodeNoteModel(nNewAddress); + if (pNote) + { + LoadNodes(pNote); + LoadNote(pNote); + } + else + { + LoadNodes(nullptr); + LoadNote(nullptr); + } + + UpdatePointerChain(GetSelectedNode()); + } +} + +void PointerInspectorViewModel::GetPointerChain(gsl::index nIndex, std::stack& sChain) const +{ + const auto* pNode = m_vNodes.GetItemAt(nIndex); + Expects(pNode != nullptr); + + while (!pNode->IsRootNode()) + { + sChain.push(pNode); + pNode = m_vNodes.GetItemAt(pNode->GetParentIndex()); + Expects(pNode != nullptr); + } + + sChain.push(pNode); +} + +const ra::data::models::CodeNoteModel* PointerInspectorViewModel::FindNestedCodeNoteModel( + const ra::data::models::CodeNoteModel& pRootNote, int nNewNode) +{ + const auto nIndex = m_vNodes.FindItemIndex(LookupItemViewModel::IdProperty, nNewNode); + if (nIndex == -1) + return nullptr; + + std::stack sChain; + GetPointerChain(nIndex, sChain); + + const auto* pParentNote = &pRootNote; + do + { + const auto* pNestedNode = sChain.top(); + Expects(pNestedNode != nullptr); + const auto nOffset = pNestedNode->GetOffset(); + + const auto* pNestedNote = pParentNote->GetPointerNoteAtOffset(nOffset); + if (!pNestedNote) + return nullptr; + + pParentNote = pNestedNote; + sChain.pop(); + } while (!sChain.empty()); + + return pParentNote; +} + +void PointerInspectorViewModel::OnSelectedNodeChanged(int nNewNode) +{ + const auto* pNote = UpdatePointerChain(nNewNode); + LoadNote(pNote); +} + +const ra::data::models::CodeNoteModel* PointerInspectorViewModel::UpdatePointerChain(int nNewNode) +{ + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes(); + const auto nCurrentAddress = GetCurrentAddress(); + const auto* pNote = pCodeNotes ? pCodeNotes->FindCodeNoteModel(nCurrentAddress) : nullptr; + + auto nNewNodeIndex = Nodes().FindItemIndex(PointerNodeViewModel::IdProperty, nNewNode); + if (nNewNodeIndex == -1) + nNewNodeIndex = 0; + + std::stack sChain; + GetPointerChain(nNewNodeIndex, sChain); + + gsl::index nCount = gsl::narrow_cast(PointerChain().Count()); + gsl::index nInsertIndex = 0; + PointerChain().BeginUpdate(); + + do + { + StructFieldViewModel* pItem = nullptr; + if (nInsertIndex < nCount) + { + pItem = PointerChain().GetItemAt(nInsertIndex); + } + else + { + ++nCount; + pItem = &PointerChain().Add(); + } + + Expects(pItem != nullptr); + pItem->BeginInitialization(); + + const auto* pNode = sChain.top(); + Expects(pNode != nullptr); + if (pNode->IsRootNode()) + { + pItem->m_nOffset = nCurrentAddress; + pItem->SetOffset(L""); + } + else + { + pItem->m_nOffset = pNode->GetOffset(); + + std::wstring sLabel; + if (nInsertIndex > 1) + sLabel = std::wstring(nInsertIndex - 1, ' '); + sLabel += ra::StringPrintf(L"+%04x", pItem->m_nOffset); + pItem->SetOffset(sLabel); + + pNote = pNote ? pNote->GetPointerNoteAtOffset(pItem->m_nOffset) : nullptr; + } + + if (pNote) + { + pItem->SetDescription(pNote->GetPointerDescription()); + pItem->SetSize(pNote->GetMemSize()); + } + else + { + pItem->SetDescription(L""); + pItem->SetSize(MemSize::EightBit); + } + + UpdatePointerChainRowColor(*pItem); + pItem->EndInitialization(); + + ++nInsertIndex; + + sChain.pop(); + } while (!sChain.empty()); + + while (nCount > nInsertIndex) + Bookmarks().RemoveAt(--nCount); + + UpdatePointerChainValues(); + + PointerChain().EndUpdate(); + + return pNote; +} + +void PointerInspectorViewModel::SyncField(PointerInspectorViewModel::StructFieldViewModel& pFieldViewModel, const ra::data::models::CodeNoteModel& pOffsetNote) +{ + const auto nSize = pOffsetNote.GetMemSize(); + pFieldViewModel.SetSize((nSize == MemSize::Unknown) ? MemSize::EightBit : nSize); + + const auto& sDescription = pOffsetNote.GetPrimaryNote(); + const auto nIndex = sDescription.find('\n'); + if (nIndex == std::string::npos) + pFieldViewModel.SetDescription(sDescription); + else + pFieldViewModel.SetDescription(sDescription.substr(0, nIndex)); +} + +void PointerInspectorViewModel::LoadNote(const ra::data::models::CodeNoteModel* pNote) +{ + if (pNote == nullptr) + { + SetCurrentAddressNote(L""); + m_pCurrentNote = nullptr; + Bookmarks().Clear(); + return; + } + + SetCurrentAddressNote(pNote->GetPrimaryNote()); + + m_pCurrentNote = pNote; + const auto nBaseAddress = m_pCurrentNote->GetPointerAddress(); + gsl::index nCount = gsl::narrow_cast(Bookmarks().Count()); + + gsl::index nInsertIndex = 0; + Bookmarks().BeginUpdate(); + pNote->EnumeratePointerNotes([this, &nCount, &nInsertIndex, nBaseAddress] + (ra::ByteAddress nAddress, const ra::data::models::CodeNoteModel& pOffsetNote) + { + const auto nOffset = nAddress - nBaseAddress; + const std::wstring sOffset = ra::StringPrintf(L"+%04x", nOffset); + + StructFieldViewModel* pItem = nullptr; + if (nInsertIndex < nCount) + { + pItem = Bookmarks().GetItemAt(nInsertIndex); + Expects(pItem != nullptr); + pItem->SetSelected(false); + pItem->SetFormat(MemFormat::Hex); + } + else + { + ++nCount; + pItem = &Bookmarks().Add(); + } + + pItem->BeginInitialization(); + + pItem->m_nOffset = nOffset; + pItem->SetOffset(sOffset); + SyncField(*pItem, pOffsetNote); + + pItem->EndInitialization(); + + ++nInsertIndex; + return true; + }); + + while (nCount > nInsertIndex) + Bookmarks().RemoveAt(--nCount); + + UpdateValues(); + Bookmarks().EndUpdate(); +} + +static void LoadSubNotes(LookupItemViewModelCollection& vNodes, + const ra::data::models::CodeNoteModel& pNote, ra::ByteAddress nBaseAddress, int nDepth, int nParentIndex) +{ + pNote.EnumeratePointerNotes([&vNodes, nBaseAddress, nDepth, nParentIndex] + (ra::ByteAddress nAddress, const ra::data::models::CodeNoteModel& pOffsetNote) { + const auto nOffset = nAddress - nBaseAddress; + if (!pOffsetNote.IsPointer()) + return true; + + std::wstring sLabel; + if (nDepth > 1) + sLabel = std::wstring(gsl::narrow_cast(nDepth) - 1, ' '); + sLabel += ra::StringPrintf(L"+%04x | %s", nOffset, pOffsetNote.GetPointerDescription()); + + vNodes.Add(nParentIndex, nOffset, sLabel); + + if (pOffsetNote.HasNestedPointers()) + LoadSubNotes(vNodes, pOffsetNote, pOffsetNote.GetPointerAddress(), nDepth + 1, gsl::narrow_cast(vNodes.Count() - 1)); + + return true; + }); +} + +void PointerInspectorViewModel::LoadNodes(const ra::data::models::CodeNoteModel* pNote) +{ + const int nSelectedNode = GetSelectedNode(); + + m_vNodes.BeginUpdate(); + m_vNodes.Clear(); + + if (pNote == nullptr) + { + m_vNodes.Add(-1, ra::to_signed(GetCurrentAddress()), L"Root"); + } + else + { + m_vNodes.Add(-1, ra::to_signed(GetCurrentAddress()), pNote->GetPrimaryNote()); + if (pNote->HasNestedPointers()) + LoadSubNotes(m_vNodes, *pNote, pNote->GetPointerAddress(), 1, 0); + } + + m_vNodes.EndUpdate(); + + // if the selected item is no longer available, select the root node + if (m_vNodes.FindItemIndex(LookupItemViewModel::IdProperty, nSelectedNode) == -1) + SetSelectedNode(PointerNodeViewModel::RootNodeId); +} + +void PointerInspectorViewModel::UpdatePointerChainValues() +{ + ra::ByteAddress nAddress = 0; + const auto nCount = gsl::narrow_cast(PointerChain().Count()); + + PointerChain().BeginUpdate(); + + for (gsl::index nIndex = 0; nIndex < nCount; ++nIndex) + { + auto* pPointer = PointerChain().GetItemAt(nIndex); + if (pPointer != nullptr) + { + nAddress += pPointer->m_nOffset; + pPointer->SetAddressWithoutUpdatingValue(nAddress); + + if (pPointer->MemoryChanged()) + UpdatePointerChainRowColor(*pPointer); + + nAddress = pPointer->GetCurrentValueRaw(); + } + } + + PointerChain().EndUpdate(); +} + +void PointerInspectorViewModel::UpdatePointerChainRowColor(PointerInspectorViewModel::StructFieldViewModel& pPointer) +{ + const auto nPointerValue = pPointer.GetCurrentValueRaw(); + if (nPointerValue == 0) + { + // pointer is null + pPointer.SetRowColor(ra::ui::Color(0xFFFFC0C0)); + return; + } + + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + bool bValid = false; + + MemSize nMemSize = MemSize::Unknown; + uint32_t nMask = 0xFFFFFFFF; + uint32_t nOffset = 0; + if (pConsoleContext.GetRealAddressConversion(&nMemSize, &nMask, &nOffset)) + { + if (nMemSize == MemSize::TwentyFourBit) + nMemSize = MemSize::ThirtyTwoBit; + + const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + const auto nRawPointer = pEmulatorContext.ReadMemory(pPointer.GetAddress(), nMemSize); + bValid = (pConsoleContext.ByteAddressFromRealAddress(nRawPointer) != 0xFFFFFFFF); + } + else + { + bValid = (nPointerValue <= pConsoleContext.MaxAddress()); + } + + if (!bValid) + { + // pointer is invalid + pPointer.SetRowColor(ra::ui::Color(0xFFFFFFC0)); + } + else + { + // pointer is valid + pPointer.SetRowColor( + ra::ui::Color(ra::to_unsigned(MemoryBookmarkViewModel::RowColorProperty.GetDefaultValue()))); + } +} + +void PointerInspectorViewModel::UpdateValues() +{ + const auto nBaseAddress = (m_pCurrentNote != nullptr) ? m_pCurrentNote->GetPointerAddress() : 0U; + + auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); + pEmulatorContext.RemoveNotifyTarget(*this); + + Bookmarks().BeginUpdate(); + + const auto nCount = gsl::narrow_cast(Bookmarks().Count()); + for (gsl::index nIndex = 0; nIndex < nCount; ++nIndex) + { + auto* pField = Bookmarks().GetItemAt(nIndex); + if (pField != nullptr) + { + pField->SetAddress(nBaseAddress + pField->m_nOffset); + UpdateBookmark(*pField); + } + } + Bookmarks().EndUpdate(); + + pEmulatorContext.AddNotifyTarget(*this); +} + +void PointerInspectorViewModel::DoFrame() +{ + UpdatePointerChainValues(); + UpdateValues(); +} + +void PointerInspectorViewModel::CopyDefinition() const +{ + const auto sDefinition = GetDefinition(); + ra::services::ServiceLocator::Get().SetText(ra::Widen(sDefinition)); +} + +std::string PointerInspectorViewModel::GetDefinition() const +{ + std::string sBuffer; + + const auto nSelectedFieldIndex = Bookmarks().FindItemIndex(LookupItemViewModel::IsSelectedProperty, true); + if (nSelectedFieldIndex == -1) + return sBuffer; + + const auto nSelectedNodeIndex = Nodes().FindItemIndex(LookupItemViewModel::IdProperty, GetSelectedNode()); + if (nSelectedNodeIndex == -1) + return sBuffer; + + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes(); + if (pCodeNotes == nullptr) + return sBuffer; + + std::stack sChain; + GetPointerChain(nSelectedNodeIndex, sChain); + + auto* pNote = pCodeNotes->FindCodeNoteModel(GetCurrentAddress()); + Expects(pNote != nullptr); + + do + { + const auto* pNode = sChain.top(); + Expects(pNode != nullptr); + + if (!pNode->IsRootNode()) + pNote = pNote->GetPointerNoteAtOffset(pNode->GetOffset()); + Expects(pNote != nullptr); + + ra::services::AchievementLogicSerializer::AppendConditionType(sBuffer, ra::services::TriggerConditionType::AddAddress); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, + pNote->GetMemSize(), pNote->GetAddress()); + ra::services::AchievementLogicSerializer::AppendConditionSeparator(sBuffer); + + sChain.pop(); + } while (!sChain.empty()); + + const auto* vmField = Bookmarks().GetItemAt(nSelectedFieldIndex); + Expects(vmField != nullptr); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Address, + vmField->GetSize(), ra::to_unsigned(vmField->m_nOffset)); + ra::services::AchievementLogicSerializer::AppendOperator(sBuffer, ra::services::TriggerOperatorType::Equals); + ra::services::AchievementLogicSerializer::AppendOperand(sBuffer, ra::services::TriggerOperandType::Value, + MemSize::ThirtyTwoBit, vmField->GetCurrentValueRaw()); + + return sBuffer; +} + +} // namespace viewmodels +} // namespace ui +} // namespace ra diff --git a/src/ui/viewmodels/PointerInspectorViewModel.hh b/src/ui/viewmodels/PointerInspectorViewModel.hh new file mode 100644 index 00000000..c622e7ba --- /dev/null +++ b/src/ui/viewmodels/PointerInspectorViewModel.hh @@ -0,0 +1,196 @@ +#ifndef RA_UI_POINTERINSPECTORVIEWMODEL_H +#define RA_UI_POINTERINSPECTORVIEWMODEL_H +#pragma once + +#include "data\Types.hh" +#include "data\context\GameContext.hh" + +#include "ui\viewmodels\MemoryBookmarksViewModel.hh" + +namespace ra { +namespace data { +namespace models { + +class CodeNoteModel; + +} // namespace models +} // namespace data +} // namespace ra + +namespace ra { +namespace ui { +namespace viewmodels { + +class PointerInspectorViewModel : public MemoryBookmarksViewModel +{ +public: + GSL_SUPPRESS_F6 PointerInspectorViewModel(); + ~PointerInspectorViewModel() noexcept = default; + + PointerInspectorViewModel(const PointerInspectorViewModel&) noexcept = delete; + PointerInspectorViewModel& operator=(const PointerInspectorViewModel&) noexcept = delete; + PointerInspectorViewModel(PointerInspectorViewModel&&) noexcept = delete; + PointerInspectorViewModel& operator=(PointerInspectorViewModel&&) noexcept = delete; + + /// + /// The for the current address. + /// + static const IntModelProperty CurrentAddressProperty; + + /// + /// Gets the current address. + /// + ra::ByteAddress GetCurrentAddress() const { return GetValue(CurrentAddressProperty); } + + /// + /// Sets the current address. + /// + void SetCurrentAddress(const ra::ByteAddress nValue) { SetValue(CurrentAddressProperty, nValue); } + + /// + /// The for the current address as a string. + /// + static const StringModelProperty CurrentAddressTextProperty; + + /// + /// Gets the current address as a string. + /// + const std::wstring& GetCurrentAddressText() const { return GetValue(CurrentAddressTextProperty); } + + /// + /// Sets the current address as a string. + /// + void SetCurrentAddressText(const std::wstring& sValue) { SetValue(CurrentAddressTextProperty, sValue); } + + /// + /// The for the current address's note. + /// + static const StringModelProperty CurrentAddressNoteProperty; + + /// + /// Gets the current address's note. + /// + const std::wstring& GetCurrentAddressNote() const { return GetValue(CurrentAddressNoteProperty); } + + /// + /// Sets the current address's note. + /// + void SetCurrentAddressNote(const std::wstring& sValue) { SetValue(CurrentAddressNoteProperty, sValue); } + + class StructFieldViewModel : public MemoryBookmarkViewModel + { + public: + /// + /// The for the field offset. + /// + static const StringModelProperty OffsetProperty; + + /// + /// Gets the field offset. + /// + const std::wstring& GetOffset() const { return GetValue(OffsetProperty); } + + /// + /// Sets the field offset. + /// + void SetOffset(const std::wstring& sValue) { SetValue(OffsetProperty, sValue); } + + using MemoryBookmarkViewModel::SetAddressWithoutUpdatingValue; + + int32_t m_nOffset = 0; + }; + + class PointerNodeViewModel : public LookupItemViewModel + { + public: + static const int RootNodeId = -1; + + PointerNodeViewModel(gsl::index nParentIndex, int32_t nOffset, const std::wstring& sLabel) noexcept + : LookupItemViewModel(nParentIndex < 0 ? RootNodeId : gsl::narrow_cast((nParentIndex << 24) | (nOffset & 0x00FFFFFF)), sLabel), + m_nParentIndex(nParentIndex), + m_nOffset(nOffset) + { + } + + bool IsRootNode() const noexcept { return m_nParentIndex == -1; } + gsl::index GetParentIndex() const noexcept { return m_nParentIndex; } + int32_t GetOffset() const noexcept { return m_nOffset; } + + private: + gsl::index m_nParentIndex = -1; + int32_t m_nOffset = 0; + }; + + /// + /// Gets the list of available nodes. + /// + LookupItemViewModelCollection& Nodes() noexcept { return m_vNodes; } + + /// + /// Gets the list of available nodes. + /// + const LookupItemViewModelCollection& Nodes() const noexcept { return m_vNodes; } + + /// + /// The for the selected node. + /// + static const IntModelProperty SelectedNodeProperty; + + /// + /// Gets the selected node index. + /// + int GetSelectedNode() const { return GetValue(SelectedNodeProperty); } + + /// + /// Sets the selected node index. + /// + void SetSelectedNode(int nValue) { SetValue(SelectedNodeProperty, nValue); } + + /// + /// Gets the pointer chain list. + /// + ViewModelCollection& PointerChain() noexcept { return m_vPointerChain; } + + /// + /// Gets the pointer chain list. + /// + const ViewModelCollection& PointerChain() const noexcept { return m_vPointerChain; } + + void DoFrame() override; + + void CopyDefinition() const; + +protected: + void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; + void OnValueChanged(const StringModelProperty::ChangeArgs& args) override; + + // GameContext::NotifyTarget + void OnActiveGameChanged() override; + void OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring& sNewNote) override; + +private: + void OnCurrentAddressChanged(ra::ByteAddress nNewAddress); + void OnSelectedNodeChanged(int nNode); + void LoadNote(const ra::data::models::CodeNoteModel* pNote); + void LoadNodes(const ra::data::models::CodeNoteModel* pNote); + const ra::data::models::CodeNoteModel* FindNestedCodeNoteModel(const ra::data::models::CodeNoteModel& pRootNote, int nNewNode); + void GetPointerChain(gsl::index nIndex, std::stack& sChain) const; + void SyncField(StructFieldViewModel& pFieldViewModel, const ra::data::models::CodeNoteModel& pOffsetNote); + const ra::data::models::CodeNoteModel* UpdatePointerChain(int nNewNode); + void UpdatePointerChainRowColor(StructFieldViewModel& pPointer); + void UpdatePointerChainValues(); + void UpdateValues(); + std::string GetDefinition() const; + + LookupItemViewModelCollection m_vNodes; + ViewModelCollection m_vPointerChain; + bool m_bSyncingAddress = false; + + const ra::data::models::CodeNoteModel* m_pCurrentNote = nullptr; +}; + +} // namespace viewmodels +} // namespace ui +} // namespace ra + +#endif !RA_UI_POINTERINSPECTORVIEWMODEL_H diff --git a/src/ui/viewmodels/WindowManager.hh b/src/ui/viewmodels/WindowManager.hh index 93f4f436..10a1ea61 100644 --- a/src/ui/viewmodels/WindowManager.hh +++ b/src/ui/viewmodels/WindowManager.hh @@ -8,6 +8,7 @@ #include "MemoryBookmarksViewModel.hh" #include "MemoryInspectorViewModel.hh" #include "PointerFinderViewModel.hh" +#include "PointerInspectorViewModel.hh" #include "RichPresenceMonitorViewModel.hh" namespace ra { @@ -24,6 +25,7 @@ public: MemoryInspectorViewModel MemoryInspector; CodeNotesViewModel CodeNotes; PointerFinderViewModel PointerFinder; + PointerInspectorViewModel PointerInspector; }; } // namespace viewmodels diff --git a/src/ui/win32/Desktop.cpp b/src/ui/win32/Desktop.cpp index addfea01..bc09d2a9 100644 --- a/src/ui/win32/Desktop.cpp +++ b/src/ui/win32/Desktop.cpp @@ -19,6 +19,7 @@ #include "ui/win32/NewAssetDialog.hh" #include "ui/win32/OverlaySettingsDialog.hh" #include "ui/win32/PointerFinderDialog.hh" +#include "ui/win32/PointerInspectorDialog.hh" #include "ui/win32/ProgressDialog.hh" #include "ui/win32/RichPresenceDialog.hh" #include "ui/win32/UnknownGameDialog.hh" @@ -47,6 +48,7 @@ Desktop::Desktop() noexcept m_vDialogPresenters.emplace_back(new (std::nothrow) AssetListDialog::Presenter); m_vDialogPresenters.emplace_back(new (std::nothrow) AssetEditorDialog::Presenter); m_vDialogPresenters.emplace_back(new (std::nothrow) PointerFinderDialog::Presenter); + m_vDialogPresenters.emplace_back(new (std::nothrow) PointerInspectorDialog::Presenter); m_vDialogPresenters.emplace_back(new (std::nothrow) FileDialog::Presenter); m_vDialogPresenters.emplace_back(new (std::nothrow) OverlaySettingsDialog::Presenter); m_vDialogPresenters.emplace_back(new (std::nothrow) NewAssetDialog::Presenter); diff --git a/src/ui/win32/MemoryBookmarksDialog.cpp b/src/ui/win32/MemoryBookmarksDialog.cpp index 6ea1de2f..9188fd28 100644 --- a/src/ui/win32/MemoryBookmarksDialog.cpp +++ b/src/ui/win32/MemoryBookmarksDialog.cpp @@ -5,8 +5,11 @@ #include "data\context\EmulatorContext.hh" #include "ui\viewmodels\MessageBoxViewModel.hh" +#include "ui\viewmodels\PointerInspectorViewModel.hh" #include "ui\win32\bindings\GridAddressColumnBinding.hh" +#include "ui\win32\bindings\GridBookmarkFormatColumnBinding.hh" +#include "ui\win32\bindings\GridBookmarkValueColumnBinding.hh" #include "ui\win32\bindings\GridLookupColumnBinding.hh" #include "ui\win32\bindings\GridNumberColumnBinding.hh" #include "ui\win32\bindings\GridTextColumnBinding.hh" @@ -20,7 +23,8 @@ namespace win32 { bool MemoryBookmarksDialog::Presenter::IsSupported(const ra::ui::WindowViewModelBase& vmViewModel) noexcept { - return (dynamic_cast(&vmViewModel) != nullptr); + return (dynamic_cast(&vmViewModel) != nullptr && + dynamic_cast(&vmViewModel) == nullptr); } void MemoryBookmarksDialog::Presenter::ShowModal(ra::ui::WindowViewModelBase& vmViewModel, HWND) @@ -80,100 +84,6 @@ void MemoryBookmarksDialog::BookmarksGridBinding::OnTotalMemorySizeChanged() // ------------------------------------ -class GridBookmarkValueColumnBinding : public ra::ui::win32::bindings::GridTextColumnBinding -{ -public: - GridBookmarkValueColumnBinding(const StringModelProperty& pBoundProperty) noexcept - : ra::ui::win32::bindings::GridTextColumnBinding(pBoundProperty) - { - } - - HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override - { - const auto& vmItems = static_cast(pInfo.pGridBinding)->GetItems(); - if (vmItems.GetItemValue(pInfo.nItemIndex, MemoryBookmarksViewModel::MemoryBookmarkViewModel::ReadOnlyProperty)) - return nullptr; - - return ra::ui::win32::bindings::GridTextColumnBinding::CreateInPlaceEditor(hParent, pInfo); - } - - bool SetText(ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex, const std::wstring& sValue) override - { - auto* vmItem = dynamic_cast(vmItems.GetViewModelAt(nIndex)); - if (vmItem != nullptr) - { - std::wstring sError; - if (!vmItem->SetCurrentValue(sValue, sError)) - { - ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Invalid Input", sError); - return false; - } - } - - return true; - } -}; - -// ------------------------------------ - -class GridBookmarkFormatColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding -{ -public: - GridBookmarkFormatColumnBinding(const IntModelProperty& pBoundProperty, const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept - : ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems) - { - } - - std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override - { - if (IsHidden(vmItems, nIndex)) - return L""; - - return ra::ui::win32::bindings::GridLookupColumnBinding::GetText(vmItems, nIndex); - } - - bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override - { - if (pProperty == MemoryBookmarksViewModel::MemoryBookmarkViewModel::SizeProperty) - return true; - - return ra::ui::win32::bindings::GridLookupColumnBinding::DependsOn(pProperty); - } - - HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override - { - auto* pGridBinding = static_cast(pInfo.pGridBinding); - Expects(pGridBinding != nullptr); - - if (IsHidden(pGridBinding->GetItems(), pInfo.nItemIndex)) - return nullptr; - - return GridLookupColumnBinding::CreateInPlaceEditor(hParent, pInfo); - } - -private: - static bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) - { - const auto nSize = ra::itoe(vmItems.GetItemValue(nIndex, MemoryBookmarksViewModel::MemoryBookmarkViewModel::SizeProperty)); - switch (nSize) - { - case MemSize::Float: - case MemSize::FloatBigEndian: - case MemSize::Double32: - case MemSize::Double32BigEndian: - case MemSize::MBF32: - case MemSize::MBF32LE: - case MemSize::Text: - return true; - - default: - return false; - } - } -}; - -// ------------------------------------ - MemoryBookmarksDialog::MemoryBookmarksDialog(MemoryBookmarksViewModel& vmMemoryBookmarks) : DialogBase(vmMemoryBookmarks), m_bindBookmarks(vmMemoryBookmarks) @@ -213,14 +123,14 @@ MemoryBookmarksDialog::MemoryBookmarksDialog(MemoryBookmarksViewModel& vmMemoryB pSizeColumn->SetReadOnly(false); m_bindBookmarks.BindColumn(2, std::move(pSizeColumn)); - auto pFormatColumn = std::make_unique( + auto pFormatColumn = std::make_unique( MemoryBookmarksViewModel::MemoryBookmarkViewModel::FormatProperty, vmMemoryBookmarks.Formats()); pFormatColumn->SetHeader(L"Format"); pFormatColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 32); pFormatColumn->SetReadOnly(false); m_bindBookmarks.BindColumn(3, std::move(pFormatColumn)); - auto pValueColumn = std::make_unique( + auto pValueColumn = std::make_unique( MemoryBookmarksViewModel::MemoryBookmarkViewModel::CurrentValueProperty); pValueColumn->SetHeader(L"Value"); pValueColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 72); @@ -228,7 +138,7 @@ MemoryBookmarksDialog::MemoryBookmarksDialog(MemoryBookmarksViewModel& vmMemoryB pValueColumn->SetReadOnly(false); m_bindBookmarks.BindColumn(4, std::move(pValueColumn)); - auto pPriorColumn = std::make_unique( + auto pPriorColumn = std::make_unique( MemoryBookmarksViewModel::MemoryBookmarkViewModel::PreviousValueProperty); pPriorColumn->SetHeader(L"Prior"); pPriorColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 72); diff --git a/src/ui/win32/PointerFinderDialog.hh b/src/ui/win32/PointerFinderDialog.hh index 2fd6b479..ade82669 100644 --- a/src/ui/win32/PointerFinderDialog.hh +++ b/src/ui/win32/PointerFinderDialog.hh @@ -21,7 +21,7 @@ namespace win32 { class PointerFinderDialog : public DialogBase { public: - explicit PointerFinderDialog(viewmodels::PointerFinderViewModel& vmNewAsset); + explicit PointerFinderDialog(viewmodels::PointerFinderViewModel& vmPointerFinder); virtual ~PointerFinderDialog() noexcept = default; PointerFinderDialog(const PointerFinderDialog&) noexcept = delete; PointerFinderDialog& operator=(const PointerFinderDialog&) noexcept = delete; diff --git a/src/ui/win32/PointerInspectorDialog.cpp b/src/ui/win32/PointerInspectorDialog.cpp new file mode 100644 index 00000000..de419847 --- /dev/null +++ b/src/ui/win32/PointerInspectorDialog.cpp @@ -0,0 +1,216 @@ +#include "PointerInspectorDialog.hh" + +#include "RA_Defs.h" +#include "RA_Log.h" +#include "RA_Resource.h" + +#include "ui\viewmodels\WindowManager.hh" + +#include "ui\win32\bindings\GridAddressColumnBinding.hh" +#include "ui\win32\bindings\GridBookmarkFormatColumnBinding.hh" +#include "ui\win32\bindings\GridBookmarkValueColumnBinding.hh" +#include "ui\win32\bindings\GridLookupColumnBinding.hh" +#include "ui\win32\bindings\GridTextColumnBinding.hh" + +using ra::ui::viewmodels::MemoryViewerViewModel; +using ra::ui::viewmodels::PointerInspectorViewModel; +using ra::ui::win32::bindings::GridColumnBinding; + +namespace ra { +namespace ui { +namespace win32 { + +bool PointerInspectorDialog::Presenter::IsSupported(const ra::ui::WindowViewModelBase& vmViewModel) noexcept +{ + return (dynamic_cast(&vmViewModel) != nullptr); +} + +void PointerInspectorDialog::Presenter::ShowModal(ra::ui::WindowViewModelBase& vmViewModel, HWND) +{ + ShowWindow(vmViewModel); +} + +void PointerInspectorDialog::Presenter::ShowWindow(ra::ui::WindowViewModelBase& vmViewModel) +{ + auto* vmPointerFinder = dynamic_cast(&vmViewModel); + Expects(vmPointerFinder != nullptr); + + if (m_pDialog == nullptr) + { + m_pDialog.reset(new PointerInspectorDialog(*vmPointerFinder)); + if (!m_pDialog->CreateDialogWindow(MAKEINTRESOURCE(IDD_RA_POINTERINSPECTOR), this)) + RA_LOG_ERR("Could not create Pointer Inspector dialog!"); + } + + m_pDialog->ShowDialogWindow(); +} + +void PointerInspectorDialog::Presenter::OnClosed() noexcept { m_pDialog.reset(); } + +// ------------------------------------ + +PointerInspectorDialog::PointerInspectorDialog(PointerInspectorViewModel& vmPointerFinder) + : DialogBase(vmPointerFinder), + m_bindAddress(vmPointerFinder), + m_bindNodes(vmPointerFinder), + m_bindDescription(vmPointerFinder), + m_bindPointerChain(vmPointerFinder), + m_bindFields(vmPointerFinder) +{ + m_bindWindow.SetInitialPosition(RelativePosition::After, RelativePosition::Near, "Pointer Finder"); + m_bindWindow.BindLabel(IDC_RA_PAUSE, PointerInspectorViewModel::PauseButtonTextProperty); + m_bindWindow.BindLabel(IDC_RA_FREEZE, PointerInspectorViewModel::FreezeButtonTextProperty); + m_bindWindow.BindEnabled(IDC_RA_ADDBOOKMARK, PointerInspectorViewModel::HasSelectionProperty); + m_bindWindow.BindEnabled(IDC_RA_COPY_ALL, PointerInspectorViewModel::HasSelectionProperty); + m_bindWindow.BindEnabled(IDC_RA_PAUSE, PointerInspectorViewModel::HasSelectionProperty); + m_bindWindow.BindEnabled(IDC_RA_FREEZE, PointerInspectorViewModel::HasSelectionProperty); + + m_bindAddress.BindText(PointerInspectorViewModel::CurrentAddressTextProperty, ra::ui::win32::bindings::TextBoxBinding::UpdateMode::Typing); + m_bindNodes.BindItems(vmPointerFinder.Nodes()); + m_bindNodes.BindSelectedItem(PointerInspectorViewModel::SelectedNodeProperty); + m_bindDescription.BindText(PointerInspectorViewModel::CurrentAddressNoteProperty); + + auto pOffsetColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::OffsetProperty); + pOffsetColumn->SetHeader(L"Offset"); + pOffsetColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 80); + m_bindFields.BindColumn(0, std::move(pOffsetColumn)); + + auto pDescriptionColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::DescriptionProperty); + pDescriptionColumn->SetHeader(L"Description"); + pDescriptionColumn->SetWidth(GridColumnBinding::WidthType::Fill, 80); + pDescriptionColumn->SetReadOnly(false); + m_bindFields.BindColumn(1, std::move(pDescriptionColumn)); + + auto pSizeColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::SizeProperty, vmPointerFinder.Sizes()); + pSizeColumn->SetHeader(L"Size"); + pSizeColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 76); + pSizeColumn->SetAlignment(ra::ui::RelativePosition::Far); + pSizeColumn->SetReadOnly(false); + m_bindFields.BindColumn(2, std::move(pSizeColumn)); + + auto pFormatColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::FormatProperty, vmPointerFinder.Formats()); + pFormatColumn->SetHeader(L"Format"); + pFormatColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 32); + pFormatColumn->SetReadOnly(false); + m_bindFields.BindColumn(3, std::move(pFormatColumn)); + + auto pValueColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::CurrentValueProperty); + pValueColumn->SetHeader(L"Value"); + pValueColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 72); + pValueColumn->SetAlignment(ra::ui::RelativePosition::Center); + pValueColumn->SetReadOnly(false); + m_bindFields.BindColumn(4, std::move(pValueColumn)); + + auto pBehaviorColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::BehaviorProperty, vmPointerFinder.Behaviors()); + pBehaviorColumn->SetHeader(L"Behavior"); + pBehaviorColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 60); + pBehaviorColumn->SetReadOnly(false); + m_bindFields.BindColumn(5, std::move(pBehaviorColumn)); + + m_bindFields.BindItems(vmPointerFinder.Bookmarks()); + m_bindFields.BindIsSelected(PointerInspectorViewModel::StructFieldViewModel::IsSelectedProperty); + m_bindFields.BindRowColor(PointerInspectorViewModel::StructFieldViewModel::RowColorProperty); + m_bindFields.SetShowGridLines(true); + + pOffsetColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::OffsetProperty); + pOffsetColumn->SetHeader(L"Offset"); + pOffsetColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 80); + m_bindPointerChain.BindColumn(0, std::move(pOffsetColumn)); + + auto pAddressColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::AddressProperty); + pAddressColumn->SetHeader(L"Address"); + pAddressColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 80); + m_bindPointerChain.BindColumn(1, std::move(pAddressColumn)); + + pValueColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::CurrentValueProperty); + pValueColumn->SetHeader(L"Value"); + pValueColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 72); + pValueColumn->SetAlignment(ra::ui::RelativePosition::Center); + m_bindPointerChain.BindColumn(2, std::move(pValueColumn)); + + pDescriptionColumn = std::make_unique( + PointerInspectorViewModel::StructFieldViewModel::DescriptionProperty); + pDescriptionColumn->SetHeader(L"Description"); + pDescriptionColumn->SetWidth(GridColumnBinding::WidthType::Fill, 80); + m_bindPointerChain.BindColumn(3, std::move(pDescriptionColumn)); + + m_bindPointerChain.BindItems(vmPointerFinder.PointerChain()); + m_bindPointerChain.BindRowColor(PointerInspectorViewModel::StructFieldViewModel::RowColorProperty); + + using namespace ra::bitwise_ops; + SetAnchor(IDC_RA_ADDRESS, Anchor::Top | Anchor::Left); + SetAnchor(IDC_RA_FILTER_VALUE, Anchor::Top | Anchor::Left | Anchor::Right); + SetAnchor(IDC_RA_DESCRIPTION, Anchor::Top | Anchor::Left | Anchor::Right); + SetAnchor(IDC_RA_LBX_GROUPS, Anchor::Top | Anchor::Left | Anchor::Right); + SetAnchor(IDC_RA_LBX_ADDRESSES, Anchor::Top | Anchor::Left | Anchor::Bottom | Anchor::Right); + SetAnchor(IDC_RA_ADDBOOKMARK, Anchor::Left | Anchor::Bottom); + SetAnchor(IDC_RA_COPY_ALL, Anchor::Left | Anchor::Bottom); + SetAnchor(IDC_RA_PAUSE, Anchor::Right | Anchor::Bottom); + SetAnchor(IDC_RA_FREEZE, Anchor::Right | Anchor::Bottom); + + SetMinimumSize(480, 258); +} + +BOOL PointerInspectorDialog::OnInitDialog() +{ + m_bindAddress.SetControl(*this, IDC_RA_ADDRESS); + m_bindNodes.SetControl(*this, IDC_RA_FILTER_VALUE); + m_bindDescription.SetControl(*this, IDC_RA_DESCRIPTION); + m_bindPointerChain.SetControl(*this, IDC_RA_LBX_GROUPS); + m_bindFields.SetControl(*this, IDC_RA_LBX_ADDRESSES); + + return DialogBase::OnInitDialog(); +} + +BOOL PointerInspectorDialog::OnCommand(WORD nCommand) +{ + switch (nCommand) + { + //case IDC_RA_ADDBOOKMARK: { + // auto* vmPointerInspector = dynamic_cast(&m_vmWindow); + // if (vmPointerInspector) + // vmPointerInspector->AddBookmark(); + + // return TRUE; + //} + + case IDC_RA_COPY_ALL: { + const auto* vmPointerInspector = dynamic_cast(&m_vmWindow); + if (vmPointerInspector) + vmPointerInspector->CopyDefinition(); + + return TRUE; + } + + case IDC_RA_PAUSE: { + auto* vmPointerInspector = dynamic_cast(&m_vmWindow); + if (vmPointerInspector) + vmPointerInspector->TogglePauseSelected(); + + return TRUE; + } + + case IDC_RA_FREEZE: { + auto* vmPointerInspector = dynamic_cast(&m_vmWindow); + if (vmPointerInspector) + vmPointerInspector->ToggleFreezeSelected(); + + return TRUE; + } + } + + return DialogBase::OnCommand(nCommand); +} + +} // namespace win32 +} // namespace ui +} // namespace ra diff --git a/src/ui/win32/PointerInspectorDialog.hh b/src/ui/win32/PointerInspectorDialog.hh new file mode 100644 index 00000000..466ba7f6 --- /dev/null +++ b/src/ui/win32/PointerInspectorDialog.hh @@ -0,0 +1,58 @@ +#ifndef RA_UI_WIN32_DLG_POINTERINSPECTOR_H +#define RA_UI_WIN32_DLG_POINTERINSPECTOR_H +#pragma once + +#include "ui/viewmodels/PointerInspectorViewModel.hh" + +#include "ui/win32/DialogBase.hh" +#include "ui/win32/IDialogPresenter.hh" + +#include "ui/win32/bindings/GridBinding.hh" +#include "ui/win32/bindings/ComboBoxBinding.hh" +#include "ui/win32/bindings/TextBoxBinding.hh" + +#include + +namespace ra { +namespace ui { +namespace win32 { + +class PointerInspectorDialog : public DialogBase +{ +public: + explicit PointerInspectorDialog(viewmodels::PointerInspectorViewModel& vmPointerInspector); + virtual ~PointerInspectorDialog() noexcept = default; + PointerInspectorDialog(const PointerInspectorDialog&) noexcept = delete; + PointerInspectorDialog& operator=(const PointerInspectorDialog&) noexcept = delete; + PointerInspectorDialog(PointerInspectorDialog&&) noexcept = delete; + PointerInspectorDialog& operator=(PointerInspectorDialog&&) noexcept = delete; + + class Presenter : public IClosableDialogPresenter + { + public: + bool IsSupported(const ra::ui::WindowViewModelBase& viewModel) noexcept override; + void ShowWindow(ra::ui::WindowViewModelBase& viewModel) override; + void ShowModal(ra::ui::WindowViewModelBase& viewModel, HWND hParentWnd) override; + void OnClosed() noexcept override; + + private: + std::unique_ptr m_pDialog; + }; + +protected: + BOOL OnInitDialog() override; + BOOL OnCommand(WORD nCommand) override; + +private: + bindings::TextBoxBinding m_bindAddress; + bindings::ComboBoxBinding m_bindNodes; + bindings::TextBoxBinding m_bindDescription; + bindings::GridBinding m_bindPointerChain; + bindings::GridBinding m_bindFields; +}; + +} // namespace win32 +} // namespace ui +} // namespace ra + +#endif // !RA_UI_WIN32_DLG_POINTERINSPECTOR_H diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp index b7a05f9f..c7ed0044 100644 --- a/src/ui/win32/bindings/GridBinding.cpp +++ b/src/ui/win32/bindings/GridBinding.cpp @@ -110,6 +110,7 @@ void GridBinding::UpdateLayout() for (gsl::index i = 0; ra::to_unsigned(i) < m_vColumns.size(); ++i) { const auto& pColumn = *m_vColumns.at(i); + Expects(&pColumn != nullptr); switch (pColumn.GetWidthType()) { case GridColumnBinding::WidthType::Pixels: diff --git a/src/ui/win32/bindings/GridBookmarkFormatColumnBinding.hh b/src/ui/win32/bindings/GridBookmarkFormatColumnBinding.hh new file mode 100644 index 00000000..bdacf5bf --- /dev/null +++ b/src/ui/win32/bindings/GridBookmarkFormatColumnBinding.hh @@ -0,0 +1,77 @@ +#ifndef RA_UI_WIN32_GRIDBOOKMARKFORMATCOLUMNBINDING_H +#define RA_UI_WIN32_GRIDBOOKMARKFORMATCOLUMNBINDING_H +#pragma once + +#include "GridBinding.hh" +#include "GridLookupColumnBinding.hh" + +#include "ui\viewmodels\MemoryBookmarksViewModel.hh" + +namespace ra { +namespace ui { +namespace win32 { +namespace bindings { + +class GridBookmarkFormatColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding +{ +public: + GridBookmarkFormatColumnBinding(const IntModelProperty& pBoundProperty, + const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept : + ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems) + {} + + std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override + { + if (IsHidden(vmItems, nIndex)) + return L""; + + return ra::ui::win32::bindings::GridLookupColumnBinding::GetText(vmItems, nIndex); + } + + bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override + { + if (pProperty == ra::ui::viewmodels::MemoryBookmarksViewModel::MemoryBookmarkViewModel::SizeProperty) + return true; + + return ra::ui::win32::bindings::GridLookupColumnBinding::DependsOn(pProperty); + } + + HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override + { + auto* pGridBinding = static_cast(pInfo.pGridBinding); + Expects(pGridBinding != nullptr); + + if (IsHidden(pGridBinding->GetItems(), pInfo.nItemIndex)) + return nullptr; + + return GridLookupColumnBinding::CreateInPlaceEditor(hParent, pInfo); + } + +private: + static bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) + { + const auto nSize = ra::itoe( + vmItems.GetItemValue(nIndex, ra::ui::viewmodels::MemoryBookmarksViewModel::MemoryBookmarkViewModel::SizeProperty)); + switch (nSize) + { + case MemSize::Float: + case MemSize::FloatBigEndian: + case MemSize::Double32: + case MemSize::Double32BigEndian: + case MemSize::MBF32: + case MemSize::MBF32LE: + case MemSize::Text: + return true; + + default: + return false; + } + } +}; + +} // namespace bindings +} // namespace win32 +} // namespace ui +} // namespace ra + +#endif // !RA_UI_WIN32_GRIDBOOKMARKFORMATCOLUMNBINDING_H diff --git a/src/ui/win32/bindings/GridBookmarkValueColumnBinding.hh b/src/ui/win32/bindings/GridBookmarkValueColumnBinding.hh new file mode 100644 index 00000000..3f5c2349 --- /dev/null +++ b/src/ui/win32/bindings/GridBookmarkValueColumnBinding.hh @@ -0,0 +1,55 @@ +#ifndef RA_UI_WIN32_GRIDBOOKMARKVALUECOLUMNBINDING_H +#define RA_UI_WIN32_GRIDBOOKMARKVALUECOLUMNBINDING_H +#pragma once + +#include "GridBinding.hh" +#include "GridTextColumnBinding.hh" + +#include "ui\viewmodels\MemoryBookmarksViewModel.hh" +#include "ui\viewmodels\MessageBoxViewModel.hh" + +namespace ra { +namespace ui { +namespace win32 { +namespace bindings { + +class GridBookmarkValueColumnBinding : public ra::ui::win32::bindings::GridTextColumnBinding +{ +public: + GridBookmarkValueColumnBinding(const StringModelProperty& pBoundProperty) noexcept : + ra::ui::win32::bindings::GridTextColumnBinding(pBoundProperty) + {} + + HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override + { + const auto& vmItems = static_cast(pInfo.pGridBinding)->GetItems(); + if (vmItems.GetItemValue(pInfo.nItemIndex, ra::ui::viewmodels::MemoryBookmarksViewModel::MemoryBookmarkViewModel::ReadOnlyProperty)) + return nullptr; + + return ra::ui::win32::bindings::GridTextColumnBinding::CreateInPlaceEditor(hParent, pInfo); + } + + bool SetText(ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex, const std::wstring& sValue) override + { + auto* vmItem = dynamic_cast( + vmItems.GetViewModelAt(nIndex)); + if (vmItem != nullptr) + { + std::wstring sError; + if (!vmItem->SetCurrentValue(sValue, sError)) + { + ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"Invalid Input", sError); + return false; + } + } + + return true; + } +}; + +} // namespace bindings +} // namespace win32 +} // namespace ui +} // namespace ra + +#endif // !RA_UI_WIN32_GRIDBOOKMARKVALUECOLUMNBINDING_H diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp index f2873bad..ae72c81b 100644 --- a/tests/Exports_Tests.cpp +++ b/tests/Exports_Tests.cpp @@ -469,7 +469,7 @@ TEST_CLASS(Exports_Tests) ra::data::context::mocks::MockUserContext mockUserContext; RA_MenuItem menu[32]; - Assert::AreEqual(17, _RA_GetPopupMenuItems(menu)); + Assert::AreEqual(18, _RA_GetPopupMenuItems(menu)); AssertMenuItem(&menu[0], IDM_RA_FILES_LOGIN, L"&Login"); AssertMenuItem(&menu[1], 0, nullptr); AssertMenuItem(&menu[2], IDM_RA_HARDCORE_MODE, L"&Hardcore Mode"); @@ -487,6 +487,7 @@ TEST_CLASS(Exports_Tests) AssertMenuItem(&menu[14], IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); AssertMenuItem(&menu[15], 0, nullptr); AssertMenuItem(&menu[16], IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); + AssertMenuItem(&menu[17], IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); } TEST_METHOD(TestGetPopupMenuItemsLoggedIn) @@ -496,7 +497,7 @@ TEST_CLASS(Exports_Tests) mockUserContext.Initialize("User", "TOKEN"); RA_MenuItem menu[32]; - Assert::AreEqual(23, _RA_GetPopupMenuItems(menu)); + Assert::AreEqual(24, _RA_GetPopupMenuItems(menu)); AssertMenuItem(&menu[0], IDM_RA_FILES_LOGOUT, L"Log&out"); AssertMenuItem(&menu[1], 0, nullptr); AssertMenuItem(&menu[2], IDM_RA_OPENUSERPAGE, L"Open my &User Page"); @@ -517,9 +518,10 @@ TEST_CLASS(Exports_Tests) AssertMenuItem(&menu[17], IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); AssertMenuItem(&menu[18], 0, nullptr); AssertMenuItem(&menu[19], IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); - AssertMenuItem(&menu[20], 0, nullptr); - AssertMenuItem(&menu[21], IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); - AssertMenuItem(&menu[22], IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); + AssertMenuItem(&menu[20], IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); + AssertMenuItem(&menu[21], 0, nullptr); + AssertMenuItem(&menu[22], IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); + AssertMenuItem(&menu[23], IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); } TEST_METHOD(TestGetPopupMenuItemsChecked) @@ -532,7 +534,7 @@ TEST_CLASS(Exports_Tests) mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true); RA_MenuItem menu[32]; - Assert::AreEqual(23, _RA_GetPopupMenuItems(menu)); + Assert::AreEqual(24, _RA_GetPopupMenuItems(menu)); AssertMenuItem(&menu[0], IDM_RA_FILES_LOGOUT, L"Log&out"); AssertMenuItem(&menu[1], 0, nullptr); AssertMenuItem(&menu[2], IDM_RA_OPENUSERPAGE, L"Open my &User Page"); @@ -553,9 +555,10 @@ TEST_CLASS(Exports_Tests) AssertMenuItem(&menu[17], IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); AssertMenuItem(&menu[18], 0, nullptr); AssertMenuItem(&menu[19], IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); - AssertMenuItem(&menu[20], 0, nullptr); - AssertMenuItem(&menu[21], IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); - AssertMenuItem(&menu[22], IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); + AssertMenuItem(&menu[20], IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); + AssertMenuItem(&menu[21], 0, nullptr); + AssertMenuItem(&menu[22], IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); + AssertMenuItem(&menu[23], IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); } TEST_METHOD(TestUpdateAppTitle) diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj index 5eb2d465..67c1cf1c 100644 --- a/tests/RA_Integration.Tests.vcxproj +++ b/tests/RA_Integration.Tests.vcxproj @@ -349,6 +349,7 @@ + @@ -425,6 +426,7 @@ + diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters index 7b2e0833..854984e2 100644 --- a/tests/RA_Integration.Tests.vcxproj.filters +++ b/tests/RA_Integration.Tests.vcxproj.filters @@ -474,6 +474,12 @@ Tests\Data\Models + + Tests\UI\ViewModels + + + Code + Code diff --git a/tests/RA_StringUtils_Tests.cpp b/tests/RA_StringUtils_Tests.cpp index 242bb0e0..94bad312 100644 --- a/tests/RA_StringUtils_Tests.cpp +++ b/tests/RA_StringUtils_Tests.cpp @@ -79,23 +79,23 @@ TEST_CLASS(RA_StringUtils_Tests) Assert::AreEqual(std::wstring(L"test test\r\ntest"), Trim(std::wstring(L"\ttest test\r\ntest\r\n\r\n"))); } - TEST_METHOD(TestToString) + TEST_METHOD(TestToAString) { - Assert::AreEqual(std::string("0"), ToString(0)); - Assert::AreEqual(std::string("1"), ToString(1)); - Assert::AreEqual(std::string{'a'}, ToString('a')); - Assert::AreEqual(std::string{'a'}, ToString(L'a')); - Assert::AreEqual(std::string{'3'}, ToString('3')); - Assert::AreEqual(std::string{'3'}, ToString(L'3')); - Assert::AreEqual(std::string("99"), ToString(99)); - Assert::AreEqual(std::string("-3"), ToString(-3)); - Assert::AreEqual(std::string("0"), ToString(0U)); - Assert::AreEqual(std::string("1"), ToString(1U)); - Assert::AreEqual(std::string("99"), ToString(99U)); - Assert::AreEqual(std::string("Apple"), ToString("Apple")); - Assert::AreEqual(std::string("Apple"), ToString(std::string("Apple"))); - Assert::AreEqual(std::string("Apple"), ToString(L"Apple")); - Assert::AreEqual(std::string("Apple"), ToString(std::wstring(L"Apple"))); + Assert::AreEqual(std::string("0"), ToAString(0)); + Assert::AreEqual(std::string("1"), ToAString(1)); + Assert::AreEqual(std::string{'a'}, ToAString('a')); + Assert::AreEqual(std::string{'a'}, ToAString(L'a')); + Assert::AreEqual(std::string{'3'}, ToAString('3')); + Assert::AreEqual(std::string{'3'}, ToAString(L'3')); + Assert::AreEqual(std::string("99"), ToAString(99)); + Assert::AreEqual(std::string("-3"), ToAString(-3)); + Assert::AreEqual(std::string("0"), ToAString(0U)); + Assert::AreEqual(std::string("1"), ToAString(1U)); + Assert::AreEqual(std::string("99"), ToAString(99U)); + Assert::AreEqual(std::string("Apple"), ToAString("Apple")); + Assert::AreEqual(std::string("Apple"), ToAString(std::string("Apple"))); + Assert::AreEqual(std::string("Apple"), ToAString(L"Apple")); + Assert::AreEqual(std::string("Apple"), ToAString(std::wstring(L"Apple"))); } TEST_METHOD(TestToWString) diff --git a/tests/RA_UnitTestHelpers.h b/tests/RA_UnitTestHelpers.h index 72b1924f..681954af 100644 --- a/tests/RA_UnitTestHelpers.h +++ b/tests/RA_UnitTestHelpers.h @@ -122,6 +122,20 @@ std::wstring ToString(const ComparisonType& t) } } +template<> +std::wstring ToString(const ra::MemFormat& t) +{ + switch (t) + { + case ra::MemFormat::Hex: + return L"Hex"; + case ra::MemFormat::Dec: + return L"Dec"; + default: + return std::to_wstring(ra::etoi(t)); + } +} + #pragma warning(pop) } // namespace CppUnitTestFramework diff --git a/tests/data/models/CodeNoteModel_Tests.cpp b/tests/data/models/CodeNoteModel_Tests.cpp index 610453ce..1e8fba45 100644 --- a/tests/data/models/CodeNoteModel_Tests.cpp +++ b/tests/data/models/CodeNoteModel_Tests.cpp @@ -335,6 +335,29 @@ TEST_CLASS(CodeNoteModel_Tests) AssertIndirectNote(note, 0x448, L"[32-bit BE] Not-nested number", MemSize::ThirtyTwoBitBigEndian, 4); } + + TEST_METHOD(TestCodeNoteImpliedPointerChain) + { + CodeNoteModelHarness note; + const std::wstring sNote = + L"Root Pointer [24 bits]\n" + L"\n" + L"+0x8318\n" + L"++0x8014\n" + L"+++0x8004 = First [32-bits]\n" + L"+++0x8008 = Second [32-bits]"; + note.SetNote(sNote); + + Assert::AreEqual(MemSize::TwentyFourBit, note.GetMemSize()); + Assert::AreEqual(sNote, note.GetNote()); // full note for pointer address + + const auto* nestedNote = AssertIndirectNote(note, 0x8318, + L"+0x8014\n++0x8004 = First [32-bits]\n++0x8008 = Second [32-bits]", MemSize::ThirtyTwoBit, 4); + const auto* nestedNote2 = AssertIndirectNote(*nestedNote, 0x8014, + L"+0x8004 = First [32-bits]\n+0x8008 = Second [32-bits]", MemSize::ThirtyTwoBit, 4); + AssertIndirectNote(*nestedNote2, 0x8004, L"First [32-bits]", MemSize::ThirtyTwoBit, 4); + AssertIndirectNote(*nestedNote2, 0x8008, L"Second [32-bits]", MemSize::ThirtyTwoBit, 4); + } }; } // namespace tests diff --git a/tests/services/AchievementRuntimeExports_Tests.cpp b/tests/services/AchievementRuntimeExports_Tests.cpp index 8c5a1ac9..52092e00 100644 --- a/tests/services/AchievementRuntimeExports_Tests.cpp +++ b/tests/services/AchievementRuntimeExports_Tests.cpp @@ -313,7 +313,7 @@ TEST_CLASS(AchievementRuntimeExports_Tests) const rc_client_raintegration_menu_t* pMenu; pMenu = _Rcheevos_RAIntegrationGetMenu(); - Assert::AreEqual(12U, pMenu->num_items); + Assert::AreEqual(13U, pMenu->num_items); AssertMenuItem(pMenu, 0, IDM_RA_HARDCORE_MODE, "&Hardcore Mode"); AssertMenuItem(pMenu, 1, IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning"); AssertMenuSeparator(pMenu, 2); @@ -326,13 +326,14 @@ TEST_CLASS(AchievementRuntimeExports_Tests) AssertMenuItem(pMenu, 9, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); AssertMenuSeparator(pMenu, 10); AssertMenuItem(pMenu, 11, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); + AssertMenuItem(pMenu, 12, IDM_RA_FILES_POINTERINSPECTOR, "Pointer &Inspector"); runtime.mockUserContext.Initialize("User", "ApiToken"); runtime.AssertMenuChangedEventSeen(); runtime.ResetSeenEvents(); pMenu = _Rcheevos_RAIntegrationGetMenu(); - Assert::AreEqual(18U, pMenu->num_items); + Assert::AreEqual(19U, pMenu->num_items); AssertMenuItem(pMenu, 0, IDM_RA_OPENUSERPAGE, "Open my &User Page"); AssertMenuItem(pMenu, 1, IDM_RA_OPENGAMEPAGE, "Open this &Game's Page"); AssertMenuSeparator(pMenu, 2); @@ -348,15 +349,16 @@ TEST_CLASS(AchievementRuntimeExports_Tests) AssertMenuItem(pMenu, 12, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); AssertMenuSeparator(pMenu, 13); AssertMenuItem(pMenu, 14, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); - AssertMenuSeparator(pMenu, 15); - AssertMenuItem(pMenu, 16, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); - AssertMenuItem(pMenu, 17, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); + AssertMenuItem(pMenu, 15, IDM_RA_FILES_POINTERINSPECTOR, "Pointer &Inspector"); + AssertMenuSeparator(pMenu, 16); + AssertMenuItem(pMenu, 17, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); + AssertMenuItem(pMenu, 18, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::NonHardcoreWarning, true); pMenu = _Rcheevos_RAIntegrationGetMenu(); - Assert::AreEqual(18U, pMenu->num_items); + Assert::AreEqual(19U, pMenu->num_items); AssertMenuItem(pMenu, 0, IDM_RA_OPENUSERPAGE, "Open my &User Page"); AssertMenuItem(pMenu, 1, IDM_RA_OPENGAMEPAGE, "Open this &Game's Page"); AssertMenuSeparator(pMenu, 2); @@ -372,9 +374,10 @@ TEST_CLASS(AchievementRuntimeExports_Tests) AssertMenuItem(pMenu, 12, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); AssertMenuSeparator(pMenu, 13); AssertMenuItem(pMenu, 14, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); - AssertMenuSeparator(pMenu, 15); - AssertMenuItem(pMenu, 16, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); - AssertMenuItem(pMenu, 17, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); + AssertMenuItem(pMenu, 15, IDM_RA_FILES_POINTERINSPECTOR, "Pointer &Inspector"); + AssertMenuSeparator(pMenu, 16); + AssertMenuItem(pMenu, 17, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); + AssertMenuItem(pMenu, 18, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); } diff --git a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp index 49be1bc6..5606e883 100644 --- a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp +++ b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp @@ -134,7 +134,7 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.BuildMenu(); - menu.AssertMenuSize(17); + menu.AssertMenuSize(18); menu.AssertMenuItem(0, IDM_RA_FILES_LOGIN, L"&Login"); menu.AssertMenuSeparator(1); menu.AssertMenuItem(2, IDM_RA_HARDCORE_MODE, L"&Hardcore Mode"); @@ -152,6 +152,7 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.AssertMenuItem(14, IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); menu.AssertMenuSeparator(15); menu.AssertMenuItem(16, IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); + menu.AssertMenuItem(17, IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); } TEST_METHOD(TestBuildMenuLoggedIn) @@ -161,7 +162,7 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.BuildMenu(); - menu.AssertMenuSize(23); + menu.AssertMenuSize(24); menu.AssertMenuItem(0, IDM_RA_FILES_LOGOUT, L"Log&out"); menu.AssertMenuSeparator(1); menu.AssertMenuItem(2, IDM_RA_OPENUSERPAGE, L"Open my &User Page"); @@ -182,9 +183,10 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.AssertMenuItem(17, IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); menu.AssertMenuSeparator(18); menu.AssertMenuItem(19, IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); - menu.AssertMenuSeparator(20); - menu.AssertMenuItem(21, IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); - menu.AssertMenuItem(22, IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); + menu.AssertMenuItem(20, IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); + menu.AssertMenuSeparator(21); + menu.AssertMenuItem(22, IDM_RA_REPORTBROKENACHIEVEMENTS, L"&Report Achievement Problem"); + menu.AssertMenuItem(23, IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); } TEST_METHOD(TestBuildMenuOffline) @@ -194,7 +196,7 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.BuildMenu(); - menu.AssertMenuSize(17); + menu.AssertMenuSize(18); menu.AssertMenuItem(0, IDM_RA_HARDCORE_MODE, L"&Hardcore Mode"); menu.AssertMenuItem(1, IDM_RA_NON_HARDCORE_WARNING, L"Non-Hardcore &Warning"); menu.AssertMenuSeparator(2); @@ -210,8 +212,9 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.AssertMenuItem(12, IDM_RA_PARSERICHPRESENCE, L"Rich &Presence Monitor"); menu.AssertMenuSeparator(13); menu.AssertMenuItem(14, IDM_RA_FILES_POINTERFINDER, L"Pointer &Finder"); - menu.AssertMenuSeparator(15); - menu.AssertMenuItem(16, IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); + menu.AssertMenuItem(15, IDM_RA_FILES_POINTERINSPECTOR, L"Pointer &Inspector"); + menu.AssertMenuSeparator(16); + menu.AssertMenuItem(17, IDM_RA_GETROMCHECKSUM, L"View Game H&ash"); } TEST_METHOD(TestLoginHardcoreValidClient) @@ -562,6 +565,34 @@ TEST_CLASS(IntegrationMenuViewModel_Tests) menu.AssertShowWindow(IDM_RA_PARSERICHPRESENCE, false, "", DialogResult::None); } + TEST_METHOD(TestShowPointerFinderHardcoreAbort) + { + IntegrationMenuViewModelHarness menu; + menu.AssertShowWindow( + IDM_RA_FILES_POINTERFINDER, true, "find pointers", DialogResult::No); + } + + TEST_METHOD(TestShowPointerFinderNonHardcore) + { + IntegrationMenuViewModelHarness menu; + menu.AssertShowWindow( + IDM_RA_FILES_POINTERFINDER, false, "find pointer", DialogResult::None); + } + + TEST_METHOD(TestShowPointerInspectorHardcoreAbort) + { + IntegrationMenuViewModelHarness menu; + menu.AssertShowWindow( + IDM_RA_FILES_POINTERINSPECTOR, true, "inspect pointers", DialogResult::No); + } + + TEST_METHOD(TestShowPointerInspectorNonHardcore) + { + IntegrationMenuViewModelHarness menu; + menu.AssertShowWindow( + IDM_RA_FILES_POINTERINSPECTOR, false, "inspect pointers", DialogResult::None); + } + TEST_METHOD(TestOpenAllHardcore) { IntegrationMenuViewModelHarness menu; diff --git a/tests/ui/viewmodels/PointerInspectorViewModel_Tests.cpp b/tests/ui/viewmodels/PointerInspectorViewModel_Tests.cpp new file mode 100644 index 00000000..d18b54b2 --- /dev/null +++ b/tests/ui/viewmodels/PointerInspectorViewModel_Tests.cpp @@ -0,0 +1,474 @@ +#include "CppUnitTest.h" + +#include "api\DeleteCodeNote.hh" +#include "api\UpdateCodeNote.hh" + +#include "ui\viewmodels\PointerInspectorViewModel.hh" + +#include "tests\ui\UIAsserts.hh" +#include "tests\RA_UnitTestHelpers.h" + +#include "tests\mocks\MockClipboard.hh" +#include "tests\mocks\MockConfiguration.hh" +#include "tests\mocks\MockConsoleContext.hh" +#include "tests\mocks\MockDesktop.hh" +#include "tests\mocks\MockEmulatorContext.hh" +#include "tests\mocks\MockGameContext.hh" +#include "tests\mocks\MockLocalStorage.hh" +#include "tests\mocks\MockUserContext.hh" +#include "tests\mocks\MockServer.hh" +#include "tests\mocks\MockThreadPool.hh" +#include "tests\mocks\MockWindowManager.hh" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace ra { +namespace ui { +namespace viewmodels { +namespace tests { + +TEST_CLASS(PointerInspectorViewModel_Tests) +{ +private: + class PointerInspectorViewModelHarness : public PointerInspectorViewModel + { + public: + ra::data::context::mocks::MockConsoleContext mockConsoleContext; + ra::data::context::mocks::MockGameContext mockGameContext; + ra::data::context::mocks::MockUserContext mockUserContext; + ra::data::context::mocks::MockEmulatorContext mockEmulatorContext; + ra::services::mocks::MockClipboard mockClipboard; + + std::array memory{}; + + GSL_SUPPRESS_F6 PointerInspectorViewModelHarness() + { + InitializeNotifyTargets(); + + for (size_t i = 0; i < memory.size(); ++i) + memory.at(i) = gsl::narrow_cast(i); + + mockEmulatorContext.MockMemory(memory); + + mockUserContext.SetUsername("Author"); + + mockGameContext.InitializeCodeNotes(); + } + + ~PointerInspectorViewModelHarness() = default; + + PointerInspectorViewModelHarness(const PointerInspectorViewModelHarness&) noexcept = delete; + PointerInspectorViewModelHarness& operator=(const PointerInspectorViewModelHarness&) noexcept = delete; + PointerInspectorViewModelHarness(PointerInspectorViewModelHarness&&) noexcept = delete; + PointerInspectorViewModelHarness& operator=(PointerInspectorViewModelHarness&&) noexcept = delete; + + void DoFrame() override + { + mockGameContext.DoFrame(); // ensure note pointers get updated + PointerInspectorViewModel::DoFrame(); + } + + const std::wstring* FindCodeNote(ra::ByteAddress nAddress) const + { + const auto* pCodeNotes = mockGameContext.Assets().FindCodeNotes(); + return (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNote(nAddress) : nullptr; + } + + void AssertField(gsl::index nIndex, int32_t nOffset, ra::ByteAddress nAddress, + const std::wstring& sOffset, const std::wstring& sDescription, + MemSize nSize, MemFormat nFormat, const std::wstring& sCurrentValue) + { + const auto* pField = Bookmarks().GetItemAt(nIndex); + Assert::IsNotNull(pField); + Ensures(pField != nullptr); + Assert::AreEqual(nOffset, pField->m_nOffset); + Assert::AreEqual(nAddress, pField->GetAddress()); + Assert::AreEqual(sOffset, pField->GetOffset()); + Assert::AreEqual(sDescription, pField->GetDescription()); + Assert::AreEqual(nSize, pField->GetSize()); + Assert::AreEqual(nFormat, pField->GetFormat()); + Assert::AreEqual(sCurrentValue, pField->GetCurrentValue()); + } + + void AssertPointerChain(gsl::index nIndex, const std::wstring& sOffset, + ra::ByteAddress nAddress, const std::wstring& sDescription, + const std::wstring& sValue) + { + const auto* pPointer = PointerChain().GetItemAt(nIndex); + Assert::IsNotNull(pPointer); + Ensures(pPointer != nullptr); + Assert::AreEqual(sOffset, pPointer->GetOffset()); + Assert::AreEqual(nAddress, pPointer->GetAddress()); + Assert::AreEqual(sDescription, pPointer->GetDescription()); + Assert::AreEqual(sValue, pPointer->GetCurrentValue()); + } + + void AssertPointerChainAddressValid(gsl::index nIndex) + { + const auto* pPointer = PointerChain().GetItemAt(nIndex); + Assert::IsNotNull(pPointer); + Ensures(pPointer != nullptr); + Assert::AreEqual(ra::ui::Color(0x00000000).ARGB, pPointer->GetRowColor().ARGB); + } + + void AssertPointerChainAddressInvalid(gsl::index nIndex) + { + const auto* pPointer = PointerChain().GetItemAt(nIndex); + Assert::IsNotNull(pPointer); + Ensures(pPointer != nullptr); + Assert::AreEqual(ra::ui::Color(0xFFFFFFC0).ARGB, pPointer->GetRowColor().ARGB); + } + + void AssertPointerChainAddressNull(gsl::index nIndex) + { + const auto* pPointer = PointerChain().GetItemAt(nIndex); + Assert::IsNotNull(pPointer); + Ensures(pPointer != nullptr); + Assert::AreEqual(ra::ui::Color(0xFFFFC0C0).ARGB, pPointer->GetRowColor().ARGB); + } + }; + + +public: + TEST_METHOD(TestInitialValues) + { + PointerInspectorViewModelHarness inspector; + + Assert::AreEqual({ 0U }, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0000"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(), inspector.GetCurrentAddressNote()); + } + + TEST_METHOD(TestSetCurrentAddress) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + inspector.SetCurrentAddress({ 3U }); + + Assert::AreEqual({ 3U }, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0003"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(), inspector.GetCurrentAddressNote()); + } + + TEST_METHOD(TestSetCurrentAddressText) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + inspector.SetCurrentAddressText(L"3"); + + Assert::AreEqual({ 3U }, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"3"), inspector.GetCurrentAddressText()); /* don't update text when user types in partial address */ + Assert::AreEqual(std::wstring(), inspector.GetCurrentAddressNote()); + } + + TEST_METHOD(TestSetCurrentAddressWithNote) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + inspector.SetCurrentAddress({ 3U }); + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({3U}, L"Note on 3"); + + Assert::AreEqual({ 3U }, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0003"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(L"Note on 3"), inspector.GetCurrentAddressNote()); + Assert::AreEqual({ 0U }, inspector.Bookmarks().Count()); + + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({3U}, L"Modified Note on 3"); + + Assert::AreEqual(std::wstring(L"Modified Note on 3"), inspector.GetCurrentAddressNote()); + } + + TEST_METHOD(TestSetCurrentAddressWithPointerNote) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + std::array memory = {}; + for (uint8_t i = 8; i < memory.size(); i += 4) + memory.at(i) = i; + inspector.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 12; + + inspector.SetCurrentAddress({4U}); + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({4U}, + L"[32-bit pointer] Player data\n" + L"+4: [32-bit] Current HP\n" + L"+8: [32-bit] Max HP"); + + Assert::AreEqual({ 4U }, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0004"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(L"[32-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + + Assert::AreEqual({ 2U }, inspector.Bookmarks().Count()); + + inspector.AssertField(0, 4, 16U, L"+0004", L"[32-bit] Current HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000010"); + inspector.AssertField(1, 8, 20U, L"+0008", L"[32-bit] Max HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000014"); + } + + TEST_METHOD(TestSelectedNode) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + std::array memory = {}; + for (uint8_t i = 4; i < memory.size(); i += 4) + memory.at(i) = i + 8; + inspector.mockEmulatorContext.MockMemory(memory); + + inspector.SetCurrentAddress({4U}); + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({4U}, + L"[8-bit pointer] Player data\n" + L"+0x00: [8-bit pointer] Row 1\n" + L".+0x00: [8-bit] Column 1a\n" + L".+0x04: [8-bit pointer] Column 1b\n" + L"..+0x00: [8-bit] Column 1a\n" + L".+0x08: [8-bit] Column 1c\n" + L"+0x08: [8-bit pointer] Row 2\n" + L"+0x10: [8-bit pointer] Row 3\n" + L"+0x18: Generic data" + ); + + Assert::AreEqual({4U}, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0004"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + + Assert::AreEqual({5U}, inspector.Nodes().Count()); + Assert::AreEqual(PointerInspectorViewModel::PointerNodeViewModel::RootNodeId, inspector.Nodes().GetItemAt(0)->GetId()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Player data"), inspector.Nodes().GetItemAt(0)->GetLabel()); + Assert::AreEqual(0x00000000, inspector.Nodes().GetItemAt(1)->GetId()); + Assert::AreEqual(std::wstring(L"+0000 | [8-bit pointer] Row 1"), inspector.Nodes().GetItemAt(1)->GetLabel()); + Assert::AreEqual(0x01000004, inspector.Nodes().GetItemAt(2)->GetId()); + Assert::AreEqual(std::wstring(L" +0004 | [8-bit pointer] Column 1b"), inspector.Nodes().GetItemAt(2)->GetLabel()); + Assert::AreEqual(0x00000008, inspector.Nodes().GetItemAt(3)->GetId()); + Assert::AreEqual(std::wstring(L"+0008 | [8-bit pointer] Row 2"), inspector.Nodes().GetItemAt(3)->GetLabel()); + Assert::AreEqual(0x00000010, inspector.Nodes().GetItemAt(4)->GetId()); + Assert::AreEqual(std::wstring(L"+0010 | [8-bit pointer] Row 3"), inspector.Nodes().GetItemAt(4)->GetLabel()); + + Assert::AreEqual(PointerInspectorViewModel::PointerNodeViewModel::RootNodeId, inspector.GetSelectedNode()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + Assert::AreEqual({4U}, inspector.Bookmarks().Count()); + inspector.AssertField(0, 0, 0x0CU, L"+0000", L"[8-bit pointer] Row 1", MemSize::EightBit, MemFormat::Hex, L"14"); + inspector.AssertField(1, 8, 0x14U, L"+0008", L"[8-bit pointer] Row 2", MemSize::EightBit, MemFormat::Hex, L"1c"); + inspector.AssertField(2, 16, 0x1CU, L"+0010", L"[8-bit pointer] Row 3", MemSize::EightBit, MemFormat::Hex, L"24"); + inspector.AssertField(3, 24, 0x24U, L"+0018", L"Generic data", MemSize::EightBit, MemFormat::Hex, L"2c"); + + Assert::AreEqual({1U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[8-bit pointer] Player data", L"0c"); + + inspector.SetSelectedNode(0x00000008); + Assert::AreEqual(0x00000008, inspector.GetSelectedNode()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Row 2"), inspector.GetCurrentAddressNote()); + Assert::AreEqual({0U}, inspector.Bookmarks().Count()); + + Assert::AreEqual({2U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[8-bit pointer] Player data", L"0c"); + inspector.AssertPointerChain(1, L"+0008", 0x14, L"[8-bit pointer] Row 2", L"1c"); + + inspector.SetSelectedNode(0x00000000); + Assert::AreEqual(0x00000000, inspector.GetSelectedNode()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Row 1"), inspector.GetCurrentAddressNote()); + Assert::AreEqual({3U}, inspector.Bookmarks().Count()); + inspector.AssertField(0, 0, 0x14U, L"+0000", L"[8-bit] Column 1a", MemSize::EightBit, MemFormat::Hex, L"1c"); + inspector.AssertField(1, 4, 0x18U, L"+0004", L"[8-bit pointer] Column 1b", MemSize::EightBit, MemFormat::Hex, L"20"); + inspector.AssertField(2, 8, 0x1CU, L"+0008", L"[8-bit] Column 1c", MemSize::EightBit, MemFormat::Hex, L"24"); + + Assert::AreEqual({2U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[8-bit pointer] Player data", L"0c"); + inspector.AssertPointerChain(1, L"+0000", 0x0C, L"[8-bit pointer] Row 1", L"14"); + } + + TEST_METHOD(TestCopyDefinition) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + std::array memory = {}; + for (uint8_t i = 4; i < memory.size(); i += 4) + memory.at(i) = i + 8; + inspector.mockEmulatorContext.MockMemory(memory); + + inspector.SetCurrentAddress({4U}); + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({4U}, + L"[8-bit pointer] Player data\n" + L"+0x00: [8-bit pointer] Row 1\n" + L".+0x00: [8-bit] Column 1a\n" + L".+0x04: [8-bit pointer] Column 1b\n" + L"..+0x00: [8-bit] Column 1a\n" + L".+0x08: [8-bit] Column 1c\n" + L"+0x08: [8-bit pointer] Row 2\n" + L"+0x10: [8-bit pointer] Row 3\n" + L"+0x18: Generic data" + ); + + Assert::AreEqual({4U}, inspector.GetCurrentAddress()); + + Assert::AreEqual(PointerInspectorViewModel::PointerNodeViewModel::RootNodeId, inspector.GetSelectedNode()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + inspector.AssertField(1, 8, 0x14U, L"+0008", L"[8-bit pointer] Row 2", MemSize::EightBit, MemFormat::Hex, L"1c"); + + inspector.CopyDefinition(); + Assert::AreEqual(std::wstring(), inspector.mockClipboard.GetText()); + + inspector.Bookmarks().GetItemAt(1)->SetSelected(true); + inspector.CopyDefinition(); + Assert::AreEqual(std::wstring(L"I:0xH0004_0xH0008=28"), inspector.mockClipboard.GetText()); + + inspector.SetSelectedNode(0x01000004); + Assert::AreEqual(0x01000004, inspector.GetSelectedNode()); + Assert::AreEqual(std::wstring(L"[8-bit pointer] Column 1b"), inspector.GetCurrentAddressNote()); + inspector.AssertField(0, 0, 0x20U, L"+0000", L"[8-bit] Column 1a", MemSize::EightBit, MemFormat::Hex, L"28"); + inspector.CopyDefinition(); + Assert::AreEqual(std::wstring(), inspector.mockClipboard.GetText()); + + inspector.Bookmarks().GetItemAt(0)->SetSelected(true); + inspector.CopyDefinition(); + Assert::AreEqual(std::wstring(L"I:0xH0004_I:0xH0000_I:0xH0004_0xH0000=40"), inspector.mockClipboard.GetText()); + } + + TEST_METHOD(TestDoFrame) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + std::array memory = {}; + for (uint8_t i = 8; i < memory.size(); i += 4) + memory.at(i) = i; + inspector.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 12; + + inspector.SetCurrentAddress({4U}); + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({4U}, + L"[32-bit pointer] Player data\n" + L"+4: [32-bit] Current HP\n" + L"+8: [32-bit] Max HP"); + + Assert::AreEqual({4U}, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0004"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(L"[32-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + + Assert::AreEqual({2U}, inspector.Bookmarks().Count()); + inspector.AssertField(0, 4, 16U, L"+0004", L"[32-bit] Current HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000010"); + inspector.AssertField(1, 8, 20U, L"+0008", L"[32-bit] Max HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000014"); + + Assert::AreEqual({1U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"0000000c"); + + // data changes + memory.at(17) = 1; + memory.at(20) = 99; + inspector.DoFrame(); + inspector.AssertField(0, 4, 16U, L"+0004", L"[32-bit] Current HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000110"); + inspector.AssertField(1, 8, 20U, L"+0008", L"[32-bit] Max HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000063"); + + Assert::AreEqual({1U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"0000000c"); + + // pointer changes + memory.at(4) = 13; + inspector.DoFrame(); + inspector.AssertField(0, 4, 17U, L"+0004", L"[32-bit] Current HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"63000001"); + inspector.AssertField(1, 8, 21U, L"+0008", L"[32-bit] Max HP", MemSize::ThirtyTwoBit, MemFormat::Hex, L"18000000"); + + Assert::AreEqual({1U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"0000000d"); + } + + TEST_METHOD(TestDoFrameNested) + { + PointerInspectorViewModelHarness inspector; + inspector.mockGameContext.SetGameId(1); + inspector.mockGameContext.NotifyActiveGameChanged(); // enable note support + + std::array memory = {}; + inspector.mockConsoleContext.AddMemoryRegion(0x00, 0x3F, ra::data::context::ConsoleContext::AddressType::SystemRAM); + inspector.mockEmulatorContext.MockMemory(memory); + + inspector.mockGameContext.Assets().FindCodeNotes()->SetCodeNote({4U}, + L"[32-bit pointer] Player data\n" + L"+4: [32-bit pointer] 1st player\n" + L"++8: [32-bit pointer] Health\n" + L"+++0: [32-bit] Current HP\n" + L"+++8: [32-bit] Max HP"); + + memory.at(0x0004) = 0x0C; + memory.at(0x0008) = 0x08; + memory.at(0x000C) = 0x30; + memory.at(0x0010) = 0x34; + memory.at(0x0038) = 0x24; + memory.at(0x003C) = 0x20; + memory.at(0x0020) = 0x56; + memory.at(0x0024) = 0x60; + memory.at(0x0028) = 0x64; + + // update pointer values in note + inspector.mockGameContext.Assets().FindCodeNotes()->DoFrame(); + + inspector.SetCurrentAddress({4U}); + Assert::AreEqual({4U}, inspector.GetCurrentAddress()); + Assert::AreEqual(std::wstring(L"0x0004"), inspector.GetCurrentAddressText()); + Assert::AreEqual(std::wstring(L"[32-bit pointer] Player data"), inspector.GetCurrentAddressNote()); + + inspector.SetSelectedNode(0x01000008); + Assert::AreEqual(std::wstring(L"[32-bit pointer] Health"), inspector.GetCurrentAddressNote()); + + // all pointers are good + Assert::AreEqual({2U}, inspector.Bookmarks().Count()); + inspector.AssertField(0, 0, 0x20U, L"+0000", L"[32-bit] Current HP", // 20+00=20 (56) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000056"); + inspector.AssertField(1, 8, 0x28U, L"+0008", L"[32-bit] Max HP", // 20+08=28 (64) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000064"); + + Assert::AreEqual({3U}, inspector.PointerChain().Count()); + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"0000000c"); // 04 (0C) + inspector.AssertPointerChain(1, L"+0004", 0x10, L"[32-bit pointer] 1st player", L"00000034"); // 0C+04=10 (34) + inspector.AssertPointerChain(2, L" +0008", 0x3C, L"[32-bit pointer] Health", L"00000020"); // 34+08=3C (20) + inspector.AssertPointerChainAddressValid(0); + inspector.AssertPointerChainAddressValid(1); + inspector.AssertPointerChainAddressValid(2); + + // second level is null + memory.at(0x0010) = 0x00; + inspector.DoFrame(); + + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"0000000c"); // 04 (0C) + inspector.AssertPointerChain(1, L"+0004", 0x10, L"[32-bit pointer] 1st player", L"00000000"); // 0C+04=10 (00) + inspector.AssertPointerChain(2, L" +0008", 0x08, L"[32-bit pointer] Health", L"00000008"); // 00+08=08 (08) + inspector.AssertPointerChainAddressValid(0); + inspector.AssertPointerChainAddressNull(1); + inspector.AssertPointerChainAddressValid(2); + inspector.AssertField(0, 0, 0x08U, L"+0000", L"[32-bit] Current HP", // 08+00=08 (08) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000008"); + inspector.AssertField(1, 8, 0x10U, L"+0008", L"[32-bit] Max HP", // 08+08=10 (00) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000000"); + + // third level is out of range + memory.at(0x0004) = 0x20; + inspector.DoFrame(); + + inspector.AssertPointerChain(0, L"", 0x04, L"[32-bit pointer] Player data", L"00000020"); // 04 (20) + inspector.AssertPointerChain(1, L"+0004", 0x24, L"[32-bit pointer] 1st player", L"00000060"); // 20+04=24 (60) + inspector.AssertPointerChain(2, L" +0008", 0x68, L"[32-bit pointer] Health", L"00000000"); // 60+08=68 (00) + inspector.AssertPointerChainAddressValid(0); + inspector.AssertPointerChainAddressInvalid(1); + inspector.AssertPointerChainAddressNull(2); + inspector.AssertField(0, 0, 0x00U, L"+0000", L"[32-bit] Current HP", // 00+00=00 (00) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000000"); + inspector.AssertField(1, 8, 0x08U, L"+0008", L"[32-bit] Max HP", // 00+08=08 (08) + MemSize::ThirtyTwoBit, MemFormat::Hex, L"00000008"); + } +}; + +} // namespace tests +} // namespace viewmodels +} // namespace ui +} // namespace ra