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