diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj
index 611699a5..5e2fac99 100644
--- a/src/RA_Integration.vcxproj
+++ b/src/RA_Integration.vcxproj
@@ -157,6 +157,7 @@
+
@@ -188,6 +189,7 @@
+
@@ -333,6 +335,7 @@
+
@@ -379,6 +382,7 @@
+
diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters
index 55261723..0f0c778c 100644
--- a/src/RA_Integration.vcxproj.filters
+++ b/src/RA_Integration.vcxproj.filters
@@ -396,6 +396,12 @@
Services\Impl
+
+ UI\ViewModels
+
+
+ UI\Win32
+
Data\Models
@@ -974,6 +980,12 @@
Services\Impl
+
+ UI\ViewModels
+
+
+ UI\Win32
+
Data\Models
diff --git a/src/RA_Resource.h b/src/RA_Resource.h
index d08538eb..4c4747b7 100644
--- a/src/RA_Resource.h
+++ b/src/RA_Resource.h
@@ -153,6 +153,7 @@
#define IDC_RA_LBX_REGIONS 1248
#define IDC_RA_ADD_REGION 1249
#define IDC_RA_REMOVE_REGION 1250
+#define IDC_RA_VIEW_DETAIL 1251
#define IDD_RA_MEMORY 1501
@@ -170,6 +171,7 @@
#define IDD_RA_POINTERFINDER 1514
#define IDD_RA_POINTERINSPECTOR 1515
#define IDD_RA_MEMORYREGIONS 1516
+#define IDD_RA_TRIGGERSUMMARY 1517
#define IDC_RA_PASSWORD 1535
#define IDC_RA_SAVEPASSWORD 1536
#define IDC_RA_USERNAME 1549
@@ -224,7 +226,7 @@
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 122
#define _APS_NEXT_COMMAND_VALUE 40001
-#define _APS_NEXT_CONTROL_VALUE 1251
+#define _APS_NEXT_CONTROL_VALUE 1252
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 4f4c392e..c7a182b5 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -154,6 +154,7 @@ BEGIN
LTEXT "Requirements:",IDC_STATIC,50,55,48,8
ICON "",IDC_RA_ERROR_INDICATOR,98,53,13,13,SS_NOTIFY | SS_CENTERIMAGE
CONTROL "",IDC_RA_LBX_CONDITIONS,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_OWNERDATA | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,50,66,391,109
+ PUSHBUTTON "Explain",IDC_RA_VIEW_DETAIL,207,53,44,13
CONTROL "Highlights",IDC_RA_CHK_HIGHLIGHTS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,257,53,45,13
CONTROL "Pause on Reset",IDC_RA_CHK_PAUSE_ON_RESET,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,307,53,65,13
CONTROL "Pause on Trigger",IDC_RA_CHK_PAUSE_ON_TRIGGER,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,376,53,65,13
@@ -166,6 +167,15 @@ BEGIN
CONTROL "Show Decimal",IDC_RA_CHK_SHOW_DECIMALS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,383,179,54,10
END
+IDD_RA_TRIGGERSUMMARY DIALOGEX 0, 0, 480, 240
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
+CAPTION "Trigger Summary"
+FONT 8, "MS Shell Dlg", 400, 0, 0x0
+BEGIN
+ CONTROL "",IDC_RA_LBX_CONDITIONS,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_CLIPCHILDREN | WS_BORDER,4,4,472,216
+ PUSHBUTTON "OK",IDOK,426,223,50,14
+END
+
IDD_RA_GAMETITLESEL DIALOGEX 0, 0, 249, 156
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_TOOLWINDOW
diff --git a/src/devkit/data/ModelPropertyContainer.hh b/src/devkit/data/ModelPropertyContainer.hh
index b19a4768..2a76a77c 100644
--- a/src/devkit/data/ModelPropertyContainer.hh
+++ b/src/devkit/data/ModelPropertyContainer.hh
@@ -14,7 +14,7 @@ class ModelPropertyContainer
public:
GSL_SUPPRESS_F6 ModelPropertyContainer() = default;
-#ifdef DEBUG
+#ifndef NDEBUG
virtual ~ModelPropertyContainer() noexcept {
m_bDestructed = true;
}
diff --git a/src/ui/EditorTheme.hh b/src/ui/EditorTheme.hh
index d58943ff..e178415b 100644
--- a/src/ui/EditorTheme.hh
+++ b/src/ui/EditorTheme.hh
@@ -48,6 +48,16 @@ public:
Color ColorTriggerResetTrue() const noexcept { return m_colorTriggerResetTrue; }
Color ColorTriggerPauseTrue() const noexcept { return m_colorTriggerPauseTrue; }
+ // ===== explain =====
+
+ Color ColorExplainConflicting() const noexcept { return m_colorExplainConflicting; }
+ Color ColorExplainTriggerWhen() const noexcept { return m_colorExplainTriggerWhen; }
+ Color ColorExplainWhile() const noexcept { return m_colorExplainWhile; }
+ Color ColorExplainUnless() const noexcept { return m_colorExplainUnless; }
+ Color ColorExplainStartingWhen() const noexcept { return m_colorExplainStartingWhen; }
+ Color ColorExplainFailingWhen() const noexcept { return m_colorExplainFailingWhen; }
+ Color ColorExplainImpotent() const noexcept { return m_colorExplainImpotent; }
+
// ===== methods =====
void LoadFromFile();
@@ -76,6 +86,14 @@ private:
Color m_colorTriggerBecomingTrue{ 255, 192, 255, 255 };
Color m_colorTriggerResetTrue{ 255, 255, 255, 192 };
Color m_colorTriggerPauseTrue{ 255, 255, 192, 192 };
+
+ Color m_colorExplainConflicting{ 255, 255, 192, 192 }; // red
+ Color m_colorExplainTriggerWhen{ 255, 192, 255, 192 }; // green
+ Color m_colorExplainWhile{ 255, 192, 192, 255 }; // blue
+ Color m_colorExplainUnless{ 255, 255, 192, 100 }; // orange
+ Color m_colorExplainStartingWhen{ 255, 255, 232, 128 }; // yellow
+ Color m_colorExplainFailingWhen{ 255, 255, 192, 192 }; // red
+ Color m_colorExplainImpotent{ 255, 192, 192, 192 }; // gray
};
} // namespace ui
diff --git a/src/ui/viewmodels/TriggerSummaryViewModel.cpp b/src/ui/viewmodels/TriggerSummaryViewModel.cpp
new file mode 100644
index 00000000..536bd1f4
--- /dev/null
+++ b/src/ui/viewmodels/TriggerSummaryViewModel.cpp
@@ -0,0 +1,731 @@
+#include "TriggerSummaryViewModel.hh"
+
+#include "RA_Defs.h"
+
+#include "data\Memory.hh"
+#include "data\context\GameContext.hh"
+
+#include "services\ServiceLocator.hh"
+
+#include "ui\EditorTheme.hh"
+
+#include "util\Strings.hh"
+
+#include
+
+namespace ra {
+namespace ui {
+namespace viewmodels {
+
+const StringModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::IndicesProperty("TriggerClauseViewModel", "Indices", L"");
+const StringModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::ReferenceProperty("TriggerClauseViewModel", "Reference", L"0x0000");
+const StringModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::OperationProperty("TriggerClauseViewModel", "Operation", L"is");
+const StringModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::TargetProperty("TriggerClauseViewModel", "Target", L"");
+const StringModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::TallyProperty("TriggerClauseViewModel", "Tally", L"");
+const IntModelProperty TriggerSummaryViewModel::TriggerClauseViewModel::ColorProperty("TriggerClauseViewModel", "Color", 0);
+
+enum TriggerSummaryViewModel::TriggerClauseViewModel::TriggerClauseType : int
+{
+ None = 0,
+ Is, // direct equality comparison (=)
+ IsNot, // direct inequality comparison (!=)
+ Comparison, // open-ended comparison (> >= < <=)
+ AlwaysTrue, // logically impossible to ever be false
+ AlwaysFalse, // logically impossible to ever be true
+ Changed, // value differs from its delta
+ HasntChanged, // value doesn't differ from its delta
+ ChangedTo, // direct equality comparison (=) with a delta check
+ ChangedFrom, // direct inequality comparison (!=) with a delta check
+};
+
+using TriggerClauseType = TriggerSummaryViewModel::TriggerClauseViewModel::TriggerClauseType;
+
+static constexpr bool IsChangeType(TriggerClauseType nType)
+{
+ switch (nType)
+ {
+ case TriggerClauseType::Changed:
+ case TriggerClauseType::ChangedTo:
+ case TriggerClauseType::ChangedFrom:
+ // these are all specific checks of a memory address and its delta.
+ // as such, they'll only be true on the frame they change.
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+static bool IsSameMemoryReference(const rc_operand_t& pOperand1, const rc_operand_t& pOperand2) noexcept
+{
+ switch (pOperand1.type) {
+ case RC_OPERAND_CONST:
+ return (pOperand1.value.num == pOperand2.value.num);
+ case RC_OPERAND_FP:
+ return (pOperand1.value.dbl == pOperand2.value.dbl);
+ case RC_OPERAND_RECALL:
+ return (pOperand1.value.memref == pOperand2.value.memref);
+ default:
+ break;
+ }
+
+ if (pOperand1.size != pOperand2.size)
+ return false;
+
+ return (pOperand1.value.memref == pOperand2.value.memref);
+}
+
+static constexpr uint32_t ParseUInt32(std::wstring_view sValue)
+{
+ uint32_t nValue = 0;
+ for (auto c : sValue)
+ {
+ nValue *= 10;
+ nValue += (c - '0');
+ }
+
+ return nValue;
+}
+
+static void ParseRanges(std::vector>& vRanges, const std::wstring& sIndices)
+{
+ const std::wstring_view sRemaining = sIndices;
+ size_t nIndex;
+ do
+ {
+ nIndex = sRemaining.find(',');
+ const auto sRange = sRemaining.substr(0, nIndex);
+
+ const auto nIndex2 = sRange.find('-');
+ if (nIndex2 == std::wstring::npos)
+ {
+ const uint32_t nValue = ParseUInt32(sRange);
+ vRanges.push_back({ nValue, nValue });
+ }
+ else
+ {
+ const uint32_t nStart = ParseUInt32(sRange.substr(0, nIndex2));
+ const uint32_t nEnd = ParseUInt32(sRange.substr(nIndex2 + 1));
+ vRanges.push_back({ nStart, nEnd });
+ }
+ } while (nIndex < sRemaining.length());
+}
+
+static void MergeIndices(TriggerSummaryViewModel::TriggerClauseViewModel& pClause1, const TriggerSummaryViewModel::TriggerClauseViewModel& pClause2)
+{
+ std::vector> vRanges;
+ ParseRanges(vRanges, pClause1.GetIndices());
+ ParseRanges(vRanges, pClause2.GetIndices());
+
+ std::sort(vRanges.begin(), vRanges.end(), [](const std::pair& a, const std::pair& b) {
+ return a.first < b.first;
+ });
+
+ uint32_t nStart = 0xFFFFFFFF;
+ uint32_t nEnd = nStart;
+
+ std::wstring sNewIndices;
+ for (const auto& pRange : vRanges)
+ {
+ if (pRange.first == nEnd + 1)
+ {
+ nEnd = pRange.second;
+ }
+ else
+ {
+ if (nStart != 0xFFFFFFFF)
+ {
+ sNewIndices.append(std::to_wstring(nStart));
+ if (nStart != nEnd)
+ {
+ sNewIndices.push_back(L'-');
+ sNewIndices.append(std::to_wstring(nEnd));
+ }
+
+ sNewIndices.push_back(L',');
+ }
+
+ nStart = pRange.first;
+ nEnd = pRange.second;
+ }
+ }
+
+ sNewIndices.append(std::to_wstring(nStart));
+ if (nStart != nEnd)
+ {
+ sNewIndices.push_back(L'-');
+ sNewIndices.append(std::to_wstring(nEnd));
+ }
+
+ pClause1.SetIndices(sNewIndices);
+}
+
+bool TriggerSummaryViewModel::MergeClauses(TriggerSummaryViewModel::TriggerClauseViewModel& pClause,
+ const TriggerSummaryViewModel::TriggerClauseViewModel& pDiscardClause,
+ gsl::index nDiscardIndex, TriggerClauseType nNewType, const std::wstring& sNewOperation)
+{
+ pClause.SetOperation(sNewOperation);
+ pClause.nType = nNewType;
+ MergeIndices(pClause, pDiscardClause);
+ m_vClauses.RemoveAt(nDiscardIndex);
+ return true;
+}
+
+bool TriggerSummaryViewModel::MergeChangedToFrom(
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause1,
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause2,
+ gsl::index nIndex1, gsl::index nIndex2)
+{
+ if (pClause1.pCondition->operand1.size >= RC_MEMSIZE_BIT_0 &&
+ pClause1.pCondition->operand1.size <= RC_MEMSIZE_BIT_7)
+ {
+ // differing bit values is just a toggle
+ if (pClause1.pCondition->operand2.type == RC_OPERAND_CONST &&
+ pClause2.pCondition->operand2.type == RC_OPERAND_CONST &&
+ pClause1.pCondition->operand2.value.num != pClause2.pCondition->operand2.value.num)
+ {
+ if (pClause1.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ // a == n && da == ~n
+ return MergeClauses(pClause1, pClause2, nIndex2, TriggerClauseType::ChangedTo, L"changed to");
+ }
+ else if (pClause2.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ // da == ~n && a == n
+ return MergeClauses(pClause2, pClause1, nIndex1, TriggerClauseType::ChangedTo, L"changed to");
+ }
+
+ return false;
+ }
+ }
+
+ // differing non-bit values have to report both the before and after expectations
+ if (pClause1.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ pClause1.nType = TriggerClauseType::ChangedTo;
+ pClause1.SetOperation(L"changed to");
+ }
+ else
+ {
+ pClause1.nType = TriggerClauseType::ChangedFrom;
+ pClause1.SetOperation(L"changed from");
+ }
+
+ if (pClause2.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ pClause2.nType = TriggerClauseType::ChangedTo;
+ pClause2.SetOperation(L"changed to");
+ }
+ else
+ {
+ pClause2.nType = TriggerClauseType::ChangedFrom;
+ pClause2.SetOperation(L"changed from");
+ }
+
+ return false;
+}
+
+bool TriggerSummaryViewModel::MergeClauses(
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause1,
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause2,
+ gsl::index nIndex1, gsl::index nIndex2)
+{
+ if (pClause1.nType == TriggerClauseType::Is)
+ {
+ if (pClause2.nType == TriggerClauseType::IsNot)
+ {
+ if (IsSameMemoryReference(pClause1.pCondition->operand2, pClause2.pCondition->operand2))
+ {
+ if (pClause1.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ // a == x && da != x
+ return MergeClauses(pClause1, pClause2, nIndex2, TriggerClauseType::ChangedTo, L"changed to");
+ }
+ else if (pClause2.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ // da == x && a != x
+ return MergeClauses(pClause2, pClause1, nIndex1, TriggerClauseType::ChangedFrom, L"changed from");
+ }
+
+ return false;
+ }
+
+ // a == x && a != y ~> a == x
+ m_vClauses.RemoveAt(nIndex2);
+ return true;
+ }
+ else if (pClause2.nType == TriggerClauseType::Comparison &&
+ pClause1.pCondition->operand1.memref_access_type == RC_OPERAND_ADDRESS)
+ {
+ if (IsSameMemoryReference(pClause1.pCondition->operand2, pClause2.pCondition->operand2))
+ {
+ if (pClause2.pCondition->oper == RC_OPERATOR_LT)
+ {
+ // a == x && da < x
+ return MergeClauses(pClause1, pClause2, nIndex2, TriggerClauseType::ChangedTo, L"increased to");
+ }
+ else if (pClause2.pCondition->oper == RC_OPERATOR_GT)
+ {
+ // a == x && da > x
+ return MergeClauses(pClause1, pClause2, nIndex2, TriggerClauseType::ChangedTo, L"decreased to");
+ }
+ }
+ }
+ else if (pClause2.nType == TriggerClauseType::Is)
+ {
+ return MergeChangedToFrom(pClause1, pClause2, nIndex1, nIndex2);
+ }
+ }
+ else if (pClause2.nType == TriggerClauseType::Is)
+ {
+ return MergeClauses(pClause2, pClause1, nIndex2, nIndex1);
+ }
+
+ return false;
+}
+
+static constexpr bool IsValueCharacter(wchar_t c)
+{
+ if (c >= '0' && c <= '9')
+ return true;
+ if (c >= 'a' && c <= 'f')
+ return true;
+ if (c >= 'A' && c <= 'F')
+ return true;
+ if (isspace(c))
+ return true;
+ if (c == 'h' || c == 'H' || c == 'x' || c == '-')
+ return true;
+ return false;
+}
+
+static std::wstring EnumValueFromText(std::wstring_view sEnumText)
+{
+ const auto nSplit = sEnumText.find_first_of(L"=:");
+ auto sEnumDescription = sEnumText.substr(nSplit + 1);
+ for (size_t i = 0; i < nSplit; ++i)
+ {
+ if (!IsValueCharacter(sEnumText.at(i)))
+ {
+ sEnumDescription = sEnumText.substr(0, nSplit);
+ break;
+ }
+ }
+
+ auto nIndex = sEnumDescription.find_first_not_of(L" \t\n\r");
+ if (nIndex != 0 && nIndex != std::string::npos)
+ sEnumDescription = sEnumDescription.substr(nIndex);
+
+ nIndex = sEnumDescription.find_last_not_of(L" \t\n\r");
+ if (nIndex < sEnumDescription.length() - 1)
+ sEnumDescription = sEnumDescription.substr(0, nIndex);
+
+ return std::wstring(sEnumDescription);
+}
+
+static void HandleOperation(TriggerSummaryViewModel::TriggerClauseViewModel& pClause, uint8_t nOperation)
+{
+ std::wstring sOperation;
+ if (rc_operand_is_memref(&pClause.pCondition->operand1))
+ {
+ switch (pClause.pCondition->operand1.type)
+ {
+ case RC_OPERAND_ADDRESS:
+ sOperation = L"is";
+ break;
+
+ case RC_OPERAND_PRIOR:
+ sOperation = L"was";
+ break;
+
+ case RC_OPERAND_DELTA:
+ sOperation = L"last frame was";
+ break;
+ }
+ }
+ else
+ {
+ sOperation = L"is";
+ }
+
+ switch (nOperation)
+ {
+ case RC_OPERATOR_EQ:
+ pClause.nType = TriggerClauseType::Is;
+ break;
+
+ case RC_OPERATOR_NE:
+ sOperation += L" not";
+ pClause.nType = TriggerClauseType::IsNot;
+ break;
+
+ case RC_OPERATOR_GE:
+ sOperation += L" at least";
+ pClause.nType = TriggerClauseType::Comparison;
+ break;
+
+ case RC_OPERATOR_GT:
+ sOperation += L" greater than";
+ pClause.nType = TriggerClauseType::Comparison;
+ break;
+
+ case RC_OPERATOR_LE:
+ sOperation += L" at most";
+ pClause.nType = TriggerClauseType::Comparison;
+ break;
+
+ case RC_OPERATOR_LT:
+ sOperation += L" less than";
+ pClause.nType = TriggerClauseType::Comparison;
+ break;
+ }
+
+ pClause.SetOperation(sOperation);
+}
+
+static std::wstring OperandToString(const rc_operand_t& pOperand)
+{
+ switch (pOperand.type)
+ {
+ case RC_OPERAND_CONST:
+ return std::to_wstring(pOperand.value.num);
+
+ case RC_OPERAND_ADDRESS:
+ case RC_OPERAND_DELTA:
+ case RC_OPERAND_PRIOR:
+ {
+ const auto& pMemoryContext = ra::services::ServiceLocator::Get();
+ return ra::util::String::Widen(pMemoryContext.FormatAddress(pOperand.value.memref->address));
+ }
+
+ default:
+ return L"???";
+ }
+}
+
+static void HandleTally(TriggerSummaryViewModel::TriggerClauseViewModel& pClause, const rc_condition_t& pCondition)
+{
+ if (pCondition.required_hits == 1)
+ {
+ // don't say "do something once" for a single captured hit starting condition
+ if (pCondition.type != RC_CONDITION_STANDARD)
+ pClause.SetTally(L"once");
+ }
+ else if (pCondition.required_hits > 1)
+ {
+ if (IsChangeType(pClause.nType))
+ pClause.SetTally(ra::util::String::Printf(L"%u times", pCondition.required_hits));
+ else
+ pClause.SetTally(ra::util::String::Printf(L"for %u frames", pCondition.required_hits));
+ }
+}
+
+static void HandleCompareMemoryReferenceToSelf(TriggerSummaryViewModel::TriggerClauseViewModel& pClause, const rc_condition_t& pCondition)
+{
+ pClause.SetTarget(L"");
+
+ if (pCondition.operand1.memref_access_type == pCondition.operand2.memref_access_type)
+ {
+ switch (pCondition.oper)
+ {
+ case RC_OPERATOR_EQ:
+ case RC_OPERATOR_GE:
+ case RC_OPERATOR_LE:
+ // delta = delta ~> always true
+ pClause.SetOperation(L"unimportant");
+ pClause.nType = TriggerClauseType::AlwaysTrue;
+ break;
+
+ case RC_OPERATOR_NE:
+ case RC_OPERATOR_GT:
+ case RC_OPERATOR_LT:
+ // delta != delta ~> always false
+ pClause.SetOperation(L"invalid");
+ pClause.nType = TriggerClauseType::AlwaysFalse;
+ break;
+ }
+ }
+ else
+ {
+ switch (pCondition.oper)
+ {
+ case RC_OPERATOR_EQ:
+ pClause.SetOperation(L"hasn't changed");
+ pClause.nType = TriggerClauseType::HasntChanged;
+ break;
+
+ case RC_OPERATOR_NE:
+ pClause.SetOperation(L"changed");
+ break;
+
+ case RC_OPERATOR_LT:
+ pClause.SetOperation(pCondition.operand1.memref_access_type == RC_OPERAND_ADDRESS
+ ? L"decreased" // val < delta
+ : L"increased"); // delta < val
+ pClause.nType = TriggerClauseType::Changed;
+ break;
+
+ case RC_OPERATOR_GT:
+ pClause.SetOperation(pCondition.operand1.memref_access_type == RC_OPERAND_ADDRESS
+ ? L"increased" // val > delta
+ : L"decreased"); // delta > val
+ pClause.nType = TriggerClauseType::Changed;
+ break;
+
+ case RC_OPERATOR_LE:
+ pClause.SetOperation(pCondition.operand1.memref_access_type == RC_OPERAND_ADDRESS
+ ? L"did not increase" // val <= delta
+ : L"did not decrease"); // delta <= val
+ pClause.nType = TriggerClauseType::Changed;
+ break;
+
+ case RC_OPERATOR_GE:
+ pClause.SetOperation(pCondition.operand1.memref_access_type == RC_OPERAND_ADDRESS
+ ? L"did not decrease" // val >= delta
+ : L"did not increase"); // delta >= val
+ pClause.nType = TriggerClauseType::Changed;
+ break;
+ }
+ }
+}
+
+void TriggerSummaryViewModel::InitializeFrom(const rc_condset_t& pCondSet)
+{
+ uint32_t nFirstIndex = 0;
+ uint32_t nLastIndex = 0;
+
+ const auto* pMemoryNotes = ra::services::ServiceLocator::Get().Assets().FindMemoryNotes();
+
+ const auto* pCondition = pCondSet.conditions;
+ for (; pCondition; pCondition = pCondition->next)
+ {
+ auto& pClause = m_vClauses.Add();
+
+ nFirstIndex = nLastIndex + 1;
+ nLastIndex = nFirstIndex;
+ if (pCondition->type == RC_CONDITION_ADD_ADDRESS)
+ {
+ do
+ {
+ nLastIndex++;
+ pCondition = pCondition->next;
+ } while (pCondition && pCondition->type == RC_CONDITION_ADD_ADDRESS);
+
+ if (!pCondition)
+ {
+ m_vClauses.RemoveAt(m_vClauses.Count() - 1);
+ break;
+ }
+
+ pClause.SetIndices(ra::util::String::Printf(L"%u-%u", nFirstIndex, nLastIndex));
+ }
+ else
+ {
+ pClause.SetIndices(std::to_wstring(nFirstIndex));
+ }
+
+ pClause.pCondition = pCondition;
+
+ const ra::data::models::MemoryNoteModel* pNote = nullptr;
+ if (pMemoryNotes && rc_operand_is_memref(&pCondition->operand1))
+ pNote = pMemoryNotes->FindMemoryNoteModel(pCondition->operand1.value.memref->address);
+
+ if (pNote)
+ {
+ const auto pSubNote = pNote->GetSubNote(ra::data::Memory::SizeFromRcheevosSize(pCondition->operand1.size));
+ if (!pSubNote.empty())
+ pClause.SetReference(EnumValueFromText(pSubNote));
+ else
+ pClause.SetReference(pNote->GetSummary());
+ }
+ else
+ {
+ pClause.SetReference(OperandToString(pCondition->operand1));
+ }
+
+ HandleOperation(pClause, pCondition->oper);
+
+ if (rc_operand_is_memref(&pCondition->operand2))
+ {
+ if (pCondition->operand1.value.memref == pCondition->operand2.value.memref)
+ {
+ // comparing value to itself
+ HandleCompareMemoryReferenceToSelf(pClause, *pCondition);
+ HandleTally(pClause, *pCondition);
+ continue;
+ }
+ }
+ else if (pNote)
+ {
+ auto nTarget = pCondition->operand2.value.num;
+
+ // a < 1 ~> a == 0
+ if (nTarget == 1 && pCondition->oper == RC_OPERATOR_LT)
+ {
+ nTarget = 0;
+ HandleOperation(pClause, RC_OPERATOR_EQ);
+ }
+
+ 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));
+ }
+
+ HandleTally(pClause, *pCondition);
+
+ if (rc_operand_is_memref(&pCondition->operand1))
+ {
+ for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vClauses.Count()) - 1; ++nIndex)
+ {
+ auto* pOtherClause = m_vClauses.GetItemAt(nIndex);
+ if (pOtherClause && IsSameMemoryReference(pCondition->operand1, pOtherClause->pCondition->operand1))
+ {
+ if (MergeClauses(*pOtherClause, pClause, nIndex, m_vClauses.Count() - 1))
+ break;
+ }
+ }
+ }
+ }
+}
+
+void TriggerSummaryViewModel::AddHeaders()
+{
+ enum class TriggerClauseBucket
+ {
+ None,
+ Trigger,
+ Ongoing,
+ Unless,
+ Start,
+ Restart,
+ Unimportant,
+ Conflicting,
+
+ Count,
+ };
+
+ std::vector> vBuckets(ra::etoi(TriggerClauseBucket::Count));
+
+ for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vClauses.Count()); ++nIndex)
+ {
+ auto* pClause = m_vClauses.GetItemAt(nIndex);
+ if (!pClause)
+ continue;
+
+ auto nBucket = TriggerClauseBucket::Ongoing;
+
+ switch (pClause->nType)
+ {
+ default:
+ // anything that is only true for one frame should be classified as a trigger.
+ if (IsChangeType(pClause->nType))
+ nBucket = TriggerClauseBucket::Trigger;
+ break;
+
+ case TriggerClauseType::AlwaysTrue:
+ nBucket = TriggerClauseBucket::Unimportant;
+ break;
+
+ case TriggerClauseType::AlwaysFalse:
+ nBucket = TriggerClauseBucket::Conflicting;
+ break;
+ }
+
+ switch (pClause->pCondition->type)
+ {
+ case RC_CONDITION_PAUSE_IF:
+ nBucket = TriggerClauseBucket::Unless;
+ break;
+
+ case RC_CONDITION_RESET_IF:
+ nBucket = TriggerClauseBucket::Restart;
+ break;
+
+ case RC_CONDITION_TRIGGER:
+ nBucket = TriggerClauseBucket::Trigger;
+ break;
+
+ default:
+ if (pClause->pCondition->required_hits > 0)
+ nBucket = TriggerClauseBucket::Start;
+ break;
+ }
+
+ vBuckets.at(ra::etoi(nBucket)).push_back(pClause);
+ }
+
+ const auto& pTheme = ra::services::ServiceLocator::Get();
+
+ gsl::index nInsertIndex = 0;
+ auto fBuildGroup = [this, &vBuckets, &nInsertIndex](TriggerClauseBucket nBucket, const std::wstring& sHeader, ra::ui::Color nColor)
+ {
+ const auto& vBucketItems = vBuckets.at(ra::etoi(nBucket));
+ if (vBucketItems.empty())
+ return;
+
+ auto& vmHeader = m_vClauses.Add();
+ vmHeader.SetReference(sHeader);
+ vmHeader.SetOperation(L"");
+ vmHeader.SetColor(nColor);
+ m_vClauses.MoveItem(m_vClauses.Count() - 1, nInsertIndex++);
+
+ for (const auto* pClause : vBucketItems)
+ {
+ for (gsl::index nIndex = nInsertIndex; nIndex < gsl::narrow_cast(m_vClauses.Count()); ++nIndex)
+ {
+ if (m_vClauses.GetItemAt(nIndex) == pClause)
+ {
+ if (nIndex != nInsertIndex)
+ m_vClauses.MoveItem(nIndex, nInsertIndex);
+ ++nInsertIndex;
+ break;
+ }
+ }
+ }
+ };
+
+ fBuildGroup(TriggerClauseBucket::Conflicting, L"--- CONFLICTING ---", pTheme.ColorExplainConflicting());
+
+ if (!vBuckets.at(ra::etoi(TriggerClauseBucket::Trigger)).empty())
+ {
+ fBuildGroup(TriggerClauseBucket::Trigger, L"--- TRIGGER WHEN ---", pTheme.ColorExplainTriggerWhen());
+ fBuildGroup(TriggerClauseBucket::Ongoing, L"--- WHILE ---", pTheme.ColorExplainWhile());
+ }
+ else
+ {
+ // No trigger clauses. Promote the Ongoing clauses to trigger clauses
+ fBuildGroup(TriggerClauseBucket::Ongoing, L"--- TRIGGER WHEN ---", pTheme.ColorExplainTriggerWhen());
+ }
+
+ const auto vUnlessItems = vBuckets.at(ra::etoi(TriggerClauseBucket::Unless));
+ if (!vUnlessItems.empty()) {
+ fBuildGroup(TriggerClauseBucket::Unless, vUnlessItems.size() > 1 ? L"--- UNLESS ANY ---" : L"--- UNLESS ---", pTheme.ColorExplainUnless());
+ }
+
+ fBuildGroup(TriggerClauseBucket::Start, L"--- STARTING WHEN ---", pTheme.ColorExplainStartingWhen());
+
+ const auto vRestartItems = vBuckets.at(ra::etoi(TriggerClauseBucket::Restart));
+ if (!vRestartItems.empty()) {
+ fBuildGroup(TriggerClauseBucket::Restart, vRestartItems.size() > 1 ? L"--- FAILING WHEN ANY ---" : L"--- FAILING WHEN ---", pTheme.ColorExplainFailingWhen());
+ }
+
+ fBuildGroup(TriggerClauseBucket::Unimportant, L"--- IMPOTENT ---", pTheme.ColorExplainImpotent());
+}
+
+} // namespace viewmodels
+} // namespace ui
+} // namespace ra
diff --git a/src/ui/viewmodels/TriggerSummaryViewModel.hh b/src/ui/viewmodels/TriggerSummaryViewModel.hh
new file mode 100644
index 00000000..02db0085
--- /dev/null
+++ b/src/ui/viewmodels/TriggerSummaryViewModel.hh
@@ -0,0 +1,84 @@
+#ifndef RA_UI_TRIGGERSUMMARYVIEWMODEL_H
+#define RA_UI_TRIGGERSUMMARYVIEWMODEL_H
+#pragma once
+
+#include "ui\WindowViewModelBase.hh"
+#include "ui\ViewModelCollection.hh"
+#include "ui\Types.hh"
+
+struct rc_condset_t;
+
+namespace ra {
+namespace ui {
+namespace viewmodels {
+
+class TriggerSummaryViewModel : public WindowViewModelBase
+{
+public:
+ GSL_SUPPRESS_F6 TriggerSummaryViewModel() noexcept = default;
+ ~TriggerSummaryViewModel() = default;
+
+ TriggerSummaryViewModel(const TriggerSummaryViewModel&) noexcept = delete;
+ TriggerSummaryViewModel& operator=(const TriggerSummaryViewModel&) noexcept = delete;
+ TriggerSummaryViewModel(TriggerSummaryViewModel&&) noexcept = delete;
+ TriggerSummaryViewModel& operator=(TriggerSummaryViewModel&&) noexcept = delete;
+
+ void InitializeFrom(const rc_condset_t& pCondSet);
+ void AddHeaders();
+
+ class TriggerClauseViewModel : public ViewModelBase
+ {
+ public:
+ static const StringModelProperty IndicesProperty;
+ const std::wstring& GetIndices() const { return GetValue(IndicesProperty); }
+ void SetIndices(const std::wstring& sValue) { SetValue(IndicesProperty, sValue); }
+
+ static const StringModelProperty ReferenceProperty;
+ const std::wstring& GetReference() const { return GetValue(ReferenceProperty); }
+ void SetReference(const std::wstring& sValue) { SetValue(ReferenceProperty, sValue); }
+
+ static const StringModelProperty OperationProperty;
+ const std::wstring& GetOperation() const { return GetValue(OperationProperty); }
+ void SetOperation(const std::wstring& sValue) { SetValue(OperationProperty, sValue); }
+
+ static const StringModelProperty TargetProperty;
+ const std::wstring& GetTarget() const { return GetValue(TargetProperty); }
+ void SetTarget(const std::wstring& sValue) { SetValue(TargetProperty, sValue); }
+
+ static const StringModelProperty TallyProperty;
+ const std::wstring& GetTally() const { return GetValue(TallyProperty); }
+ void SetTally(const std::wstring& sValue) { SetValue(TallyProperty, sValue); }
+
+ static const IntModelProperty ColorProperty;
+ Color GetColor() const { return Color(ra::to_unsigned(GetValue(ColorProperty))); }
+ void SetColor(Color value) { SetValue(ColorProperty, ra::to_signed(value.ARGB)); }
+
+ const rc_condition_t* pCondition = nullptr;
+ enum TriggerClauseType : int;
+ TriggerClauseType nType = ra::itoe(0);
+ };
+
+ ViewModelCollection& Clauses() noexcept { return m_vClauses; }
+ const ViewModelCollection& Clauses() const noexcept { return m_vClauses; }
+
+private:
+ bool MergeClauses(
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause1,
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause2,
+ gsl::index nIndex1, gsl::index nIndex2);
+ bool MergeChangedToFrom(
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause1,
+ TriggerSummaryViewModel::TriggerClauseViewModel& pClause2,
+ gsl::index nIndex1, gsl::index nIndex2);
+ bool MergeClauses(TriggerSummaryViewModel::TriggerClauseViewModel& pClause,
+ const TriggerSummaryViewModel::TriggerClauseViewModel& pDiscardClause,
+ gsl::index nDiscardIndex, TriggerClauseViewModel::TriggerClauseType nNewType, const std::wstring& sNewOperation);
+
+ ViewModelCollection m_vClauses;
+};
+
+} // namespace viewmodels
+} // namespace ui
+} // namespace ra
+
+#endif !RA_UI_TRIGGERSUMMARYVIEWMODEL_H
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index 2e5b9433..b2fc4eaa 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -20,6 +20,7 @@
#include "ui\EditorTheme.hh"
#include "ui\viewmodels\MessageBoxViewModel.hh"
#include "ui\viewmodels\WindowManager.hh"
+#include "ui\viewmodels\TriggerSummaryViewModel.hh"
namespace ra {
namespace ui {
@@ -938,6 +939,30 @@ void TriggerViewModel::UpdateFrom(const rc_value_t& pValue)
UpdateGroups(*m_pTrigger);
}
+void TriggerViewModel::Summarize()
+{
+ auto* pGroup = m_vGroups.GetItemAt(GetSelectedGroupIndex());
+ if (!pGroup)
+ return;
+
+ const auto* pCondSet = pGroup->GetConditionSet(IsValue());
+ if (!pCondSet)
+ return;
+
+ const auto& pAssetEditor = ra::services::ServiceLocator::Get().AssetEditor;
+ TriggerSummaryViewModel vmSummary;
+
+ if (m_vGroups.Count() == 1)
+ vmSummary.SetWindowTitle(L"Trigger Summary - " + pAssetEditor.GetAsset()->GetName());
+ else
+ vmSummary.SetWindowTitle(pGroup->GetLabel() + L" Summary - " + pAssetEditor.GetAsset()->GetName());
+
+ vmSummary.InitializeFrom(*pCondSet);
+ vmSummary.AddHeaders();
+
+ vmSummary.ShowModal(pAssetEditor);
+}
+
void TriggerViewModel::SelectRange(gsl::index nFrom, gsl::index nTo, bool bValue)
{
m_vConditions.RemoveNotifyTarget(*this);
diff --git a/src/ui/viewmodels/TriggerViewModel.hh b/src/ui/viewmodels/TriggerViewModel.hh
index 6e5a9c98..61ab945b 100644
--- a/src/ui/viewmodels/TriggerViewModel.hh
+++ b/src/ui/viewmodels/TriggerViewModel.hh
@@ -122,6 +122,8 @@ public:
void AddGroup();
void RemoveGroup();
+ void Summarize();
+
///
/// Gets the list of condition types.
///
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 3b056bb7..76def23a 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -842,6 +842,7 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
m_bindWindow.BindEnabled(IDC_RA_ADD_GROUP, AssetEditorViewModel::IsAssetLoadedProperty);
m_bindWindow.BindEnabled(IDC_RA_DELETE_GROUP, AssetEditorViewModel::IsAssetLoadedProperty);
m_bindWindow.BindEnabled(IDC_RA_COPY_ALL, AssetEditorViewModel::IsAssetLoadedProperty);
+ m_bindWindow.BindEnabled(IDC_RA_VIEW_DETAIL, AssetEditorViewModel::IsAssetLoadedProperty);
m_bindWindow.BindEnabled(IDC_RA_LBX_CONDITIONS, AssetEditorViewModel::IsAssetLoadedProperty);
m_bindWindow.BindEnabled(IDC_RA_ADD_COND, AssetEditorViewModel::IsAssetLoadedProperty);
m_bindWindow.BindEnabled(IDC_RA_DELETE_COND, AssetEditorViewModel::IsAssetLoadedProperty);
@@ -1025,6 +1026,7 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
SetAnchor(IDC_RA_DELETE_GROUP, Anchor::Bottom | Anchor::Left);
SetAnchor(IDC_RA_COPY_ALL, Anchor::Bottom | Anchor::Left);
SetAnchor(IDC_RA_ERROR_INDICATOR, Anchor::Top | Anchor::Left);
+ SetAnchor(IDC_RA_VIEW_DETAIL, Anchor::Top | Anchor::Right);
SetAnchor(IDC_RA_CHK_HIGHLIGHTS, Anchor::Top | Anchor::Right);
SetAnchor(IDC_RA_CHK_PAUSE_ON_RESET, Anchor::Top | Anchor::Right);
SetAnchor(IDC_RA_CHK_PAUSE_ON_TRIGGER, Anchor::Top | Anchor::Right);
@@ -1090,6 +1092,10 @@ BOOL AssetEditorDialog::OnInitDialog()
SendMessage(::GetDlgItem(GetHWND(), IDC_RA_TYPE), CB_SETDROPPEDWIDTH, 70, 0);
SendMessage(::GetDlgItem(GetHWND(), IDC_RA_FORMAT), CB_SETDROPPEDWIDTH, 136, 0);
+#ifndef _DEBUG
+ ShowWindow(::GetDlgItem(GetHWND(), IDC_RA_VIEW_DETAIL), SW_HIDE);
+#endif
+
return DialogBase::OnInitDialog();
}
@@ -1235,6 +1241,15 @@ BOOL AssetEditorDialog::OnCommand(WORD nCommand)
return TRUE;
}
+
+ case IDC_RA_VIEW_DETAIL:
+ {
+ auto* vmAssetEditor = dynamic_cast(&m_vmWindow);
+ if (vmAssetEditor)
+ vmAssetEditor->Trigger().Summarize();
+
+ return TRUE;
+ }
}
return DialogBase::OnCommand(nCommand);
diff --git a/src/ui/win32/Desktop.cpp b/src/ui/win32/Desktop.cpp
index e0f64f1c..4a68bc5a 100644
--- a/src/ui/win32/Desktop.cpp
+++ b/src/ui/win32/Desktop.cpp
@@ -23,6 +23,7 @@
#include "ui/win32/PointerInspectorDialog.hh"
#include "ui/win32/ProgressDialog.hh"
#include "ui/win32/RichPresenceDialog.hh"
+#include "ui/win32/TriggerSummaryDialog.hh"
#include "ui/win32/UnknownGameDialog.hh"
#include "ui/win32/bindings/ControlBinding.hh"
@@ -51,6 +52,7 @@ Desktop::Desktop() noexcept
m_vDialogPresenters.emplace_back(new (std::nothrow) AssetEditorDialog::Presenter);
m_vDialogPresenters.emplace_back(new (std::nothrow) PointerFinderDialog::Presenter);
m_vDialogPresenters.emplace_back(new (std::nothrow) PointerInspectorDialog::Presenter);
+ m_vDialogPresenters.emplace_back(new (std::nothrow) TriggerSummaryDialog::Presenter);
m_vDialogPresenters.emplace_back(new (std::nothrow) FileDialog::Presenter);
m_vDialogPresenters.emplace_back(new (std::nothrow) OverlaySettingsDialog::Presenter);
m_vDialogPresenters.emplace_back(new (std::nothrow) NewAssetDialog::Presenter);
diff --git a/src/ui/win32/TriggerSummaryDialog.cpp b/src/ui/win32/TriggerSummaryDialog.cpp
new file mode 100644
index 00000000..08b85b39
--- /dev/null
+++ b/src/ui/win32/TriggerSummaryDialog.cpp
@@ -0,0 +1,107 @@
+#include "TriggerSummaryDialog.hh"
+
+#include "RA_Resource.h"
+
+#include "data\context\EmulatorContext.hh"
+
+#include "ui\viewmodels\MessageBoxViewModel.hh"
+#include "ui\viewmodels\PointerInspectorViewModel.hh"
+
+#include "ui\win32\bindings\GridAddressColumnBinding.hh"
+#include "ui\win32\bindings\GridMemoryWatchFormatColumnBinding.hh"
+#include "ui\win32\bindings\GridMemoryWatchValueColumnBinding.hh"
+#include "ui\win32\bindings\GridLookupColumnBinding.hh"
+#include "ui\win32\bindings\GridNumberColumnBinding.hh"
+#include "ui\win32\bindings\GridTextColumnBinding.hh"
+
+#include "util\EnumOps.hh"
+#include "util\Log.hh"
+
+using ra::ui::viewmodels::TriggerSummaryViewModel;
+using ra::ui::win32::bindings::GridColumnBinding;
+
+namespace ra {
+namespace ui {
+namespace win32 {
+
+bool TriggerSummaryDialog::Presenter::IsSupported(const ra::ui::WindowViewModelBase& vmViewModel) noexcept
+{
+ return (dynamic_cast(&vmViewModel) != nullptr);
+}
+
+void TriggerSummaryDialog::Presenter::ShowModal(ra::ui::WindowViewModelBase& vmViewModel, HWND hParentWnd)
+{
+ auto* vmTriggerSummary = dynamic_cast(&vmViewModel);
+ Expects(vmTriggerSummary != nullptr);
+
+ TriggerSummaryDialog oDialog(*vmTriggerSummary);
+ oDialog.CreateModalWindow(MAKEINTRESOURCE(IDD_RA_TRIGGERSUMMARY), this, hParentWnd);
+}
+
+void TriggerSummaryDialog::Presenter::ShowWindow(ra::ui::WindowViewModelBase& vmViewModel)
+{
+ ShowModal(vmViewModel, nullptr);
+}
+
+// ------------------------------------
+
+TriggerSummaryDialog::TriggerSummaryDialog(TriggerSummaryViewModel& vmTriggerSummary)
+ : DialogBase(vmTriggerSummary),
+ m_bindClauses(vmTriggerSummary)
+{
+ m_bindWindow.SetInitialPosition(RelativePosition::Center, RelativePosition::Center, "Trigger Summary");
+
+ auto pIndicesColumn = std::make_unique(
+ TriggerSummaryViewModel::TriggerClauseViewModel::IndicesProperty);
+ pIndicesColumn->SetHeader(L"Indices");
+ pIndicesColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 80);
+ pIndicesColumn->SetAlignment(ra::ui::RelativePosition::Far);
+ m_bindClauses.BindColumn(0, std::move(pIndicesColumn));
+
+ auto pReferenceColumn = std::make_unique(
+ TriggerSummaryViewModel::TriggerClauseViewModel::ReferenceProperty);
+ pReferenceColumn->SetHeader(L"Reference");
+ pReferenceColumn->SetWidth(GridColumnBinding::WidthType::Fill, 40);
+ pReferenceColumn->BindTooltip(TriggerSummaryViewModel::TriggerClauseViewModel::ReferenceProperty);
+ m_bindClauses.BindColumn(1, std::move(pReferenceColumn));
+
+ auto pOperationColumn = std::make_unique(
+ TriggerSummaryViewModel::TriggerClauseViewModel::OperationProperty);
+ pOperationColumn->SetHeader(L"Operation");
+ pOperationColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 140);
+ m_bindClauses.BindColumn(2, std::move(pOperationColumn));
+
+ auto pTargetColumn = std::make_unique(
+ TriggerSummaryViewModel::TriggerClauseViewModel::TargetProperty);
+ pTargetColumn->SetHeader(L"Target");
+ pTargetColumn->SetWidth(GridColumnBinding::WidthType::Fill, 40);
+ pTargetColumn->BindTooltip(TriggerSummaryViewModel::TriggerClauseViewModel::TargetProperty);
+ m_bindClauses.BindColumn(3, std::move(pTargetColumn));
+
+ auto pTallyColumn = std::make_unique(
+ TriggerSummaryViewModel::TriggerClauseViewModel::TallyProperty);
+ pTallyColumn->SetHeader(L"Tally");
+ pTallyColumn->SetWidth(GridColumnBinding::WidthType::Pixels, 60);
+ m_bindClauses.BindColumn(4, std::move(pTallyColumn));
+
+ m_bindClauses.BindRowColor(TriggerSummaryViewModel::TriggerClauseViewModel::ColorProperty);
+ m_bindClauses.BindItems(vmTriggerSummary.Clauses());
+
+ using namespace ra::bitwise_ops;
+ SetAnchor(IDC_RA_LBX_CONDITIONS, Anchor::Top | Anchor::Left | Anchor::Bottom | Anchor::Right);
+ SetAnchor(IDOK, Anchor::Bottom | Anchor::Right);
+
+ SetMinimumSize(480, 192);
+}
+
+BOOL TriggerSummaryDialog::OnInitDialog()
+{
+ m_bindClauses.SetControl(*this, IDC_RA_LBX_CONDITIONS);
+ m_bindClauses.InitializeTooltips(std::chrono::seconds(30));
+
+ return DialogBase::OnInitDialog();
+}
+
+} // namespace win32
+} // namespace ui
+} // namespace ra
diff --git a/src/ui/win32/TriggerSummaryDialog.hh b/src/ui/win32/TriggerSummaryDialog.hh
new file mode 100644
index 00000000..1fc10bbb
--- /dev/null
+++ b/src/ui/win32/TriggerSummaryDialog.hh
@@ -0,0 +1,43 @@
+#ifndef RA_UI_WIN32_DLG_TRIGGERSUMMARY_H
+#define RA_UI_WIN32_DLG_TRIGGERSUMMARY_H
+#pragma once
+
+#include "ui/viewmodels/TriggerSummaryViewModel.hh"
+#include "ui/win32/bindings/GridBinding.hh"
+#include "ui/win32/DialogBase.hh"
+#include "ui/win32/IDialogPresenter.hh"
+
+namespace ra {
+namespace ui {
+namespace win32 {
+
+class TriggerSummaryDialog : public DialogBase
+{
+public:
+ explicit TriggerSummaryDialog(ra::ui::viewmodels::TriggerSummaryViewModel& vmTriggerSummary);
+ virtual ~TriggerSummaryDialog() noexcept = default;
+ TriggerSummaryDialog(const TriggerSummaryDialog&) noexcept = delete;
+ TriggerSummaryDialog& operator=(const TriggerSummaryDialog&) noexcept = delete;
+ TriggerSummaryDialog(TriggerSummaryDialog&&) noexcept = delete;
+ TriggerSummaryDialog& operator=(TriggerSummaryDialog&&) noexcept = delete;
+
+ class Presenter : public IDialogPresenter
+ {
+ public:
+ bool IsSupported(const ra::ui::WindowViewModelBase& viewModel) noexcept override;
+ void ShowWindow(ra::ui::WindowViewModelBase& viewModel) override;
+ void ShowModal(ra::ui::WindowViewModelBase& viewModel, HWND hParentWnd) override;
+ };
+
+protected:
+ BOOL OnInitDialog() override;
+
+private:
+ ra::ui::win32::bindings::GridBinding m_bindClauses;
+};
+
+} // namespace win32
+} // namespace ui
+} // namespace ra
+
+#endif // !RA_UI_WIN32_DLG_TRIGGERSUMMARY_H
diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj
index 64cc0a99..2afbc63d 100644
--- a/tests/RA_Integration.Tests.vcxproj
+++ b/tests/RA_Integration.Tests.vcxproj
@@ -142,6 +142,7 @@
+
@@ -205,6 +206,7 @@
+
diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters
index 74656eab..d768d3bc 100644
--- a/tests/RA_Integration.Tests.vcxproj.filters
+++ b/tests/RA_Integration.Tests.vcxproj.filters
@@ -423,6 +423,12 @@
Code
+
+ Code
+
+
+ Tests\UI\ViewModels
+
Code
diff --git a/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp
new file mode 100644
index 00000000..918283fc
--- /dev/null
+++ b/tests/ui/viewmodels/TriggerSummaryViewModel_Tests.cpp
@@ -0,0 +1,311 @@
+#include "CppUnitTest.h"
+
+#include "ui\viewmodels\TriggerSummaryViewModel.hh"
+
+#include "tests\devkit\context\mocks\MockConsoleContext.hh"
+#include "tests\devkit\context\mocks\MockEmulatorMemoryContext.hh"
+#include "tests\devkit\context\mocks\MockUserContext.hh"
+#include "tests\mocks\MockGameContext.hh"
+
+#include "ui\EditorTheme.hh"
+
+using namespace Microsoft::VisualStudio::CppUnitTestFramework;
+
+namespace ra {
+namespace ui {
+namespace viewmodels {
+namespace tests {
+
+TEST_CLASS(TriggerSummaryViewModel_Tests)
+{
+private:
+ class TriggerSummaryViewModelHarness : public TriggerSummaryViewModel
+ {
+ public:
+ ra::context::mocks::MockEmulatorMemoryContext mockEmulatorMemoryContext;
+ ra::context::mocks::MockUserContext mockUserContext;
+ ra::data::context::mocks::MockGameContext mockGameContext;
+
+ using TriggerSummaryViewModel::InitializeFrom;
+
+ void InitializeFrom(const std::string& sTrigger)
+ {
+ const auto nSize = rc_trigger_size(sTrigger.c_str());
+ if (nSize > 0)
+ {
+ m_sTriggerBuffer.resize(nSize);
+ rc_trigger_t* pTrigger = rc_parse_trigger(m_sTriggerBuffer.data(), sTrigger.c_str(), nullptr, 0);
+ TriggerSummaryViewModel::InitializeFrom(*pTrigger->requirement);
+ }
+ }
+
+ void AssertClause(gsl::index nIndex, const std::wstring& sIndices,
+ const std::wstring& sReference, const std::wstring& sOperation,
+ const std::wstring& sTarget, const std::wstring& sTally = L"")
+ {
+ const auto* pClause = Clauses().GetItemAt(nIndex);
+ Assert::IsNotNull(pClause);
+ Assert::AreEqual(sIndices, pClause->GetIndices(), ra::util::String::Printf(L"Indices on clause %u differ", nIndex).c_str());
+ Assert::AreEqual(sReference, pClause->GetReference(), ra::util::String::Printf(L"reference on clause %u differ", nIndex).c_str());
+ Assert::AreEqual(sOperation, pClause->GetOperation(), ra::util::String::Printf(L"Operation on clause %u differ", nIndex).c_str());
+ Assert::AreEqual(sTarget, pClause->GetTarget(), ra::util::String::Printf(L"Target on clause %u differ", nIndex).c_str());
+ Assert::AreEqual(sTally, pClause->GetTally(), ra::util::String::Printf(L"Tally on clause %u differ", nIndex).c_str());
+ }
+
+ void AssertHeader(gsl::index nIndex, const std::wstring& sHeader)
+ {
+ AssertClause(nIndex, L"", sHeader, L"", L"", L"");
+ }
+
+ private:
+ std::string m_sTriggerBuffer;
+ };
+
+public:
+ TEST_METHOD(TestSimpleNoNotes)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5_0xH2345!=0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"0x1234", L"is", L"5");
+ summary.AssertClause(1, L"2", L"0x2345", L"is not", L"0");
+ }
+
+ TEST_METHOD(TestSimpleDeltaOnly)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("d0xH1234=5_d0xH2345!=0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"0x1234", L"last frame was", L"5");
+ summary.AssertClause(1, L"2", L"0x2345", L"last frame was not", L"0");
+ }
+
+ TEST_METHOD(TestSimplePriorOnly)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("p0xH1234=5_p0xH2345!=0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"0x1234", L"was", L"5");
+ summary.AssertClause(1, L"2", L"0x2345", L"was not", L"0");
+ }
+
+ TEST_METHOD(TestSimpleHitTarget)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5.8._0xH2345>d0xH2345.4.");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"0x1234", L"is", L"5", L"for 8 frames");
+ summary.AssertClause(1, L"2", L"0x2345", L"increased", L"", L"4 times");
+ }
+
+ TEST_METHOD(TestSimpleOnce)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5.1._R:0xH2345=6.1.");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"0x1234", L"is", L"5", L""); // don't report once for "start" conditions
+ summary.AssertClause(1, L"2", L"0x2345", L"is", L"6", L"once"); // do report once for non-"start" conditions
+ }
+
+ TEST_METHOD(TestSimpleNote)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.mockGameContext.SetNote({ 0x1234U }, L"World");
+ summary.mockGameContext.SetNote({ 0x2345U }, L"[16-bit] Level");
+ summary.InitializeFrom("0xH1234=5_0x 2345!=0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"World", L"is", L"5");
+ summary.AssertClause(1, L"2", L"Level", L"is not", L"0");
+ }
+
+ TEST_METHOD(TestEnumNote)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.mockGameContext.SetNote({ 0x1234U }, L"Character (1=Bill, 2=Bob, 3=Betty, 4=Bonnie, 5=Ben)");
+ summary.mockGameContext.SetNote({ 0x2345U }, L"Difficulty\r\n0=Easy\r\n1=Normal\r\n2=Hard\r\n");
+ summary.InitializeFrom("0xH1234=5_0x 2345!=0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"Character", L"is", L"Ben");
+ summary.AssertClause(1, L"2", L"Difficulty", L"is not", L"Easy");
+ }
+
+ TEST_METHOD(TestEnumNoteGreaterThanZero)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.mockGameContext.SetNote({ 0x1234U }, L"Character (1=Bill, 2=Bob, 3=Betty, 4=Bonnie, 5=Ben)");
+ summary.mockGameContext.SetNote({ 0x2345U }, L"Difficulty\r\n0=Easy\r\n1=Normal\r\n2=Hard\r\n");
+ summary.InitializeFrom("0xH1234>2_0x 2345>0");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"Character", L"is greater than", L"Bob");
+ summary.AssertClause(1, L"2", L"Difficulty", L"is not", L"Easy"); // >0 converted to !=0
+ }
+
+ TEST_METHOD(TestEnumNoteLessThanOne)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.mockGameContext.SetNote({ 0x1234U }, L"Character (1=Bill, 2=Bob, 3=Betty, 4=Bonnie, 5=Ben)");
+ summary.mockGameContext.SetNote({ 0x2345U }, L"Difficulty\r\n0=Easy\r\n1=Normal\r\n2=Hard\r\n");
+ summary.InitializeFrom("0xH1234<1_0x 2345<1");
+
+ Assert::AreEqual({ 2U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1", L"Character", L"is", L"0");
+ summary.AssertClause(1, L"2", L"Difficulty", L"is", L"Easy"); // <1 converted to ==0
+ }
+
+ TEST_METHOD(TestMemoryReferenceDeltaSelf)
+ {
+ TriggerSummaryViewModelHarness summary;
+ summary.mockGameContext.SetNote({ 0x2345U }, L"Difficulty\r\n0=Easy\r\n1=Normal\r\n2=Hard\r\n");
+ summary.InitializeFrom("0xH1234!=d0xH1234_0x 2345>d0x 2345_0x 23455");
+
+ Assert::AreEqual({ 1U }, summary.Clauses().Count());
+ summary.AssertClause(0, L"1-2", L"World", L"decreased to", L"5");
+ }
+
+ TEST_METHOD(TestAddHeadersSimple)
+ {
+ ra::ui::EditorTheme pTheme;
+ ra::services::ServiceLocator::ServiceOverride pThemeOverride(&pTheme);
+
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5_0xH2345!=0");
+ summary.AddHeaders();
+
+ Assert::AreEqual({ 3U }, summary.Clauses().Count());
+ summary.AssertHeader(0, L"--- TRIGGER WHEN ---");
+ summary.AssertClause(1, L"1", L"0x1234", L"is", L"5");
+ summary.AssertClause(2, L"2", L"0x2345", L"is not", L"0");
+ }
+
+ TEST_METHOD(TestAddHeadersSimpleWithDelta)
+ {
+ ra::ui::EditorTheme pTheme;
+ ra::services::ServiceLocator::ServiceOverride pThemeOverride(&pTheme);
+
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5_0xH2345>d0xH2345");
+ summary.AddHeaders();
+
+ Assert::AreEqual({ 4U }, summary.Clauses().Count());
+ summary.AssertHeader(0, L"--- TRIGGER WHEN ---");
+ summary.AssertClause(1, L"2", L"0x2345", L"increased", L"");
+ summary.AssertHeader(2, L"--- WHILE ---");
+ summary.AssertClause(3, L"1", L"0x1234", L"is", L"5");
+ }
+
+ TEST_METHOD(TestAddHeadersHitTargetWithReset)
+ {
+ ra::ui::EditorTheme pTheme;
+ ra::services::ServiceLocator::ServiceOverride pThemeOverride(&pTheme);
+
+ TriggerSummaryViewModelHarness summary;
+ summary.InitializeFrom("0xH1234=5_0xH2345=6.1._R:0x3456=8.3.");
+ summary.AddHeaders();
+
+ Assert::AreEqual({ 6U }, summary.Clauses().Count());
+ summary.AssertHeader(0, L"--- TRIGGER WHEN ---");
+ summary.AssertClause(1, L"1", L"0x1234", L"is", L"5");
+ summary.AssertHeader(2, L"--- STARTING WHEN ---");
+ summary.AssertClause(3, L"2", L"0x2345", L"is", L"6");
+ summary.AssertHeader(4, L"--- FAILING WHEN ---");
+ summary.AssertClause(5, L"3", L"0x3456", L"is", L"8", L"for 3 frames");
+ }
+};
+
+} // namespace tests
+} // namespace viewmodels
+} // namespace ui
+} // namespace ra