diff --git a/src/devkit/RADevKit.vcxproj b/src/devkit/RADevKit.vcxproj
index b72d6a91..aaea74aa 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 05637bdb..6b02e5c6 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/models/MemoryNoteModel.cpp b/src/devkit/data/models/MemoryNoteModel.cpp
index 33815ef7..beca1bdb 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/devkit/data/util/IndirectNoteResolver.cpp b/src/devkit/data/util/IndirectNoteResolver.cpp
new file mode 100644
index 00000000..fc549cd6
--- /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 00000000..a3c1dca1
--- /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 3b22d1b7..e8e96bf1 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/src/ui/viewmodels/TriggerSummaryViewModel.cpp b/src/ui/viewmodels/TriggerSummaryViewModel.cpp
index 23ebac02..a7a1e341 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"
@@ -230,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)
@@ -497,6 +501,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 +527,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 +557,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 +569,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/devkit/RADevKit.Tests.vcxproj b/tests/devkit/RADevKit.Tests.vcxproj
index 27e21359..19ab5e24 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 eccf4c02..72fcd5d8 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 026f6ebf..e1b66b31 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/MemoryNoteModel_Tests.cpp b/tests/devkit/data/models/MemoryNoteModel_Tests.cpp
index 9c54b85d..b6a81f4c 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/devkit/data/models/RichPresenceModel_Tests.cpp b/tests/devkit/data/models/RichPresenceModel_Tests.cpp
index d738570e..40195d5c 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 00000000..0dcd2c4b
--- /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 918283fc..6e5f4208 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;
@@ -255,6 +271,94 @@ 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;
+ 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;