From 0c8a55863486af8f6179e0a5c09f04ba80938b8e Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 21 Jun 2026 17:13:59 -0600 Subject: [PATCH 1/3] extract IndirectNoteResolver from TriggerConditionViewModel --- src/devkit/RADevKit.vcxproj | 2 + src/devkit/RADevKit.vcxproj.filters | 9 + src/devkit/data/util/IndirectNoteResolver.cpp | 272 ++++++++++++ src/devkit/data/util/IndirectNoteResolver.hh | 63 +++ .../viewmodels/TriggerConditionViewModel.cpp | 154 +------ tests/devkit/RADevKit.Tests.vcxproj | 1 + tests/devkit/RADevKit.Tests.vcxproj.filters | 6 + .../mocks/MockEmulatorMemoryContext.hh | 12 + .../data/models/RichPresenceModel_Tests.cpp | 11 +- .../data/util/IndirectNoteResolver_Tests.cpp | 391 ++++++++++++++++++ .../TriggerSummaryViewModel_Tests.cpp | 16 + 11 files changed, 795 insertions(+), 142 deletions(-) create mode 100644 src/devkit/data/util/IndirectNoteResolver.cpp create mode 100644 src/devkit/data/util/IndirectNoteResolver.hh create mode 100644 tests/devkit/data/util/IndirectNoteResolver_Tests.cpp diff --git a/src/devkit/RADevKit.vcxproj b/src/devkit/RADevKit.vcxproj index b72d6a91b..aaea74aa9 100644 --- a/src/devkit/RADevKit.vcxproj +++ b/src/devkit/RADevKit.vcxproj @@ -76,6 +76,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/src/devkit/RADevKit.vcxproj.filters b/src/devkit/RADevKit.vcxproj.filters index 05637bdb3..6b02e5c63 100644 --- a/src/devkit/RADevKit.vcxproj.filters +++ b/src/devkit/RADevKit.vcxproj.filters @@ -28,6 +28,9 @@ {3d6b142b-5f11-4326-a76e-1044b0d23859} + + {8d4e6f6e-e2a2-4798-9c67-691fd63d9d47} + @@ -201,6 +204,9 @@ data\models + + data\util + @@ -284,5 +290,8 @@ data\models + + data\util + \ No newline at end of file diff --git a/src/devkit/data/util/IndirectNoteResolver.cpp b/src/devkit/data/util/IndirectNoteResolver.cpp new file mode 100644 index 000000000..fc549cd61 --- /dev/null +++ b/src/devkit/data/util/IndirectNoteResolver.cpp @@ -0,0 +1,272 @@ +#include "IndirectNoteResolver.hh" + +#include "context/IEmulatorMemoryContext.hh" + +#include "services/ServiceLocator.hh" + +#include "util/Strings.hh" + +#include + +namespace ra { +namespace data { +namespace util { + +static uint32_t ResolveOperandRecursive(const rc_operand_t* pOperand, + std::vector& vParentChain, + const ra::data::models::MemoryNotesModel& pMemoryNotes) +{ + rc_typed_value_t pValue; + rc_evaluate_operand(&pValue, pOperand, nullptr); + rc_typed_value_convert(&pValue, RC_VALUE_TYPE_UNSIGNED); + + // check for {recall} + if (pOperand->type == RC_OPERAND_RECALL) + { + auto* pParentNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + + auto& pNode = vParentChain.emplace_back(); + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::Recall; + + if (pParentNote) + pNode.pNote = pParentNote->GetPointerNoteAtOffset(pValue.value.u32); + else + pNode.pNote = pMemoryNotes.FindMemoryNoteModel(pValue.value.u32, false); + + pNode.nValue = pValue.value.u32; + return pValue.value.u32; + } + + // check for constant offset + if (!rc_operand_is_memref(pOperand)) + { + auto* pParentNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + + auto& pNode = vParentChain.emplace_back(); + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::ConstantOffset; + + if (pParentNote) + pNode.pNote = pParentNote->GetPointerNoteAtOffset(pValue.value.u32); + + pNode.nValue = pValue.value.u32; + return pValue.value.u32; + } + + // check for root pointer + if (pOperand->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) + { + auto& pNode = vParentChain.emplace_back(); + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::Address; + pNode.nValue = pOperand->value.memref->address; + + // find the memory note associated to the root pointer + pNode.pNote = pMemoryNotes.FindMemoryNoteModel(pNode.nValue, false); + if (!pNode.pNote) + { + const auto nStartAddress = pMemoryNotes.FindNoteStart(pNode.nValue); + if (nStartAddress != 0xFFFFFFFF && nStartAddress != pNode.nValue) + { + const auto nOffset = pNode.nValue - nStartAddress; + pNode.nValue = nStartAddress; + const auto* pNote = pMemoryNotes.FindMemoryNoteModel(nStartAddress); + pNode.pNote = pNote; + + auto& pNode2 = vParentChain.emplace_back(); + pNode2.nType = ra::data::util::IndirectNoteResolver::NodeType::Constant; + pNode2.nValue = nOffset; + pNode2.nModifierType = RC_OPERATOR_ADD; + pNode2.pNote = pNote; + } + } + + return pValue.value.u32; + } + + // process offset and recurse + GSL_SUPPRESS_TYPE1 const auto* pModifiedMemref = + reinterpret_cast(pOperand->value.memref); + ResolveOperandRecursive(&pModifiedMemref->parent, vParentChain, pMemoryNotes); + + if (vParentChain.back().nType == ra::data::util::IndirectNoteResolver::NodeType::Address) + vParentChain.back().nType = ra::data::util::IndirectNoteResolver::NodeType::DereferencedAddress; + + // calculate current values of both operands + rc_evaluate_operand(&pValue, &pModifiedMemref->parent, nullptr); + rc_typed_value_convert(&pValue, RC_VALUE_TYPE_UNSIGNED); + + rc_typed_value_t pModifier{}; + rc_evaluate_operand(&pModifier, &pModifiedMemref->modifier, nullptr); + rc_typed_value_convert(&pModifier, RC_VALUE_TYPE_UNSIGNED); + + // if it's an indirect read, determine if it's a pointer or index offset + if (pModifiedMemref->modifier_type == RC_OPERATOR_INDIRECT_READ) + { + // value is the parent pointer, modifier is the offset. combine them to get the new address + rc_typed_value_combine(&pValue, &pModifier, RC_OPERATOR_ADD); + const auto nAddress = pModifier.value.u32; + + const auto* pParentNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + if (pParentNote && !pParentNote->IsPointer()) + { + // if the parent note is not a pointer, assume it's an index + Expects(pModifiedMemref->modifier.type == RC_OPERAND_CONST); + auto& pNode = vParentChain.emplace_back(); + pNode.nValue = nAddress; + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::ArrayOffset; + + // find the memory note associated to the start of the array + pNode.pNote = pMemoryNotes.FindMemoryNoteModel(nAddress, false); + + // return the address offset into the array + return pValue.value.u32; + } + + // process the pointer + ResolveOperandRecursive(&pModifiedMemref->modifier, vParentChain, pMemoryNotes); + vParentChain.back().nModifierType = RC_OPERATOR_INDIRECT_READ; + + return pValue.value.u32; + } + + // not an indirect read. must be a scalar. + + // don't report mask on every pointer evaluation + if (pModifiedMemref->modifier_type != RC_OPERATOR_AND) + { + const auto* pParentNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + + auto& pNode = vParentChain.emplace_back(); + pNode.nModifierType = pModifiedMemref->modifier_type; + pNode.pNote = pParentNote; + + if (pModifiedMemref->modifier.type == RC_OPERAND_RECALL) + { + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::Recall; + pNode.nValue = pModifier.value.u32; + } + else if (rc_operand_is_memref(&pModifiedMemref->modifier)) + { + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::Address; + pNode.nValue = pModifiedMemref->modifier.value.memref->address; + } + else + { + pNode.nType = ra::data::util::IndirectNoteResolver::NodeType::Constant; + pNode.nValue = pModifiedMemref->modifier.value.num; + } + } + + // return the result of combining the value and the modifier + rc_typed_value_combine(&pValue, &pModifier, pModifiedMemref->modifier_type); + return pValue.value.u32; +} + +ra::data::ByteAddress IndirectNoteResolver::ResolveOperand( + const struct rc_condition_t& pCondition, bool bLeafIsOperand1, + std::vector& vParentChain) const +{ + Expects(m_pMemoryNotes != nullptr); + + if (bLeafIsOperand1) + { + const auto* pOperand1 = rc_condition_get_real_operand1(&pCondition); + if (rc_operand_is_memref(pOperand1)) + return ResolveOperandRecursive(pOperand1, vParentChain, *m_pMemoryNotes); + } + else + { + if (rc_operand_is_memref(&pCondition.operand2)) + return ResolveOperandRecursive(&pCondition.operand2, vParentChain, *m_pMemoryNotes); + } + + return 0; +} + +std::wstring IndirectNoteResolver::BuildPath(const std::vector& vParentChain) const +{ + const auto& pMemoryContext = ra::services::ServiceLocator::Get(); + std::wstring sPointerChain; + + for (const auto& pNode : vParentChain) + { + switch (pNode.nModifierType) + { + case RC_OPERATOR_INDIRECT_READ: + case RC_OPERATOR_ADD: + sPointerChain.push_back(L'+'); + break; + case RC_OPERATOR_SUB: + sPointerChain.push_back(L'-'); + break; + case RC_OPERATOR_MULT: + sPointerChain.push_back(L'*'); + break; + case RC_OPERATOR_DIV: + sPointerChain.push_back(L'/'); + break; + case RC_OPERATOR_AND: + sPointerChain.push_back(L'&'); + break; + case RC_OPERATOR_XOR: + sPointerChain.push_back(L'^'); + break; + case RC_OPERATOR_MOD: + sPointerChain.push_back(L'%'); + break; + default: + break; + } + + switch (pNode.nType) + { + case ra::data::util::IndirectNoteResolver::NodeType::Recall: + sPointerChain += ra::util::String::Printf(L"{recall:0x%02x}", pNode.nValue); + break; + + case ra::data::util::IndirectNoteResolver::NodeType::ConstantOffset: + sPointerChain += ra::util::String::Printf(L"0x%02x", pNode.nValue); + break; + + case ra::data::util::IndirectNoteResolver::NodeType::Address: + sPointerChain += pMemoryContext.FormatAddress(pNode.nValue); + break; + + case ra::data::util::IndirectNoteResolver::NodeType::DereferencedAddress: + sPointerChain.push_back('$'); + sPointerChain += pMemoryContext.FormatAddress(pNode.nValue); + break; + + case ra::data::util::IndirectNoteResolver::NodeType::ArrayOffset: + sPointerChain.insert(0, ra::util::String::Printf(L"%s[", pMemoryContext.FormatAddress(pNode.nValue))); + sPointerChain.push_back(']'); + break; + + case ra::data::util::IndirectNoteResolver::NodeType::Constant: + switch (pNode.nModifierType) + { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: // use hex for bitwise combines + sPointerChain += ra::util::String::Printf(L"0x%02x", pNode.nValue); + break; + + default: + if (pNode.nValue >= 0x1000 && (pNode.nValue & 0xFF) == 0) // large multiple of 256, use hex + sPointerChain += ra::util::String::Printf(L"0x%04x", pNode.nValue); + else if (pNode.nValue >= 10) + sPointerChain += ra::util::String::Printf(L"0x%02x", pNode.nValue); + else // otherwise use decimal + sPointerChain += std::to_wstring(pNode.nValue); + break; + } + break; + + } + } + + return sPointerChain; +} + + +} // namespace util +} // namespace data +} // namespace ra diff --git a/src/devkit/data/util/IndirectNoteResolver.hh b/src/devkit/data/util/IndirectNoteResolver.hh new file mode 100644 index 000000000..a3c1dca14 --- /dev/null +++ b/src/devkit/data/util/IndirectNoteResolver.hh @@ -0,0 +1,63 @@ +#ifndef RA_DATA_UTIL_INDIRECTNOTERESOLVER_H +#define RA_DATA_UTIL_INDIRECTNOTERESOLVER_H +#pragma once + +#include "data/Memory.hh" +#include "data/models/MemoryNotesModel.hh" + +struct rc_condition_t; + +namespace ra { +namespace data { +namespace util { + +class IndirectNoteResolver +{ +public: + IndirectNoteResolver(const ra::data::models::MemoryNotesModel& pMemoryNotes) + : m_pMemoryNotes(&pMemoryNotes) + { + } + + enum NodeType : uint8_t + { + None = 0, + Address, + DereferencedAddress, + Constant, + ConstantOffset, + ArrayOffset, + Recall, + }; + + struct Node + { + uint32_t nValue = 0; + NodeType nType = NodeType::None; + uint8_t nModifierType = 0; /* RC_OPERATOR_ */ + const ra::data::models::MemoryNoteModel* pNote = nullptr; + }; + + /// + /// Gets the value read from an operand and the path taken to get there. + /// + /// The condition to process + /// true to process pCondition.operand1, false to process pCondition.operand2 + /// [out] The path taken to reach the final address. + /// The derived address being read for the conditional comparison. + uint32_t ResolveOperand(const struct rc_condition_t& pCondition, bool bLeafIsOperand1, + std::vector& vParentChain) const; + + std::wstring BuildPath(const std::vector& vParentChain) const; + +protected: + IndirectNoteResolver() {} + + const ra::data::models::MemoryNotesModel* m_pMemoryNotes = nullptr; +}; + +} // namespace util +} // namespace data +} // namespace ra + +#endif RA_DATA_UTIL_INDIRECTNOTERESOLVER_H diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp index 3b22d1b7e..e8e96bf1f 100644 --- a/src/ui/viewmodels/TriggerConditionViewModel.cpp +++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp @@ -5,6 +5,7 @@ #include "data\context\GameContext.hh" #include "data\models\TriggerValidation.hh" +#include "data\util\IndirectNoteResolver.hh" #include "services\AchievementLogicSerializer.hh" #include "services\AchievementRuntime.hh" @@ -581,6 +582,7 @@ static void BuildOperatorTooltip(std::wstring& sTooltip, uint8_t nOperatorType) case RC_OPERATOR_MOD: sTooltip.append(L" % "); break; + case RC_OPERATOR_INDIRECT_READ: case RC_OPERATOR_ADD: sTooltip.append(L" + "); break; @@ -592,140 +594,6 @@ static void BuildOperatorTooltip(std::wstring& sTooltip, uint8_t nOperatorType) } } -static ra::data::ByteAddress GetIndirectAddressFromOperand(const rc_operand_t* pOperand, std::wstring& sPointerChain, - const ra::data::models::MemoryNoteModel** pParentNote) -{ - Expects(pParentNote != nullptr); - - rc_typed_value_t pValue; - rc_evaluate_operand(&pValue, pOperand, nullptr); - rc_typed_value_convert(&pValue, RC_VALUE_TYPE_UNSIGNED); - - // check for {recall} - if (pOperand->type == RC_OPERAND_RECALL) - { - *pParentNote = nullptr; - - sPointerChain += ra::util::String::Printf(L"{recall:0x%02x}", pValue.value.u32); - return pValue.value.u32; - } - - // check for constant offset - if (!rc_operand_is_memref(pOperand)) - { - if (*pParentNote) - *pParentNote = (*pParentNote)->GetPointerNoteAtOffset(pValue.value.u32); - - sPointerChain += ra::util::String::Printf(L"0x%02x", pValue.value.u32); - return pValue.value.u32; - } - - // check for root pointer - if (pOperand->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) - { - const auto nAddress = pOperand->value.memref->address; - - // find the memory note associated to the parent - const auto& pGameContext = ra::services::ServiceLocator::Get(); - const auto* pMemoryNotes = pGameContext.Assets().FindMemoryNotes(); - *pParentNote = pMemoryNotes ? pMemoryNotes->FindMemoryNoteModel(nAddress, false) : nullptr; - - sPointerChain.push_back('$'); - const auto& pMemoryContext = ra::services::ServiceLocator::Get(); - sPointerChain += pMemoryContext.FormatAddress(nAddress); - return pValue.value.u32; - } - - // process offset - GSL_SUPPRESS_TYPE1 const auto* pModifiedMemref = - reinterpret_cast(pOperand->value.memref); - - GetIndirectAddressFromOperand(&pModifiedMemref->parent, sPointerChain, pParentNote); - - // calculate current values of both operands - rc_evaluate_operand(&pValue, &pModifiedMemref->parent, nullptr); - rc_typed_value_convert(&pValue, RC_VALUE_TYPE_UNSIGNED); - - rc_typed_value_t pModifier{}; - rc_evaluate_operand(&pModifier, &pModifiedMemref->modifier, nullptr); - rc_typed_value_convert(&pModifier, RC_VALUE_TYPE_UNSIGNED); - - // if it's an indirect read, determine if it's a pointer or index offset - if (pModifiedMemref->modifier_type == RC_OPERATOR_INDIRECT_READ) - { - rc_typed_value_combine(&pValue, &pModifier, RC_OPERATOR_ADD); - - if (*pParentNote && !(*pParentNote)->IsPointer()) - { - // if the parent note is not a pointer, assume it's an index - Expects(pModifiedMemref->modifier.type == RC_OPERAND_CONST); - const auto nAddress = pModifier.value.u32; - const auto& pMemoryContext = ra::services::ServiceLocator::Get(); - std::wstring sPrefix = ra::util::String::Printf(L"%s[", pMemoryContext.FormatAddress(nAddress)); - sPointerChain.insert(0, sPrefix); - sPointerChain.push_back(']'); - - // find the memory note associated to the start of the array - const auto& pGameContext = ra::services::ServiceLocator::Get(); - const auto* pMemoryNotes = pGameContext.Assets().FindMemoryNotes(); - *pParentNote = pMemoryNotes ? pMemoryNotes->FindMemoryNoteModel(nAddress, false) : nullptr; - - // return the address offset into the array - return pValue.value.u32; - } - - // process the pointer - sPointerChain.push_back('+'); - GetIndirectAddressFromOperand(&pModifiedMemref->modifier, sPointerChain, pParentNote); - - // value is the parent pointer, modifier is the offset. combine them to get the new address - return pValue.value.u32; - } - - // not an indirect read. must be a scalar. - if (pModifiedMemref->modifier_type != RC_OPERATOR_AND) - { - // don't report mask on every pointer evaluation - std::wstring sTemp; - BuildOperatorTooltip(sTemp, pModifiedMemref->modifier_type); - sPointerChain.push_back(sTemp.empty() ? '?' : sTemp.at(1)); - - if (pModifiedMemref->modifier.type == RC_OPERAND_RECALL) - { - sPointerChain += ra::util::String::Printf(L"{recall:0x%02x}", pModifier.value.u32); - } - else if (rc_operand_is_memref(&pModifiedMemref->modifier)) - { - sPointerChain.push_back('$'); - const auto& pMemoryContext = ra::services::ServiceLocator::Get(); - sPointerChain += pMemoryContext.FormatAddress(pModifiedMemref->modifier.value.memref->address); - } - else - { - const auto nModifier = pModifiedMemref->modifier.value.num; - - switch (pModifiedMemref->modifier_type) - { - case RC_OPERATOR_AND: - case RC_OPERATOR_XOR: // use hex for bitwise combines - sPointerChain += ra::util::String::Printf(L"0x%02x", nModifier); - break; - - default: - if (nModifier >= 0x1000 && (nModifier & 0xFF) == 0) // large multiple of 256, use hex - sPointerChain += ra::util::String::Printf(L"0x%04x", nModifier); - else // otherwise use decimal - sPointerChain += std::to_wstring(nModifier); - break; - } - } - } - - // return the result of combining the value and the modifier - rc_typed_value_combine(&pValue, &pModifier, pModifiedMemref->modifier_type); - return pValue.value.u32; -} - const rc_condition_t* TriggerConditionViewModel::GetFirstCondition() const { const auto* pTriggerViewModel = dynamic_cast(m_pTriggerViewModel); @@ -811,14 +679,26 @@ ra::data::ByteAddress TriggerConditionViewModel::GetIndirectAddress(ra::data::By const auto* pCondition = GetCondition(); if (pCondition) { - *pLeafNote = nullptr; + auto& pCodeNotes = ra::services::ServiceLocator::Get().MemoryNotes(); + ra::data::util::IndirectNoteResolver pIndirectNoteResolver(pCodeNotes); + std::vector vParentChain; const auto* pOperand1 = rc_condition_get_real_operand1(pCondition); if (rc_operand_is_memref(pOperand1) && nAddress == pOperand1->value.memref->address) - return GetIndirectAddressFromOperand(pOperand1, sPointerChain, pLeafNote); + { + nAddress = pIndirectNoteResolver.ResolveOperand(*pCondition, true, vParentChain); + sPointerChain = pIndirectNoteResolver.BuildPath(vParentChain); + *pLeafNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + return nAddress; + } if (rc_operand_is_memref(&pCondition->operand2) && nAddress == pCondition->operand2.value.memref->address) - return GetIndirectAddressFromOperand(&pCondition->operand2, sPointerChain, pLeafNote); + { + nAddress = pIndirectNoteResolver.ResolveOperand(*pCondition, false, vParentChain); + sPointerChain = pIndirectNoteResolver.BuildPath(vParentChain); + *pLeafNote = !vParentChain.empty() ? vParentChain.back().pNote : nullptr; + return nAddress; + } } return nAddress; diff --git a/tests/devkit/RADevKit.Tests.vcxproj b/tests/devkit/RADevKit.Tests.vcxproj index 27e213596..19ab5e24b 100644 --- a/tests/devkit/RADevKit.Tests.vcxproj +++ b/tests/devkit/RADevKit.Tests.vcxproj @@ -76,6 +76,7 @@ + diff --git a/tests/devkit/RADevKit.Tests.vcxproj.filters b/tests/devkit/RADevKit.Tests.vcxproj.filters index eccf4c02e..72fcd5d88 100644 --- a/tests/devkit/RADevKit.Tests.vcxproj.filters +++ b/tests/devkit/RADevKit.Tests.vcxproj.filters @@ -76,6 +76,9 @@ data\models + + data\util + @@ -114,6 +117,9 @@ {03e69a47-e3f3-4444-a7f7-718e67212211} + + {f5339023-3443-4db7-81ca-7ba219585abf} + diff --git a/tests/devkit/context/mocks/MockEmulatorMemoryContext.hh b/tests/devkit/context/mocks/MockEmulatorMemoryContext.hh index 026f6ebf6..e1b66b31b 100644 --- a/tests/devkit/context/mocks/MockEmulatorMemoryContext.hh +++ b/tests/devkit/context/mocks/MockEmulatorMemoryContext.hh @@ -58,6 +58,18 @@ public: void MockMemoryInsecure(bool bValue) noexcept { m_bMemoryInsecure = bValue; } bool IsMemoryInsecure() const noexcept override { return m_bMemoryInsecure; } + static uint32_t Peek(uint32_t nAddress, uint32_t num_bytes, void*) + { + union x { + uint32_t n; + uint8_t buffer[4]; + } pBuffer{ 0 }; + + const auto& pEmulatorMemoryContext = ra::services::ServiceLocator::Get(); + const auto nRead = pEmulatorMemoryContext.ReadMemory(static_cast(nAddress), pBuffer.buffer, num_bytes); + return (nRead == num_bytes) ? pBuffer.n : 0; + } + private: static uint8_t ReadMemoryHelper(uint32_t nAddress); static void WriteMemoryHelper(uint32_t nAddress, uint8_t nValue); diff --git a/tests/devkit/data/models/RichPresenceModel_Tests.cpp b/tests/devkit/data/models/RichPresenceModel_Tests.cpp index d738570e7..40195d5cc 100644 --- a/tests/devkit/data/models/RichPresenceModel_Tests.cpp +++ b/tests/devkit/data/models/RichPresenceModel_Tests.cpp @@ -1,13 +1,14 @@ #include "data/models/RichPresenceModel.hh" +#include "context/mocks/MockGameContext.hh" +#include "context/mocks/MockRcClient.hh" + #include "services/impl/StringTextWriter.hh" +#include "services/mocks/MockLocalStorage.hh" -#include "tests/devkit/context/mocks/MockGameContext.hh" -#include "tests/devkit/context/mocks/MockRcClient.hh" -#include "tests/devkit/services/mocks/MockLocalStorage.hh" -#include "tests/devkit/testutil/AssetAsserts.hh" +#include "testutil/AssetAsserts.hh" +#include "testutil/CppUnitTest.hh" -using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace ra { namespace data { diff --git a/tests/devkit/data/util/IndirectNoteResolver_Tests.cpp b/tests/devkit/data/util/IndirectNoteResolver_Tests.cpp new file mode 100644 index 000000000..0dcd2c4b6 --- /dev/null +++ b/tests/devkit/data/util/IndirectNoteResolver_Tests.cpp @@ -0,0 +1,391 @@ +#include "data/util/IndirectNoteResolver.hh" + +#include "context/mocks/MockConsoleContext.hh" +#include "context/mocks/MockEmulatorMemoryContext.hh" +#include "context/mocks/MockGameContext.hh" +#include "context/mocks/MockUserContext.hh" + +#include "testutil/CppUnitTest.hh" + +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace ra { +namespace data { +namespace util { +namespace tests { + +TEST_CLASS(IndirectNoteResolver_Tests) +{ +private: + class IndirectNoteResolverHarness : public IndirectNoteResolver + { + public: + ra::context::mocks::MockEmulatorMemoryContext mockEmulatorMemoryContext; + ra::context::mocks::MockGameContext mockGameContext; + ra::context::mocks::MockUserContext mockUserContext; + + IndirectNoteResolverHarness() + : IndirectNoteResolver() + { + m_pMemoryNotes = &mockGameContext.MemoryNotes(); + } + + const rc_trigger_t* Parse(const std::string& sInput) + { + mockEmulatorMemoryContext.MockMemory(m_pMemory); + + const auto nSize = rc_trigger_size(sInput.c_str()); + Assert::IsTrue(nSize > 0); + + m_sTriggerBuffer.resize(nSize); + m_pTrigger = rc_parse_trigger(m_sTriggerBuffer.data(), sInput.c_str(), nullptr, 0); + + UpdateMemrefs(); + return m_pTrigger; + } + + void UpdateMemrefs() + { + rc_memrefs_t* memrefs = rc_trigger_get_memrefs(m_pTrigger); + rc_update_memref_values(memrefs, mockEmulatorMemoryContext.Peek, nullptr); + } + + void SetMemory(ra::data::ByteAddress nAddress, uint8_t nValue) + { + m_pMemory.at(static_cast(nAddress)) = nValue; + } + + const rc_condition_t& GetCondition(size_t nIndex) const + { + const rc_condition_t* pCondition = m_pTrigger->requirement->conditions; + for (; nIndex > 0; --nIndex) + pCondition = pCondition->next; + + return *pCondition; + } + + private: + std::array m_pMemory = { + 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, + 16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, + 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47, + 48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63, + }; + + std::string m_sTriggerBuffer; + rc_trigger_t* m_pTrigger = nullptr; + }; + +public: + TEST_METHOD(TestSimpleAddress) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 8U }, L"This is a note."); + resolver.Parse("0xH0008=3"); + + std::vector vParentChain; + Assert::AreEqual(8U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"This is a note."), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0008"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + Assert::AreEqual(0U, resolver.ResolveOperand(resolver.GetCondition(0), false, vParentChain)); + Assert::AreEqual({ 0U }, vParentChain.size()); + } + + TEST_METHOD(TestSimpleAddressNoMemoryNote) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("0xH0008=3"); + + std::vector vParentChain; + Assert::AreEqual(8U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::IsNull(vParentChain.front().pNote); + } + + TEST_METHOD(TestSimpleAddressMultiNote) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 8U }, L"This is a note."); + resolver.mockGameContext.SetNote({ 9U }, L"This is another note."); + resolver.Parse("0xH0008>0xH0009"); + + std::vector vParentChain; + Assert::AreEqual(8U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"This is a note."), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0008"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + Assert::AreEqual(9U, resolver.ResolveOperand(resolver.GetCondition(0), false, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"This is another note."), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0009"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestSimpleAddressInMiddleOfNote) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 8U }, L"[8 bytes] This is a note."); + resolver.Parse("0xH0008=0xH000C"); + + std::vector vParentChain; + Assert::AreEqual(8U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"[8 bytes] This is a note."), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0008"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + Assert::AreEqual(0x0CU, resolver.ResolveOperand(resolver.GetCondition(0), false, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::IsNotNull(vParentChain.front().pNote); + Assert::AreEqual(std::wstring(L"[8 bytes] This is a note."), vParentChain.front().pNote->GetNote()); + Assert::IsNotNull(vParentChain.back().pNote); + Assert::AreEqual(std::wstring(L"[8 bytes] This is a note."), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0008+4"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestAddressLargeArrayNote) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 8U }, L"[100 bytes] This is a note."); + + std::vector vParentChain; + resolver.Parse("0xH0008=3"); + Assert::AreEqual(8U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual(std::wstring(L"0x0008"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + resolver.Parse("0xH000C=3"); + Assert::AreEqual(0x0CU, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual(std::wstring(L"0x0008+4"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + resolver.Parse("0xH0016=3"); + Assert::AreEqual(0x16U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual(std::wstring(L"0x0008+0x0e"), resolver.BuildPath(vParentChain)); + + vParentChain.clear(); + resolver.Parse("0xH003E=3"); + Assert::AreEqual(0x3EU, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual(std::wstring(L"0x0008+0x36"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestAddressBitNote) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 8U }, L"b1=Left\r\nb2=Right\r\nb3=Upper\r\n"); + resolver.Parse("0xO0008=1"); + + std::vector vParentChain; + Assert::AreEqual(0U, resolver.ResolveOperand(resolver.GetCondition(0), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"b1=Left\r\nb2=Right\r\nb3=Upper\r\n"), vParentChain.front().pNote->GetNote()); + + Assert::AreEqual(std::wstring(L"0x0008"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestIndirectAddressNoMemoryNote) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("I:0xH0001_0xH0002=3"); + + // $0001 = 1, 1+2 = $0003 + std::vector vParentChain; + Assert::AreEqual(3U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"$0x0001+0x02"), resolver.BuildPath(vParentChain)); + Assert::IsNull(vParentChain.front().pNote); + Assert::IsNull(vParentChain.back().pNote); + } + + TEST_METHOD(TestIndirectAddress) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 1U }, L"[8-bit pointer]\n+2=This is a note."); + resolver.Parse("I:0xH0001_0xH0002=3"); + + // $0001 = 1, 1+2 = $0003 + std::vector vParentChain; + Assert::AreEqual(3U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"$0x0001+0x02"), resolver.BuildPath(vParentChain)); + Assert::AreEqual(std::wstring(L"[8-bit pointer]\n+2=This is a note."), vParentChain.front().pNote->GetNote()); + Assert::AreEqual(std::wstring(L"This is a note."), vParentChain.back().pNote->GetNote()); + } + + TEST_METHOD(TestIndirectAddressMultiply) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("I:0xH0001*3_0xH0002=3"); + + // $0001 = 1, 1*3+2 = $0005 + std::vector vParentChain; + Assert::AreEqual(5U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"$0x0001*3+0x02"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestIndirectAddressMasked) + { + IndirectNoteResolverHarness resolver; + ra::context::mocks::MockConsoleContext mockConsoleContext(ConsoleID::PSP, L"PSP"); + resolver.mockGameContext.SetNote({0U}, L"[32-bit pointer]\n+2=This is a note."); + resolver.Parse("I:0xX0000&33554431_0xH0002=6"); // 33554431 = 0x01FFFFFF + + // $0001 = 0x08000003, 0x80000003&0x01FFFFFF=0x00000003+2 = 0x00000005 + resolver.SetMemory({ 0 }, 0x03); + resolver.SetMemory({ 1 }, 0x00); + resolver.SetMemory({ 2 }, 0x00); + resolver.SetMemory({ 3 }, 0x08); + resolver.UpdateMemrefs(); + + std::vector vParentChain; + Assert::AreEqual(5U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"$0x0000+0x02"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestDoubleIndirectAddress) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 1U }, L"[8-bit pointer]\n+2=First Level [8-bit pointer]\n +3=Second Level."); + resolver.Parse("I:0xH0001_I:0xH0002_0xH0003=4"); + + // $0001 = 1, 1+2 = 0003, $0003 = 3, 3+3 = 0006, $0006 = 6 + std::vector vParentChain; + Assert::AreEqual(6U, resolver.ResolveOperand(resolver.GetCondition(2), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"$0x0001+0x02+0x03"), resolver.BuildPath(vParentChain)); + + Assert::AreEqual(std::wstring(L"[8-bit pointer]\n+2=First Level [8-bit pointer]\n +3=Second Level."), vParentChain.front().pNote->GetNote()); + Assert::AreEqual(std::wstring(L"First Level [8-bit pointer]\n+3=Second Level."), vParentChain.at(1).pNote->GetNote()); + Assert::AreEqual(std::wstring(L"Second Level."), vParentChain.back().pNote->GetNote()); + } + + TEST_METHOD(TestIndirectAddressOffset) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 1U }, L"Region differentiator"); + resolver.mockGameContext.SetNote({ 2U }, L"[US] Note for NA"); + resolver.Parse("I:0xH0001_0xH0002=3"); + + // $0001 = 1, 1+2 = $0003 + std::vector vParentChain; + Assert::AreEqual(3U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"0x0002[$0x0001]"), resolver.BuildPath(vParentChain)); + + Assert::AreEqual(std::wstring(L"Region differentiator"), vParentChain.front().pNote->GetNote()); + Assert::AreEqual(std::wstring(L"[US] Note for NA"), vParentChain.back().pNote->GetNote()); + } + + TEST_METHOD(TestIndirectAddressScaledOffset) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 1U }, L"Region differentiator"); + resolver.mockGameContext.SetNote({ 2U }, L"[US] Note for NA"); + resolver.Parse("I:0xH0001*2_0xH0002=3"); + + // $0001 = 1, 1*2+2 = $0004 + std::vector vParentChain; + Assert::AreEqual(4U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"0x0002[$0x0001*2]"), resolver.BuildPath(vParentChain)); + + Assert::AreEqual(std::wstring(L"Region differentiator"), vParentChain.front().pNote->GetNote()); + Assert::AreEqual(std::wstring(L"[US] Note for NA"), vParentChain.back().pNote->GetNote()); + } + + TEST_METHOD(TestRecallBasic) + { + IndirectNoteResolverHarness resolver; + resolver.mockGameContext.SetNote({ 1U }, L"[8-bit pointer]\n+2=First Level (8-bit pointer)\n +3=Second Level."); + resolver.Parse("K:0xH0001_I:{recall}_I:0xH0002_0xH0003=4"); + + // $0001 = 1, 1+2 = $0003, $0003 = 3, 3+3 = $0006 + std::vector vParentChain; + Assert::AreEqual(6U, resolver.ResolveOperand(resolver.GetCondition(3), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"{recall:0x01}+0x02+0x03"), resolver.BuildPath(vParentChain)); + + Assert::AreEqual(std::wstring(L"[8-bit pointer]\n+2=First Level (8-bit pointer)\n +3=Second Level."), vParentChain.front().pNote->GetNote()); + Assert::AreEqual(std::wstring(L"First Level (8-bit pointer)\n+3=Second Level."), vParentChain.at(1).pNote->GetNote()); + Assert::AreEqual(std::wstring(L"Second Level."), vParentChain.back().pNote->GetNote()); + } + + TEST_METHOD(TestRecallAddSource) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("A:1_K:0xH0001_I:{recall}_I:0xH0002_0xH0003=4"); + + // $0001 = 1, 1+1+2 = $0004, $0004 = 4, 4+3 = $0007 + std::vector vParentChain; + Assert::AreEqual(7U, resolver.ResolveOperand(resolver.GetCondition(4), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"{recall:0x02}+0x02+0x03"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestRecallSubSource) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("B:2_K:0xH0003_I:{recall}_I:0xH0002_0xH0003=4"); + + // $0003 = 3, 3-2+2 = $0003, $0003 = 3, 3+3 = $0006 + std::vector vParentChain; + Assert::AreEqual(6U, resolver.ResolveOperand(resolver.GetCondition(4), true, vParentChain)); + Assert::AreEqual({ 3U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"{recall:0x01}+0x02+0x03"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestRecallChain) + { + IndirectNoteResolverHarness resolver; + resolver.SetMemory({ 0x11 }, 0); + resolver.Parse("0xH1234=6_K:0x 0002&511_K:{recall}*2_K:{recall}+4_I:0x 0010+{recall}_K:0x 0004_I:{recall}_M:0xH0002=20"); + // 1 Byte 0x1234 = 6 + // 2 Remember Word 0x0002 & 0x01ff + // 3 Remember Recall * 2 + // 4 Remember Recall + 4 + // 5 AddAddr Word 0x0010 + Recall + // 6 Remember Word 0x0004 + // 7 AddAddr Recall + // 8 Measured Byte 0x0002 + + // 2: $0002 = 0x0302 & 0x01ff = 0x0102 + // 3: 0x0102 * 2 = 0x0204 + // 4: 0x0204 + 4 = 0x0208 + // 6: $0010 = 0x0010 + 0x0208 + 4 = 0x021c, $021c=0x0000 + // 8: 0x0000 + 0x0002 = 0x0002, $0002 = 2 + + std::vector vParentChain; + Assert::AreEqual(2U, resolver.ResolveOperand(resolver.GetCondition(7), true, vParentChain)); + Assert::AreEqual({ 2U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"{recall:0x00}+0x02"), resolver.BuildPath(vParentChain)); + } + + TEST_METHOD(TestRecallInvalid) + { + IndirectNoteResolverHarness resolver; + resolver.Parse("A:{recall}_0xH0000=1"); + + std::vector vParentChain; + Assert::AreEqual(0U, resolver.ResolveOperand(resolver.GetCondition(1), true, vParentChain)); + Assert::AreEqual({ 1U }, vParentChain.size()); + Assert::AreEqual(std::wstring(L"0x0000"), resolver.BuildPath(vParentChain)); + } +}; + +} // namespace tests +} // namespace util +} // namespace data +} // namespace ra diff --git a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp index 918283fc3..cdb8734e0 100644 --- a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp +++ b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp @@ -160,6 +160,22 @@ TEST_CLASS(TriggerSummaryViewModel_Tests) summary.AssertClause(1, L"2", L"Difficulty", L"is", L"Easy"); // <1 converted to ==0 } + TEST_METHOD(TestIndirectNote) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, + L"[32-bit pointer] Player data\r\n" + L"+0: [32-bit pointer] Class info\r\n" + L"++4: [32-bit] ID\r\n" + L"+4: [32-bit] Current HP\r\n" + L"+8: [32-bit] Max HP"); + summary.InitializeFrom("I:0xX1234_0xX0004=0xX0008_I:0xX1234_I:0xX0000_0xX0004=3"); + + Assert::AreEqual({ 2U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1-2", L"Current HP", L"equals", L"Max HP"); + summary.AssertClause(1, L"3-5", L"ID", L"is", L"3"); + } + TEST_METHOD(TestMemoryReferenceDeltaSelf) { TriggerSummaryViewModelHarness summary; From 19d9cb9141d96aba5b93853df2b0a2d7259c582a Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 21 Jun 2026 18:05:24 -0600 Subject: [PATCH 2/3] resolve indirect notes --- src/ui/viewmodels/TriggerSummaryViewModel.cpp | 86 ++++++++++++++++--- .../TriggerSummaryViewModel_Tests.cpp | 55 ++++++++++++ 2 files changed, 131 insertions(+), 10 deletions(-) diff --git a/src/ui/viewmodels/TriggerSummaryViewModel.cpp b/src/ui/viewmodels/TriggerSummaryViewModel.cpp index 23ebac02f..c64f84bac 100644 --- a/src/ui/viewmodels/TriggerSummaryViewModel.cpp +++ b/src/ui/viewmodels/TriggerSummaryViewModel.cpp @@ -4,6 +4,7 @@ #include "data\Memory.hh" #include "data\context\GameContext.hh" +#include "data\util\IndirectNoteResolver.hh" #include "services\ServiceLocator.hh" @@ -497,6 +498,7 @@ void TriggerSummaryViewModel::InitializeFrom(const rc_condset_t& pCondSet) { auto& pClause = m_vClauses.Add(); + // group AddAddress chain together nFirstIndex = nLastIndex + 1; nLastIndex = nFirstIndex; if (pCondition->type == RC_CONDITION_ADD_ADDRESS) @@ -522,9 +524,22 @@ void TriggerSummaryViewModel::InitializeFrom(const rc_condset_t& pCondSet) pClause.pCondition = pCondition; + // get note for let operand const ra::data::models::MemoryNoteModel* pNote = nullptr; if (pMemoryNotes && rc_operand_is_memref(&pCondition->operand1)) - pNote = pMemoryNotes->FindMemoryNoteModel(pCondition->operand1.value.memref->address); + { + if (nFirstIndex != nLastIndex) + { + std::vector vParentChain; + ra::data::util::IndirectNoteResolver pResolver(*pMemoryNotes); + pResolver.ResolveOperand(*pCondition, true, vParentChain); + if (!vParentChain.empty()) + pNote = vParentChain.back().pNote; + } + + if (!pNote) + pNote = pMemoryNotes->FindMemoryNoteModel(pCondition->operand1.value.memref->address); + } if (pNote) { @@ -539,6 +554,7 @@ void TriggerSummaryViewModel::InitializeFrom(const rc_condset_t& pCondSet) pClause.SetReference(OperandToString(pCondition->operand1)); } + // set operation string HandleOperation(pClause, pCondition->oper); if (rc_operand_is_memref(&pCondition->operand2)) @@ -550,39 +566,89 @@ void TriggerSummaryViewModel::InitializeFrom(const rc_condset_t& pCondSet) HandleTally(pClause, *pCondition); continue; } + else + { + // get note for right operand + const ra::data::models::MemoryNoteModel* pNote2 = nullptr; + if (pMemoryNotes && rc_operand_is_memref(&pCondition->operand2)) + { + if (nFirstIndex != nLastIndex) + { + std::vector vParentChain; + ra::data::util::IndirectNoteResolver pResolver(*pMemoryNotes); + pResolver.ResolveOperand(*pCondition, false, vParentChain); + if (!vParentChain.empty()) + pNote2 = vParentChain.back().pNote; + } + + if (!pNote2) + pNote2 = pMemoryNotes->FindMemoryNoteModel(pCondition->operand2.value.memref->address); + } + + if (pNote2) + { + const auto pSubNote = pNote2->GetSubNote(ra::data::Memory::SizeFromRcheevosSize(pCondition->operand2.size)); + if (!pSubNote.empty()) + pClause.SetTarget(EnumValueFromText(pSubNote)); + else + pClause.SetTarget(pNote2->GetSummary()); + + if (pNote) + { + auto sOperation = pClause.GetOperation(); + if (sOperation == L"is") + sOperation = L"equals"; + else if (sOperation == L"is not") + sOperation = L"does not equal"; + + if (pCondition->operand2.type == RC_OPERAND_DELTA) + sOperation += L" last frame of"; + else if (pCondition->operand2.type == RC_OPERAND_PRIOR) + sOperation += L" previous value of"; + + pClause.SetOperation(sOperation); + } + } + else + { + pClause.SetTarget(OperandToString(pCondition->operand2)); + } + } } else if (pNote) { + // right is a constant auto nTarget = pCondition->operand2.value.num; - // a < 1 ~> a == 0 + // better to match equality than bounds if (nTarget == 1 && pCondition->oper == RC_OPERATOR_LT) { + // a < 1 ~> a == 0 nTarget = 0; HandleOperation(pClause, RC_OPERATOR_EQ); } + else if (nTarget == 0 && pCondition->oper == RC_OPERATOR_GT) + { + // a > 0 ~> a != 0 + HandleOperation(pClause, RC_OPERATOR_NE); + } + // look for enum value in note const auto pEnumText = pNote->GetEnumText(nTarget); if (!pEnumText.empty()) - { pClause.SetTarget(EnumValueFromText(pEnumText)); - - // a > 0 ~> a != 0 - if (nTarget == 0 && pCondition->oper == RC_OPERATOR_GT) - HandleOperation(pClause, RC_OPERATOR_NE); - } else - { pClause.SetTarget(std::to_wstring(nTarget)); - } } else { pClause.SetTarget(OperandToString(pCondition->operand2)); } + // handle hit targets HandleTally(pClause, *pCondition); + // attempt to merge any other conditions comparing the same address (i.e. A>4 && A<6 => A between 4 and 6) if (rc_operand_is_memref(&pCondition->operand1)) { for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vClauses.Count()) - 1; ++nIndex) diff --git a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp index cdb8734e0..62d1a8e41 100644 --- a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp +++ b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp @@ -271,6 +271,61 @@ TEST_CLASS(TriggerSummaryViewModel_Tests) summary.AssertClause(0, L"1-2", L"World", L"decreased to", L"5"); } + TEST_METHOD(TestMemoryReferenceEqualsOtherMemory) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"Current HP"); + summary.mockGameContext.SetNote({ 0x1238U }, L"Max HP"); + summary.InitializeFrom("0xH1234=0xH1238"); + + Assert::AreEqual({ 1U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"Current HP", L"equals", L"Max HP"); + } + + TEST_METHOD(TestMemoryReferenceNotEqualsOtherMemory) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"Current HP"); + summary.mockGameContext.SetNote({ 0x1238U }, L"Max HP"); + summary.InitializeFrom("0xH1234!=0xH1238"); + + Assert::AreEqual({ 1U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"Current HP", L"does not equal", L"Max HP"); + } + + TEST_METHOD(TestMemoryReferenceLessThanOtherMemory) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"Current HP"); + summary.mockGameContext.SetNote({ 0x1238U }, L"Max HP"); + summary.InitializeFrom("0xH1234<0xH1238"); + + Assert::AreEqual({ 1U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"Current HP", L"is less than", L"Max HP"); + } + + TEST_METHOD(TestMemoryReferenceLessThanOrEqualOtherMemory) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"Current HP"); + summary.mockGameContext.SetNote({ 0x1238U }, L"Max HP"); + summary.InitializeFrom("0xH1234<=0xH1238"); + + Assert::AreEqual({ 1U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"Current HP", L"is at most", L"Max HP"); + } + + TEST_METHOD(TestMemoryReferenceEqualsDeltaOtherMemory) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"Current HP"); + summary.mockGameContext.SetNote({ 0x1238U }, L"Max HP"); + summary.InitializeFrom("0xH1234=d0xH1238"); + + Assert::AreEqual({ 1U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"Current HP", L"equals last frame of", L"Max HP"); + } + TEST_METHOD(TestAddHeadersSimple) { ra::ui::EditorTheme pTheme; From ea6c493d808ad17d08d90d6cfd0b035f4bb547bf Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 21 Jun 2026 18:52:11 -0600 Subject: [PATCH 3/3] don't treat [8-bit] as a range --- src/devkit/data/models/MemoryNoteModel.cpp | 10 +++- src/ui/viewmodels/TriggerSummaryViewModel.cpp | 3 ++ .../data/models/MemoryNoteModel_Tests.cpp | 48 +++++++++++++++++++ .../TriggerSummaryViewModel_Tests.cpp | 33 +++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/devkit/data/models/MemoryNoteModel.cpp b/src/devkit/data/models/MemoryNoteModel.cpp index 33815ef7c..beca1bdba 100644 --- a/src/devkit/data/models/MemoryNoteModel.cpp +++ b/src/devkit/data/models/MemoryNoteModel.cpp @@ -1248,7 +1248,10 @@ static bool ParseRange(std::wstring_view svRange, uint32_t& nLow, uint32_t& nHig if (nIndex == nStart) return false; - svHigh = svRange.substr(nStart, nIndex); + if (nIndex < svRange.size() && ra::util::String::IsAlpha(svRange.at(nIndex))) + return false; + + svHigh = svRange.substr(nStart, nIndex - nStart); } nLow = Convert(svLow, isHex); @@ -1292,7 +1295,7 @@ static std::wstring_view GetValues(const std::wstring_view svLine) // no right bracket found. take the rest of the line if (nRightBracket == std::wstring::npos) - nRightBracket = svLine.size(); + return {}; return svLine.substr(nLeftBracket + 1, nRightBracket - nLeftBracket - 1); } @@ -1562,6 +1565,9 @@ std::wstring MemoryNoteModel::GetSummary() const } } + while (!svNote.empty() && ra::util::String::IsSpace(svNote.back())) + svNote = svNote.substr(0, svNote.size() - 1); + return TrimSize(std::wstring(svNote), false); } diff --git a/src/ui/viewmodels/TriggerSummaryViewModel.cpp b/src/ui/viewmodels/TriggerSummaryViewModel.cpp index c64f84bac..a7a1e3413 100644 --- a/src/ui/viewmodels/TriggerSummaryViewModel.cpp +++ b/src/ui/viewmodels/TriggerSummaryViewModel.cpp @@ -231,6 +231,9 @@ bool TriggerSummaryViewModel::MergeClauses( TriggerSummaryViewModel::TriggerClauseViewModel& pClause2, gsl::index nIndex1, gsl::index nIndex2) { + if (pClause1.pCondition->type != pClause2.pCondition->type) + return false; + if (pClause1.nType == TriggerClauseType::Is) { if (pClause2.nType == TriggerClauseType::IsNot) diff --git a/tests/devkit/data/models/MemoryNoteModel_Tests.cpp b/tests/devkit/data/models/MemoryNoteModel_Tests.cpp index 9c54b85d3..b6a81f4c1 100644 --- a/tests/devkit/data/models/MemoryNoteModel_Tests.cpp +++ b/tests/devkit/data/models/MemoryNoteModel_Tests.cpp @@ -902,9 +902,57 @@ TEST_CLASS(MemoryNoteModel_Tests) Assert::AreEqual(std::wstring_view(L"0x4C=Green"), note.GetEnumText(0x4C)); Assert::AreEqual(std::wstring_view(L"0xA3=Blue"), note.GetEnumText(0xA3)); Assert::AreEqual(std::wstring_view(), note.GetEnumText(0x2A)); + Assert::AreEqual(std::wstring_view(), note.GetEnumText(9)); // 8-b could be a range Assert::AreEqual(ra::data::Memory::Format::Hex, note.GetDefaultMemFormat()); } + TEST_METHOD(TestGetEnumTextIndentBulletNotPrefixed) + { + MemoryNoteModelHarness note; + const std::wstring sNote = + L"[8-bit] Color:\r\n" + L"* 00=None\r\n" + L"* 10=Red\r\n" + L"* 4C=Green\r\n" + L"* A3=Blue\r\n"; + note.SetNote(sNote); + + Assert::AreEqual(std::wstring(L"Color:"), note.GetSummary()); + Assert::AreEqual(std::wstring_view(L"00=None"), note.GetEnumText(0x00)); + Assert::AreEqual(std::wstring_view(L"10=Red"), note.GetEnumText(0x10)); + Assert::AreEqual(std::wstring_view(L"4C=Green"), note.GetEnumText(0x4C)); + Assert::AreEqual(std::wstring_view(L"A3=Blue"), note.GetEnumText(0xA3)); + Assert::AreEqual(std::wstring_view(), note.GetEnumText(0x2A)); + Assert::AreEqual(std::wstring_view(), note.GetEnumText(9)); // 8-b could be a range + Assert::AreEqual(ra::data::Memory::Format::Hex, note.GetDefaultMemFormat()); + } + + TEST_METHOD(TestGetEnumTextIndentBulletIndirect) + { + MemoryNoteModelHarness note; + const std::wstring sNote = + L"[16-bit pointer] root pointer\r\n" + L"+0x06: [16-bit pointer] next\r\n" + L"+0x0A: [16-bit] Color: \r\n" + L"* 0000=None\r\n" + L"* 3010=Red\r\n" + L"* A04C=Green\r\n" + L"* 08A3=Blue\r\n"; + note.SetNote(sNote); + + const auto* pSubNote = note.GetPointerNoteAtOffset(0x0A); + Assert::IsNotNull(pSubNote); + + Assert::AreEqual(std::wstring(L"Color:"), pSubNote->GetSummary()); + Assert::AreEqual(std::wstring_view(L"0000=None"), pSubNote->GetEnumText(0x0000)); + Assert::AreEqual(std::wstring_view(L"3010=Red"), pSubNote->GetEnumText(0x3010)); + Assert::AreEqual(std::wstring_view(L"A04C=Green"), pSubNote->GetEnumText(0xA04C)); + Assert::AreEqual(std::wstring_view(L"08A3=Blue"), pSubNote->GetEnumText(0x08A3)); + Assert::AreEqual(std::wstring_view(), pSubNote->GetEnumText(0x2A)); + Assert::AreEqual(std::wstring_view(), pSubNote->GetEnumText(9)); // 8-b could be a range + Assert::AreEqual(ra::data::Memory::Format::Hex, pSubNote->GetDefaultMemFormat()); + } + TEST_METHOD(TestGetEnumTextRange) { MemoryNoteModelHarness note; diff --git a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp index 62d1a8e41..6e5f42088 100644 --- a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp +++ b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp @@ -271,6 +271,39 @@ TEST_CLASS(TriggerSummaryViewModel_Tests) summary.AssertClause(0, L"1-2", L"World", L"decreased to", L"5"); } + TEST_METHOD(TestNotEqualTwoValues) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"World"); + summary.InitializeFrom("0xH1234!=4_0xH1234!=8"); + + Assert::AreEqual({ 2U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"World", L"is not", L"4"); + summary.AssertClause(1, L"2", L"World", L"is not", L"8"); + } + + TEST_METHOD(TestNotEqualTwoEnumValues) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"World\r\n4=A\r\n8=B"); + summary.InitializeFrom("0xH1234!=4_0xH1234!=8"); + + Assert::AreEqual({ 2U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"World", L"is not", L"A"); + summary.AssertClause(1, L"2", L"World", L"is not", L"B"); + } + + TEST_METHOD(TestNotEqualOrReset) + { + TriggerSummaryViewModelHarness summary; + summary.mockGameContext.SetNote({ 0x1234U }, L"World"); + summary.InitializeFrom("0xH1234!=4_R:0xH1234=8"); + + Assert::AreEqual({ 2U }, summary.Clauses().Count()); + summary.AssertClause(0, L"1", L"World", L"is not", L"4"); + summary.AssertClause(1, L"2", L"World", L"is", L"8"); // Reset will become a header + } + TEST_METHOD(TestMemoryReferenceEqualsOtherMemory) { TriggerSummaryViewModelHarness summary;