From 2a7c61be9d38c9ca06042be2a33ffb662816790e Mon Sep 17 00:00:00 2001 From: Jamiras <32680403+Jamiras@users.noreply.github.com> Date: Sat, 10 Feb 2024 07:09:44 -0700 Subject: [PATCH] support for indirect code notes via overflow math (#1066) --- src/data/models/CodeNotesModel.cpp | 175 +++++---- src/data/models/CodeNotesModel.hh | 16 +- .../viewmodels/TriggerConditionViewModel.cpp | 26 +- tests/data/models/CodeNotesModel_Tests.cpp | 351 ++++++++++++++++++ .../TriggerConditionViewModel_Tests.cpp | 65 +++- 5 files changed, 556 insertions(+), 77 deletions(-) diff --git a/src/data/models/CodeNotesModel.cpp b/src/data/models/CodeNotesModel.cpp index 22a8f4c1..483ede17 100644 --- a/src/data/models/CodeNotesModel.cpp +++ b/src/data/models/CodeNotesModel.cpp @@ -278,20 +278,12 @@ void CodeNotesModel::ExtractSize(CodeNote& pNote) } } -static unsigned ReadPointer(const ra::data::context::EmulatorContext& pEmulatorContext, - ra::ByteAddress nPointerAddress, MemSize nSize) +static ra::ByteAddress ConvertPointer(ra::ByteAddress nAddress) { - auto nAddress = pEmulatorContext.ReadMemory(nPointerAddress, nSize); - - // assume anything annotated as a 32-bit pointer is providing a real (non-translated) address and - // attempt to do the translation ourself. - if (nSize == MemSize::ThirtyTwoBit) - { - const auto& pConsoleContext = ra::services::ServiceLocator::Get(); - const auto nConvertedAddress = pConsoleContext.ByteAddressFromRealAddress(nAddress); - if (nConvertedAddress != 0xFFFFFFFF) - nAddress = nConvertedAddress; - } + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + const auto nConvertedAddress = pConsoleContext.ByteAddressFromRealAddress(nAddress); + if (nConvertedAddress != 0xFFFFFFFF) + nAddress = nConvertedAddress; return nAddress; } @@ -322,9 +314,9 @@ void CodeNotesModel::AddCodeNote(ra::ByteAddress nAddress, const std::string& sA try { if (sNextNote.length() > 2 && sNextNote.at(1) == 'x') - offsetNote.Offset = gsl::narrow_cast(std::wcstol(sNextNote.c_str() + 2, &pEnd, 16)); + offsetNote.Offset = gsl::narrow_cast(std::wcstoll(sNextNote.c_str() + 2, &pEnd, 16)); else - offsetNote.Offset = gsl::narrow_cast(std::wcstol(sNextNote.c_str(), &pEnd, 10)); + offsetNote.Offset = gsl::narrow_cast(std::wcstoll(sNextNote.c_str(), &pEnd, 10)); } catch (const std::exception&) { @@ -367,9 +359,33 @@ void CodeNotesModel::AddCodeNote(ra::ByteAddress nAddress, const std::string& sA pointerNote.Bytes = 4; } - // capture the initial value of the pointer const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - const auto nPointerValue = ReadPointer(pEmulatorContext, nAddress, pointerNote.MemSize); + + // assume anything annotated as a 32-bit pointer will read a real (non-translated) address and + // flag it to be converted to an RA address when evaluating indirect notes in DoFrame() + if (pointerNote.MemSize == MemSize::ThirtyTwoBit || + pointerNote.MemSize == MemSize::ThirtyTwoBitBigEndian) + { + const auto nMaxAddress = pEmulatorContext.TotalMemorySize(); + + pointerData->OffsetType = OffsetType::Converted; + + // if any offset exceeds the available memory for the system, assume the user is leveraging + // overflow math instead of masking, and don't attempt to translate the addresses. + for (const auto& pNote : pointerData->OffsetNotes) + { + if (ra::to_unsigned(pNote.Offset) >= nMaxAddress) + { + pointerData->OffsetType = OffsetType::Overflow; + break; + } + } + } + + // capture the initial value of the pointer + pointerData->RawPointerValue = pEmulatorContext.ReadMemory(nAddress, pointerNote.MemSize); + const auto nPointerValue = (pointerData->OffsetType == OffsetType::Converted) + ? ConvertPointer(pointerData->RawPointerValue) : pointerData->RawPointerValue; pointerData->PointerValue = nPointerValue; pointerNote.PointerData = std::move(pointerData); @@ -451,14 +467,18 @@ ra::ByteAddress CodeNotesModel::FindCodeNoteStart(ra::ByteAddress nAddress) cons if (pCodeNote.second.PointerData == nullptr) continue; - if (nAddress >= pCodeNote.second.PointerData->PointerValue && - nAddress < pCodeNote.second.PointerData->PointerValue + pCodeNote.second.PointerData->OffsetRange) + const auto nPointerValue = pCodeNote.second.PointerData->PointerValue; + const auto nConvertedPointerValue = (pCodeNote.second.PointerData->OffsetType == OffsetType::Overflow) + ? ConvertPointer(nPointerValue) : nPointerValue; + + if (nAddress >= nConvertedPointerValue && + nAddress < nConvertedPointerValue + pCodeNote.second.PointerData->OffsetRange) { - const auto nOffset = ra::to_signed(nAddress - pCodeNote.second.PointerData->PointerValue); + const auto nOffset = ra::to_signed(nAddress - nPointerValue); for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) { if (pOffsetNote.Offset <= nOffset && pOffsetNote.Offset + ra::to_signed(pOffsetNote.Bytes) > nOffset) - return pCodeNote.second.PointerData->PointerValue + pOffsetNote.Offset; + return nPointerValue + pOffsetNote.Offset; } } } @@ -540,9 +560,13 @@ std::wstring CodeNotesModel::FindCodeNote(ra::ByteAddress nAddress, MemSize nSiz if (!pIter2.second.PointerData) continue; - if (nLastAddress >= pIter2.second.PointerData->PointerValue) + const auto nPointerValue = pIter2.second.PointerData->PointerValue; + const auto nConvertedPointerValue = (pIter2.second.PointerData->OffsetType == OffsetType::Overflow) + ? ConvertPointer(nPointerValue) : nPointerValue; + + if (nLastAddress >= nConvertedPointerValue) { - const auto nOffset = ra::to_signed(nAddress - pIter2.second.PointerData->PointerValue); + const auto nOffset = ra::to_signed(nAddress - nPointerValue); const auto nLastOffset = nOffset + ra::to_signed(nCheckBytes) - 1; for (const auto& pNote : pIter2.second.PointerData->OffsetNotes) { @@ -657,29 +681,40 @@ const CodeNotesModel::CodeNote* CodeNotesModel::FindCodeNoteInternal(ra::ByteAdd return &pIter->second; if (m_bHasPointers) + return FindIndirectCodeNoteInternal(nAddress).second; + + return nullptr; +} + +std::pair + CodeNotesModel::FindIndirectCodeNoteInternal(ra::ByteAddress nAddress) const +{ + for (const auto& pCodeNote : m_mCodeNotes) { - for (const auto& pCodeNote : m_mCodeNotes) - { - if (pCodeNote.second.PointerData == nullptr) - continue; + if (pCodeNote.second.PointerData == nullptr) + continue; - if (nAddress >= pCodeNote.second.PointerData->PointerValue && - nAddress < pCodeNote.second.PointerData->PointerValue + pCodeNote.second.PointerData->OffsetRange) + // if the pointer address was not converted, do so now. + const auto nPointerValue = pCodeNote.second.PointerData->PointerValue; + const auto nConvertedPointerValue = (pCodeNote.second.PointerData->OffsetType == OffsetType::Overflow) + ? ConvertPointer(nPointerValue) : nPointerValue; + + if (nAddress >= nConvertedPointerValue && + nAddress < nConvertedPointerValue + pCodeNote.second.PointerData->OffsetRange) + { + const auto nOffset = ra::to_signed(nAddress - nPointerValue); + for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) { - const auto nOffset = ra::to_signed(nAddress - pCodeNote.second.PointerData->PointerValue); - for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) - { - if (pOffsetNote.Offset == nOffset) - return &pOffsetNote; - } + if (pOffsetNote.Offset == nOffset) + return {pCodeNote.first, &pOffsetNote}; } } } - return nullptr; + return {0, nullptr}; } -const std::wstring* CodeNotesModel::FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const noexcept +const std::wstring* CodeNotesModel::FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const { if (!m_bHasPointers) return nullptr; @@ -691,12 +726,26 @@ const std::wstring* CodeNotesModel::FindIndirectCodeNote(ra::ByteAddress nAddres if (nAddress == pCodeNote.first) { + // look for the offset directly for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) { if (pOffsetNote.Offset == ra::to_signed(nOffset)) return &pOffsetNote.Note; } + if (pCodeNote.second.PointerData->OffsetType == OffsetType::Overflow) + { + // direct offset not found, look for converted offset + const auto nConvertedAddress = ConvertPointer(pCodeNote.second.PointerData->RawPointerValue); + nOffset += nConvertedAddress - pCodeNote.second.PointerData->RawPointerValue; + + for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) + { + if (pOffsetNote.Offset == ra::to_signed(nOffset)) + return &pOffsetNote.Note; + } + } + break; } } @@ -704,26 +753,13 @@ const std::wstring* CodeNotesModel::FindIndirectCodeNote(ra::ByteAddress nAddres return nullptr; } -ra::ByteAddress CodeNotesModel::GetIndirectSource(ra::ByteAddress nAddress) const noexcept +ra::ByteAddress CodeNotesModel::GetIndirectSource(ra::ByteAddress nAddress) const { if (m_bHasPointers) { - for (const auto& pCodeNote : m_mCodeNotes) - { - if (pCodeNote.second.PointerData == nullptr) - continue; - - if (nAddress >= pCodeNote.second.PointerData->PointerValue && - nAddress < pCodeNote.second.PointerData->PointerValue + pCodeNote.second.PointerData->OffsetRange) - { - const auto nOffset = ra::to_signed(nAddress - pCodeNote.second.PointerData->PointerValue); - for (const auto& pOffsetNote : pCodeNote.second.PointerData->OffsetNotes) - { - if (pOffsetNote.Offset == nOffset) - return pCodeNote.first; - } - } - } + const auto pCodeNote = FindIndirectCodeNoteInternal(nAddress); + if (pCodeNote.second != nullptr) + return pCodeNote.first; } return 0xFFFFFFFF; @@ -745,15 +781,19 @@ ra::ByteAddress CodeNotesModel::GetNextNoteAddress(ra::ByteAddress nAfterAddress if (!pNote.second.PointerData) continue; - if (pNote.second.PointerData->PointerValue > nBestAddress) + const auto nPointerValue = pNote.second.PointerData->PointerValue; + const auto nConvertedPointerValue = (pNote.second.PointerData->OffsetType == OffsetType::Overflow) + ? ConvertPointer(nPointerValue) : nPointerValue; + + if (nConvertedPointerValue > nBestAddress) continue; - if (pNote.second.PointerData->PointerValue + pNote.second.PointerData->OffsetRange < nAfterAddress) + if (nConvertedPointerValue + pNote.second.PointerData->OffsetRange < nAfterAddress) continue; for (const auto& pOffset : pNote.second.PointerData->OffsetNotes) { - const auto pOffsetAddress = pNote.second.PointerData->PointerValue + pOffset.Offset; + const auto pOffsetAddress = nPointerValue + pOffset.Offset; if (pOffsetAddress > nAfterAddress) { nBestAddress = std::min(nBestAddress, pOffsetAddress); @@ -793,15 +833,19 @@ ra::ByteAddress CodeNotesModel::GetPreviousNoteAddress(ra::ByteAddress nBeforeAd if (!pNote.second.PointerData) continue; - if (pNote.second.PointerData->PointerValue > nBeforeAddress) + const auto nPointerValue = pNote.second.PointerData->PointerValue; + const auto nConvertedPointerValue = (pNote.second.PointerData->OffsetType == OffsetType::Overflow) + ? ConvertPointer(nPointerValue) : nPointerValue; + + if (nConvertedPointerValue > nBeforeAddress) continue; - if (pNote.second.PointerData->PointerValue + pNote.second.PointerData->OffsetRange < nBestAddress) + if (nConvertedPointerValue + pNote.second.PointerData->OffsetRange < nBestAddress) continue; for (const auto& pOffset : pNote.second.PointerData->OffsetNotes) { - const auto pOffsetAddress = pNote.second.PointerData->PointerValue + pOffset.Offset; + const auto pOffsetAddress = nPointerValue + pOffset.Offset; if (pOffsetAddress >= nBeforeAddress) break; @@ -835,8 +879,9 @@ void CodeNotesModel::EnumerateCodeNotes(std::functionPointerValue; for (const auto& pNote : pIter.second.PointerData->OffsetNotes) - mNotes[pIter.second.PointerData->PointerValue + pNote.Offset] = &pNote; + mNotes[nPointerValue + pNote.Offset] = &pNote; } // merge in the non-pointer notes @@ -863,7 +908,13 @@ void CodeNotesModel::DoFrame() if (!pNote.second.PointerData) continue; - const auto nNewAddress = ReadPointer(pEmulatorContext, pNote.first, pNote.second.MemSize); + const auto nNewRawAddress = pEmulatorContext.ReadMemory(pNote.first, pNote.second.MemSize); + if (nNewRawAddress == pNote.second.PointerData->RawPointerValue) + continue; + pNote.second.PointerData->RawPointerValue = nNewRawAddress; + + const auto nNewAddress = (pNote.second.PointerData->OffsetType == OffsetType::Converted) + ? ConvertPointer(nNewRawAddress) : nNewRawAddress; const auto nOldAddress = pNote.second.PointerData->PointerValue; if (nNewAddress == nOldAddress) diff --git a/src/data/models/CodeNotesModel.hh b/src/data/models/CodeNotesModel.hh index a65e4c6c..0bb3d861 100644 --- a/src/data/models/CodeNotesModel.hh +++ b/src/data/models/CodeNotesModel.hh @@ -72,7 +72,7 @@ public: /// Returns the note associated with the specified address. /// /// The note associated to the address, nullptr if no note is associated to the address. - const std::wstring* FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const noexcept; + const std::wstring* FindIndirectCodeNote(ra::ByteAddress nAddress, unsigned nOffset) const; /// /// Returns the number of bytes associated to the code note at the specified address. @@ -104,7 +104,7 @@ public: /// /// Returns 0xFFFFFFFF if not found, or not an indirect note. /// - ra::ByteAddress GetIndirectSource(ra::ByteAddress nAddress) const noexcept; + ra::ByteAddress GetIndirectSource(ra::ByteAddress nAddress) const; /// /// Returns the address of the next code note after the provided address. @@ -209,11 +209,19 @@ protected: int Offset = 0; }; + enum OffsetType + { + None = 0, + Converted, + Overflow, + }; + struct PointerData { ra::ByteAddress RawPointerValue = 0; ra::ByteAddress PointerValue = 0; unsigned int OffsetRange = 0; + OffsetType OffsetType = OffsetType::None; std::vector OffsetNotes; }; @@ -223,7 +231,9 @@ protected: std::map m_mPendingCodeNotes; const CodeNote* FindCodeNoteInternal(ra::ByteAddress nAddress) const; - void EnumerateCodeNotes(std::function callback, bool bIncludeDerived) const; + std::pair FindIndirectCodeNoteInternal(ra::ByteAddress nAddress) const; + void EnumerateCodeNotes(std::function callback, + bool bIncludeDerived) const; unsigned int m_nGameId = 0; bool m_bHasPointers = false; diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp index 8cc2bc2b..bc6f7cdd 100644 --- a/src/ui/viewmodels/TriggerConditionViewModel.cpp +++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp @@ -40,6 +40,9 @@ const BoolModelProperty TriggerConditionViewModel::HasHitsProperty("TriggerCondi const BoolModelProperty TriggerConditionViewModel::CanEditHitsProperty("TriggerConditionViewModel", "CanEditHits", true); const IntModelProperty TriggerConditionViewModel::RowColorProperty("TriggerConditionViewModel", "RowColor", 0); +constexpr ra::ByteAddress UNKNOWN_ADDRESS = 0xFFFFFFFF; +constexpr ra::ByteAddress NESTED_POINTER_ADDRESS = 0xFFFFFFFE; + std::string TriggerConditionViewModel::Serialize() const { std::string buffer; @@ -629,7 +632,7 @@ static bool IsIndirectMemref(const rc_operand_t& operand) noexcept ra::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::ByteAddress nAddress, ra::ByteAddress* pPointerAddress) const { if (pPointerAddress != nullptr) - *pPointerAddress = 0xFFFFFFFF; + *pPointerAddress = UNKNOWN_ADDRESS; const auto* pTriggerViewModel = dynamic_cast(m_pTriggerViewModel); if (pTriggerViewModel == nullptr) @@ -672,6 +675,7 @@ ra::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::ByteAddress nA } bool bIsIndirect = false; + bool bIsMultiLevelIndirect = false; rc_eval_state_t oEvalState; memset(&oEvalState, 0, sizeof(oEvalState)); rc_typed_value_t value = {}; @@ -685,10 +689,17 @@ ra::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::ByteAddress nA break; if (vmCondition == this) + { + if (bIsMultiLevelIndirect && pPointerAddress != nullptr) + *pPointerAddress = NESTED_POINTER_ADDRESS; + return nAddress + oEvalState.add_address; + } if (pCondition->type == RC_CONDITION_ADD_ADDRESS) { + bIsMultiLevelIndirect = bIsIndirect; + if (bIsIndirect && pTrigger == nullptr) { // if this is part of a chain, we have to create a copy of the condition so we can point @@ -720,14 +731,14 @@ ra::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::ByteAddress nA oEvalState.add_address = value.value.u32; if (pPointerAddress != nullptr && rc_operand_is_memref(&pCondition->operand1)) - *pPointerAddress = (bIsIndirect) ? 0xFFFFFFFF : pCondition->operand1.value.memref->address; + *pPointerAddress = (bIsIndirect) ? NESTED_POINTER_ADDRESS : pCondition->operand1.value.memref->address; bIsIndirect = true; } } else { - bIsIndirect = false; + bIsIndirect = bIsMultiLevelIndirect = false; oEvalState.add_address = 0; } } @@ -746,11 +757,14 @@ std::wstring TriggerConditionViewModel::GetAddressTooltip(ra::ByteAddress nAddre if (pCodeNotes == nullptr) return ra::StringPrintf(L"%s%s\r\n[No code note]", ra::ByteAddressToString(nAddress), nOffset ? L" (indirect)" : L""); + if (nPointerAddress == NESTED_POINTER_ADDRESS) + return ra::StringPrintf(L"%s (indirect)\r\n[Nested pointer code note not supported]", ra::ByteAddressToString(nAddress)); + if (nOffset) { sAddress = ra::StringPrintf(L"%s (indirect)", ra::ByteAddressToString(nAddress)); - if (nPointerAddress != 0xFFFFFFFF) + if (nPointerAddress != UNKNOWN_ADDRESS) pNote = pCodeNotes->FindIndirectCodeNote(nPointerAddress, nOffset); else pNote = pCodeNotes->FindCodeNote(nAddress); @@ -763,8 +777,8 @@ std::wstring TriggerConditionViewModel::GetAddressTooltip(ra::ByteAddress nAddre if (!pNote) { - const auto nStartAddress = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNoteStart(nAddress) : 0xFFFFFFFF; - if (nStartAddress != nAddress && nStartAddress != 0xFFFFFFFF) + const auto nStartAddress = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNoteStart(nAddress) : UNKNOWN_ADDRESS; + if (nStartAddress != nAddress && nStartAddress != UNKNOWN_ADDRESS) { sAddress = ra::StringPrintf(L"%s [%s+%d]%s", ra::ByteAddressToString(nAddress), ra::ByteAddressToString(nStartAddress), nAddress - nStartAddress, diff --git a/tests/data/models/CodeNotesModel_Tests.cpp b/tests/data/models/CodeNotesModel_Tests.cpp index 205d7c26..ad310c00 100644 --- a/tests/data/models/CodeNotesModel_Tests.cpp +++ b/tests/data/models/CodeNotesModel_Tests.cpp @@ -240,6 +240,7 @@ TEST_CLASS(CodeNotesModel_Tests) TestCodeNoteSize(L"[float bigendian] Test", 4U, MemSize::FloatBigEndian); TestCodeNoteSize(L"[be float] Test", 4U, MemSize::FloatBigEndian); TestCodeNoteSize(L"[bigendian float] Test", 4U, MemSize::FloatBigEndian); + TestCodeNoteSize(L"[32-bit] pointer to float", 4U, MemSize::ThirtyTwoBit); TestCodeNoteSize(L"[MBF32] Test", 4U, MemSize::MBF32); TestCodeNoteSize(L"[MBF40] Test", 5U, MemSize::MBF32); @@ -677,6 +678,46 @@ TEST_CLASS(CodeNotesModel_Tests) Assert::AreEqual(std::wstring(), notes.FindCodeNote(18, MemSize::SixteenBit)); } + + TEST_METHOD(TestFindCodeNoteSizedPointerOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x88; // start with initial value for pointer (real address = 0x88, RA address = 0x08) + + const std::wstring sPointerNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF88 = Small (8-bit)\n" // 8+8=16 + L"+0xFFFFFF90 = Medium (16-bit)\n" // 16+8=24 + L"+0xFFFFFF98 = Large (32-bit)"; // 24+8=32 + notes.AddCodeNote(4, "Author", sPointerNote); + notes.AddCodeNote(40, "Author", L"After [32-bit]"); + notes.AddCodeNote(1, "Author", L"Before"); + notes.AddCodeNote(20, "Author", L"In the middle"); + + Assert::AreEqual(std::wstring(), notes.FindCodeNote(0, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Before"), notes.FindCodeNote(1, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Pointer (32-bit) [1/4]"), notes.FindCodeNote(4, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Small (8-bit) [indirect]"), notes.FindCodeNote(16, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Medium (16-bit) [1/2] [indirect]"), notes.FindCodeNote(24, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Medium (16-bit) [2/2] [indirect]"), notes.FindCodeNote(25, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Large (32-bit) [1/4] [indirect]"), notes.FindCodeNote(32, MemSize::EightBit)); + Assert::AreEqual(std::wstring(L"Large (32-bit) [4/4] [indirect]"), notes.FindCodeNote(35, MemSize::EightBit)); + Assert::AreEqual(std::wstring(), notes.FindCodeNote(36, MemSize::EightBit)); + + Assert::AreEqual(std::wstring(L"Before [partial]"), notes.FindCodeNote(0, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Before [partial]"), notes.FindCodeNote(1, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Pointer (32-bit) [partial]"), notes.FindCodeNote(4, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Small (8-bit) [partial] [indirect]"), notes.FindCodeNote(16, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Medium (16-bit) [indirect]"), notes.FindCodeNote(24, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Medium (16-bit) [partial] [indirect]"), notes.FindCodeNote(25, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Large (32-bit) [partial] [indirect]"), notes.FindCodeNote(32, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(L"Large (32-bit) [partial] [indirect]"), notes.FindCodeNote(35, MemSize::SixteenBit)); + Assert::AreEqual(std::wstring(), notes.FindCodeNote(36, MemSize::SixteenBit)); + } + TEST_METHOD(TestFindIndirectCodeNote) { CodeNotesModelHarness notes; @@ -697,6 +738,28 @@ TEST_CLASS(CodeNotesModel_Tests) Assert::IsNull(notes.FindIndirectCodeNote(1235U, 5)); // wrong base address } + TEST_METHOD(TestFindIndirectCodeNoteOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x90; // start with initial value for pointer (real address = 0x90, RA address = 0x10) + + const std::wstring sNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF81 = Small (8-bit)\n" + L"+0xFFFFFF82 = Medium (16-bit)\n" + L"+0xFFFFFF84 = Large (32-bit)"; + notes.AddCodeNote(4, "Author", sNote); + + notes.AssertIndirectNote(4U, 0x01, L"Small (8-bit)"); + notes.AssertIndirectNote(4U, 0x04, L"Large (32-bit)"); + Assert::IsNull(notes.FindIndirectCodeNote(4U, 0x00)); // no offset + Assert::IsNull(notes.FindIndirectCodeNote(4U, 0x30)); // unknown offset + Assert::IsNull(notes.FindIndirectCodeNote(8U, 0x01)); // wrong base address + } + TEST_METHOD(TestEnumerateCodeNotes) { CodeNotesModelHarness notes; @@ -794,6 +857,71 @@ TEST_CLASS(CodeNotesModel_Tests) return true; }, true); } + + TEST_METHOD(TestEnumerateCodeNotesWithIndirectOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x88; // start with initial value for pointer (real address = 0x88, RA address = 0x08) + + const std::wstring sPointerNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF88 = Small (8-bit)\n" // 8+8=16 + L"+0xFFFFFF90 = Medium (16-bit)\n" // 16+8=24 + L"+0xFFFFFF98 = Large (32-bit)"; // 24+8=32 + notes.AddCodeNote(4, "Author", sPointerNote); + notes.AddCodeNote(40, "Author", L"After [32-bit]"); + notes.AddCodeNote(1, "Author", L"Before"); + notes.AddCodeNote(20, "Author", L"In the middle"); + + int i = 0; + notes.EnumerateCodeNotes([&i, &sPointerNote](ra::ByteAddress nAddress, unsigned nBytes, const std::wstring& sNote) { + switch (i++) + { + case 0: + Assert::AreEqual({1U}, nAddress); + Assert::AreEqual(1U, nBytes); + Assert::AreEqual(std::wstring(L"Before"), sNote); + break; + case 1: + Assert::AreEqual({4U}, nAddress); + Assert::AreEqual(4U, nBytes); + Assert::AreEqual(sPointerNote, sNote); + break; + case 2: + Assert::AreEqual({16U}, nAddress); + Assert::AreEqual(1U, nBytes); + Assert::AreEqual(std::wstring(L"Small (8-bit)"), sNote); + break; + case 3: + Assert::AreEqual({20U}, nAddress); + Assert::AreEqual(1U, nBytes); + Assert::AreEqual(std::wstring(L"In the middle"), sNote); + break; + case 4: + Assert::AreEqual({24U}, nAddress); + Assert::AreEqual(2U, nBytes); + Assert::AreEqual(std::wstring(L"Medium (16-bit)"), sNote); + break; + case 5: + Assert::AreEqual({32}, nAddress); + Assert::AreEqual(4U, nBytes); + Assert::AreEqual(std::wstring(L"Large (32-bit)"), sNote); + break; + case 6: + Assert::AreEqual({40}, nAddress); + Assert::AreEqual(4U, nBytes); + Assert::AreEqual(std::wstring(L"After [32-bit]"), sNote); + break; + case 7: + Assert::Fail(L"Too many notes"); + break; + } + return true; + }, true); + } TEST_METHOD(TestDoFrame) { @@ -913,6 +1041,100 @@ TEST_CLASS(CodeNotesModel_Tests) notes.AssertNote(0x0AU, L"Medium (16-bit)", MemSize::SixteenBit, 2); } + TEST_METHOD(TestDoFrameRealAddressConversionBigEndian) + { + CodeNotesModelHarness notes; + notes.MonitorCodeNoteChanges(); + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + + std::array memory{}; + for (uint8_t i = 4; i < memory.size(); i++) + memory.at(i) = i; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(3) = 0x90; // start with initial value for pointer (real address = 0x90, RA address = 0x10) + + const std::wstring sNote = + L"Pointer (32-bit BE)\n" // only 32-bit pointers are eligible for real address conversion + L"+1 = Small (8-bit)\n" + L"+2 = Medium (16-bit)\n" + L"+4 = Large (32-bit)"; + notes.AddCodeNote(0x0000, "Author", sNote); + + // should receive notifications for the pointer note, and for each subnote + Assert::AreEqual({4U}, notes.mNewNotes.size()); + Assert::AreEqual(sNote, notes.mNewNotes[0x00]); + Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x11]); + Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x12]); + Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x14]); + + notes.AssertNoNote(0x02U); + notes.AssertNote(0x12U, L"Medium (16-bit)", MemSize::SixteenBit, 2); + + // calling DoFrame after updating the pointer should notify about all the affected subnotes + notes.mNewNotes.clear(); + memory.at(3) = 0x88; + notes.DoFrame(); + + Assert::AreEqual({6U}, notes.mNewNotes.size()); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x11]); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x12]); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x14]); + Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x09]); + Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x0A]); + Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x0C]); + + notes.AssertNoNote(0x02U); + notes.AssertNoNote(0x12U); + notes.AssertNote(0x0AU, L"Medium (16-bit)", MemSize::SixteenBit, 2); + } + + TEST_METHOD(TestDoFrameRealAddressConversionAvoidedByOverflow) + { + CodeNotesModelHarness notes; + notes.MonitorCodeNoteChanges(); + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + + std::array memory{}; + for (uint8_t i = 4; i < memory.size(); i++) + memory.at(i) = i; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(0) = 0x90; // start with initial value for pointer (real address = 0x90, RA address = 0x10) + + const std::wstring sNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF81 = Small (8-bit)\n" + L"+0xFFFFFF82 = Medium (16-bit)\n" + L"+0xFFFFFF84 = Large (32-bit)"; + notes.AddCodeNote(0x0000, "Author", sNote); + + // should receive notifications for the pointer note, and for each subnote + Assert::AreEqual({4U}, notes.mNewNotes.size()); + Assert::AreEqual(sNote, notes.mNewNotes[0x00]); + Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x11]); + Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x12]); + Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x14]); + + notes.AssertNoNote(0x02U); + notes.AssertNote(0x12U, L"Medium (16-bit)", MemSize::SixteenBit, 2); + + // calling DoFrame after updating the pointer should notify about all the affected subnotes + notes.mNewNotes.clear(); + memory.at(0) = 0x88; + notes.DoFrame(); + + Assert::AreEqual({6U}, notes.mNewNotes.size()); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x11]); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x12]); + Assert::AreEqual(std::wstring(L""), notes.mNewNotes[0x14]); + Assert::AreEqual(std::wstring(L"Small (8-bit)"), notes.mNewNotes[0x09]); + Assert::AreEqual(std::wstring(L"Medium (16-bit)"), notes.mNewNotes[0x0A]); + Assert::AreEqual(std::wstring(L"Large (32-bit)"), notes.mNewNotes[0x0C]); + + notes.AssertNoNote(0x02U); + notes.AssertNoNote(0x12U); + notes.AssertNote(0x0AU, L"Medium (16-bit)", MemSize::SixteenBit, 2); + } + TEST_METHOD(TestFindCodeNoteStartPointer) { CodeNotesModelHarness notes; @@ -943,6 +1165,36 @@ TEST_CLASS(CodeNotesModel_Tests) Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(0x18)); } + TEST_METHOD(TestFindCodeNoteStartPointerOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x88; // start with initial value for pointer (real address = 0x88, RA address = 0x08) + + const std::wstring sPointerNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF88 = Small (8-bit)\n" // 8+8=16 + L"+0xFFFFFF90 = Medium (16-bit)\n" // 16+8=24 + L"+0xFFFFFF98 = Large (32-bit)"; // 24+8=32 + notes.AddCodeNote(4, "Author", sPointerNote); + + // indirect notes are at 16 (byte), 24 (word), and 32 (dword) + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(15)); + Assert::AreEqual(16U, notes.FindCodeNoteStart(16)); + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(17)); + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(23)); + Assert::AreEqual(24U, notes.FindCodeNoteStart(24)); + Assert::AreEqual(24U, notes.FindCodeNoteStart(25)); + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(31)); + Assert::AreEqual(32U, notes.FindCodeNoteStart(32)); + Assert::AreEqual(32U, notes.FindCodeNoteStart(33)); + Assert::AreEqual(32U, notes.FindCodeNoteStart(34)); + Assert::AreEqual(32U, notes.FindCodeNoteStart(35)); + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(36)); + } + TEST_METHOD(TestGetIndirectSource) { CodeNotesModelHarness notes; @@ -976,6 +1228,37 @@ TEST_CLASS(CodeNotesModel_Tests) // non-indirect Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x08)); } + + TEST_METHOD(TestGetIndirectSourceOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x90; // start with initial value for pointer (real address = 0x90, RA address = 0x10) + + const std::wstring sNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF81 = Small (8-bit)\n" + L"+0xFFFFFF82 = Medium (16-bit)\n" + L"+0xFFFFFF84 = Large (32-bit)"; + notes.AddCodeNote(0x0004, "Author", sNote); + notes.AddCodeNote(0x0008, "Author", L"Not indirect"); + + // indirect notes are at 0x11 (byte), 0x12 (word), and 0x14 (dword) + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x10)); + Assert::AreEqual(0x4U, notes.GetIndirectSource(0x11)); + Assert::AreEqual(0x4U, notes.GetIndirectSource(0x12)); + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x13)); + Assert::AreEqual(0x4U, notes.GetIndirectSource(0x14)); + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x15)); + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x16)); + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x17)); + Assert::AreEqual(0xFFFFFFFF, notes.FindCodeNoteStart(0x18)); + + // non-indirect + Assert::AreEqual(0xFFFFFFFF, notes.GetIndirectSource(0x08)); + } TEST_METHOD(TestSetServerCodeNote) { @@ -1130,6 +1413,40 @@ TEST_CLASS(CodeNotesModel_Tests) Assert::AreEqual({0xFFFFFFFFU}, notes.GetNextNoteAddress({1234U}, true)); } + TEST_METHOD(TestGetNextNoteAddressOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x88; // start with initial value for pointer (real address = 0x88, RA address = 0x08) + + const std::wstring sPointerNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF88 = Small (8-bit)\n" // 8+8=16 + L"+0xFFFFFF90 = Medium (16-bit)\n" // 16+8=24 + L"+0xFFFFFF98 = Large (32-bit)"; // 24+8=32 + notes.AddCodeNote(4, "Author", sPointerNote); + notes.AddCodeNote(40, "Author", L"After [32-bit]"); + notes.AddCodeNote(1, "Author", L"Before"); + notes.AddCodeNote(20, "Author", L"In the middle"); + + Assert::AreEqual({1U}, notes.GetNextNoteAddress({0U})); + Assert::AreEqual({4U}, notes.GetNextNoteAddress({1U})); + Assert::AreEqual({20U}, notes.GetNextNoteAddress({4U})); + Assert::AreEqual({40U}, notes.GetNextNoteAddress({20U})); + Assert::AreEqual({0xFFFFFFFFU}, notes.GetNextNoteAddress({40U})); + + Assert::AreEqual({1U}, notes.GetNextNoteAddress({0U}, true)); + Assert::AreEqual({4U}, notes.GetNextNoteAddress({1U}, true)); + Assert::AreEqual({16U}, notes.GetNextNoteAddress({4U}, true)); + Assert::AreEqual({20U}, notes.GetNextNoteAddress({16U}, true)); + Assert::AreEqual({24U}, notes.GetNextNoteAddress({20U}, true)); + Assert::AreEqual({32U}, notes.GetNextNoteAddress({24U}, true)); + Assert::AreEqual({40U}, notes.GetNextNoteAddress({32U}, true)); + Assert::AreEqual({0xFFFFFFFFU}, notes.GetNextNoteAddress({40U}, true)); + } + TEST_METHOD(TestGetPreviousNoteAddress) { CodeNotesModelHarness notes; @@ -1156,6 +1473,40 @@ TEST_CLASS(CodeNotesModel_Tests) Assert::AreEqual({4U}, notes.GetPreviousNoteAddress({8U}, true)); Assert::AreEqual({0xFFFFFFFFU}, notes.GetPreviousNoteAddress({4U}, true)); } + + TEST_METHOD(TestGetPreviousNoteAddressOverflow) + { + CodeNotesModelHarness notes; + notes.mockConsoleContext.AddMemoryRegion(0, 31, ra::data::context::ConsoleContext::AddressType::SystemRAM, 0x80); + std::array memory{}; + notes.mockEmulatorContext.MockMemory(memory); + memory.at(4) = 0x88; // start with initial value for pointer (real address = 0x88, RA address = 0x08) + + const std::wstring sPointerNote = + L"Pointer (32-bit)\n" // only 32-bit pointers are eligible for real address conversion + L"+0xFFFFFF88 = Small (8-bit)\n" // 8+8=16 + L"+0xFFFFFF90 = Medium (16-bit)\n" // 16+8=24 + L"+0xFFFFFF98 = Large (32-bit)"; // 24+8=32 + notes.AddCodeNote(4, "Author", sPointerNote); + notes.AddCodeNote(40, "Author", L"After [32-bit]"); + notes.AddCodeNote(1, "Author", L"Before"); + notes.AddCodeNote(20, "Author", L"In the middle"); + + Assert::AreEqual({40U}, notes.GetPreviousNoteAddress({0xFFFFFFFFU})); + Assert::AreEqual({20U}, notes.GetPreviousNoteAddress({40U})); + Assert::AreEqual({4U}, notes.GetPreviousNoteAddress({20U})); + Assert::AreEqual({1U}, notes.GetPreviousNoteAddress({4U})); + Assert::AreEqual({0xFFFFFFFFU}, notes.GetPreviousNoteAddress({1U})); + + Assert::AreEqual({40U}, notes.GetPreviousNoteAddress({0xFFFFFFFFU}, true)); + Assert::AreEqual({32U}, notes.GetPreviousNoteAddress({40U}, true)); + Assert::AreEqual({24U}, notes.GetPreviousNoteAddress({32U}, true)); + Assert::AreEqual({20U}, notes.GetPreviousNoteAddress({24U}, true)); + Assert::AreEqual({16U}, notes.GetPreviousNoteAddress({20U}, true)); + Assert::AreEqual({4U}, notes.GetPreviousNoteAddress({16U}, true)); + Assert::AreEqual({1U}, notes.GetPreviousNoteAddress({4U}, true)); + Assert::AreEqual({0xFFFFFFFFU}, notes.GetPreviousNoteAddress({1U}, true)); + } }; } // namespace tests diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp index 8c9699b7..db944cbf 100644 --- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp +++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp @@ -773,12 +773,65 @@ TEST_CLASS(TriggerConditionViewModel_Tests) Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); } + TEST_METHOD(TestTooltipIndirectAddressCodeNoteOverflow) + { + IndirectAddressTriggerViewModelHarness vmTrigger; + ra::data::context::mocks::MockConsoleContext mockConsoleContext; + vmTrigger.Parse("I:0xX0000_0xH80000002=6"); + vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); + vmTrigger.mockGameContext.SetCodeNote({0U}, L"[32-bit pointer]\n+0x80000002=This is a note."); + + const auto* pCondition = vmTrigger.Conditions().GetItemAt(1); + Expects(pCondition != nullptr); + Assert::IsTrue(pCondition->IsIndirect()); + + // $0001 = 0x80000003, 0x80000003+0x80000002 = 0x100000005 (too big for uint32_t) -> truncated to $0005 + vmTrigger.SetMemory({0}, 0x03); + vmTrigger.SetMemory({1}, 0x00); + vmTrigger.SetMemory({2}, 0x00); + vmTrigger.SetMemory({3}, 0x80); + Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), + pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + + // $0001 = 0x80000004, 0x8000004+0x80000002 = 0x100000006 (too big for uint32_t) -> truncated to $0006 + vmTrigger.SetMemory({0}, 4); + Assert::AreEqual(std::wstring(L"0x0006 (indirect)\r\nThis is a note."), + pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + } + + TEST_METHOD(TestTooltipIndirectAddressCodeNoteMasked) + { + IndirectAddressTriggerViewModelHarness vmTrigger; + ra::data::context::mocks::MockConsoleContext mockConsoleContext; + vmTrigger.Parse("I:0xX0000&33554431_0xH0002=6"); // 33554431 = 0x01FFFFFF + vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); + vmTrigger.mockGameContext.SetCodeNote({0U}, L"[32-bit pointer]\n+2=This is a note."); + + const auto* pCondition = vmTrigger.Conditions().GetItemAt(1); + Expects(pCondition != nullptr); + Assert::IsTrue(pCondition->IsIndirect()); + + // $0001 = 0x80000003, 0x80000003&0x01FFFFFF=0x00000003+2 = 0x00000005 + vmTrigger.SetMemory({0}, 0x03); + vmTrigger.SetMemory({1}, 0x00); + vmTrigger.SetMemory({2}, 0x00); + vmTrigger.SetMemory({3}, 0x80); + Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nThis is a note."), + pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + + // $0001 = 0x80000004, 0x8000004+0x80000002 = 0x100000006 (too big for uint32_t) -> truncated to $0006 + vmTrigger.SetMemory({0}, 4); + Assert::AreEqual(std::wstring(L"0x0006 (indirect)\r\nThis is a note."), + pCondition->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + } + TEST_METHOD(TestTooltipDoubleIndirectAddress) { IndirectAddressTriggerViewModelHarness vmTrigger; vmTrigger.mockGameContext.InitializeCodeNotes(); vmTrigger.Parse("I:0xH0001_I:0xH0002_0xH0003=4"); vmTrigger.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); + vmTrigger.mockGameContext.SetCodeNote({1U}, L"[8-bit pointer]\n+2=First Level A\n +3=Second Level."); const auto* pCondition1 = vmTrigger.Conditions().GetItemAt(0); Expects(pCondition1 != nullptr); @@ -792,15 +845,15 @@ TEST_CLASS(TriggerConditionViewModel_Tests) Assert::IsTrue(pCondition3->IsIndirect()); // $0001 = 1, 1+2 = $0003, $0003 = 3, 3+3 = $0006 - Assert::AreEqual(std::wstring(L"0x0001\r\n[No code note]"), pCondition1->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); - Assert::AreEqual(std::wstring(L"0x0003 (indirect)\r\n[No code note]"), pCondition2->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); - Assert::AreEqual(std::wstring(L"0x0006 (indirect)\r\n[No code note]"), pCondition3->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0001\r\n[8-bit pointer]\n+2=First Level A\n +3=Second Level."), pCondition1->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0003 (indirect)\r\nFirst Level A\n +3=Second Level."), pCondition2->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0006 (indirect)\r\n[Nested pointer code note not supported]"), pCondition3->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); // $0001 = 3, 3+2 = $0005, $0005 = 5, 5+3 = $0008 vmTrigger.SetMemory({ 1 }, 3); - Assert::AreEqual(std::wstring(L"0x0001\r\n[No code note]"), pCondition1->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); - Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\n[No code note]"), pCondition2->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); - Assert::AreEqual(std::wstring(L"0x0008 (indirect)\r\n[No code note]"), pCondition3->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0001\r\n[8-bit pointer]\n+2=First Level A\n +3=Second Level."), pCondition1->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0005 (indirect)\r\nFirst Level A\n +3=Second Level."), pCondition2->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); + Assert::AreEqual(std::wstring(L"0x0008 (indirect)\r\n[Nested pointer code note not supported]"), pCondition3->GetTooltip(TriggerConditionViewModel::SourceValueProperty)); } TEST_METHOD(TestTooltipDoubleIndirectAddressExternal)