Repository: RetroAchievements/RAIntegration
Pull request #1101 (2024-05-21 13:16:34)

Add Remember and Recall Support
Depends on https://github.com/RetroAchievements/rcheevos/pull/343

Adds support for Remember condition type and Recall Operand Type to Asset Editor.


Patch:
diff --git a/rcheevos b/rcheevos
index 47b0bcdd..cecf16b5 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit 47b0bcdd9b3df44a28d8ab0a34a76d2929bf3bab
+Subproject commit cecf16b5540822e87225710aed08558f81582781
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 6ca6b2e1..0c2d03c1 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -34,8 +34,10 @@ const IntModelProperty TriggerConditionViewModel::TotalHitsProperty("TriggerCond
 const BoolModelProperty TriggerConditionViewModel::IsSelectedProperty("TriggerConditionViewModel", "IsSelected", false);
 const BoolModelProperty TriggerConditionViewModel::IsIndirectProperty("TriggerConditionViewModel", "IsIndirect", false);
 const BoolModelProperty TriggerConditionViewModel::HasSourceSizeProperty("TriggerConditionViewModel", "HasSourceSize", true);
+const BoolModelProperty TriggerConditionViewModel::HasSourceValueProperty("TriggerConditionViewModel", "HasSourceValue", true);
 const BoolModelProperty TriggerConditionViewModel::HasTargetProperty("TriggerConditionViewModel", "HasTarget", true);
 const BoolModelProperty TriggerConditionViewModel::HasTargetSizeProperty("TriggerConditionViewModel", "HasTargetSize", false);
+const BoolModelProperty TriggerConditionViewModel::HasTargetValueProperty("TriggerConditionViewModel", "HasTargetValue", true);
 const BoolModelProperty TriggerConditionViewModel::HasHitsProperty("TriggerConditionViewModel", "HasHits", true);
 const BoolModelProperty TriggerConditionViewModel::CanEditHitsProperty("TriggerConditionViewModel", "CanEditHits", true);
 const IntModelProperty TriggerConditionViewModel::RowColorProperty("TriggerConditionViewModel", "RowColor", 0);
@@ -63,6 +65,7 @@ void TriggerConditionViewModel::SerializeAppend(std::string& sBuffer) const
             case TriggerConditionType::SubSource:   sBuffer.push_back('B'); break;
             case TriggerConditionType::AddHits:     sBuffer.push_back('C'); break;
             case TriggerConditionType::SubHits:     sBuffer.push_back('D'); break;
+            case TriggerConditionType::Remember:    sBuffer.push_back('K'); break;
             case TriggerConditionType::AndNext:     sBuffer.push_back('N'); break;
             case TriggerConditionType::OrNext:      sBuffer.push_back('O'); break;
             case TriggerConditionType::MeasuredIf:  sBuffer.push_back('Q'); break;
@@ -227,6 +230,12 @@ void TriggerConditionViewModel::SerializeAppendOperand(std::string& sBuffer, Tri
             sBuffer.push_back('~');
             break;
 
+        case TriggerOperandType::Recall:
+        {
+            sBuffer.append("{recall}");
+            return;
+        }
+
         default:
             assert(!"Unknown operand type");
             break;
@@ -422,6 +431,12 @@ void TriggerConditionViewModel::SetOperand(const IntModelProperty& pTypeProperty
             break;
         }
 
+        case TriggerOperandType::Recall:
+            SetValue(pSizeProperty, ra::etoi(MemSize::ThirtyTwoBit));
+            pValue.type = RC_VALUE_TYPE_UNSIGNED;
+            pValue.value.u32 = 1;
+            break;
+
         default:
             Expects(!"Unknown operand type");
             break;
@@ -467,13 +482,15 @@ void TriggerConditionViewModel::OnValueChanged(const IntModelProperty::ChangeArg
         if (!IsAddressType(nNewType))
         {
             SetValue(HasSourceSizeProperty, false);
-            SetSourceSize(nNewType == TriggerOperandType::Value ? MemSize::ThirtyTwoBit : MemSize::Float);
+            SetSourceSize(nNewType == TriggerOperandType::Value || IsParameterlessType(nNewType) ? MemSize::ThirtyTwoBit : MemSize::Float);
         }
         else if (!IsAddressType(nOldType))
         {
             SetSourceSize(GetTargetSize());
             SetValue(HasSourceSizeProperty, true);
         }
+
+        SetValue(HasSourceValueProperty, IsParameterlessType(nNewType) ? false : true);
     }
     else if (args.Property == TargetTypeProperty)
     {
@@ -484,13 +501,15 @@ void TriggerConditionViewModel::OnValueChanged(const IntModelProperty::ChangeArg
         if (!IsAddressType(nNewType))
         {
             SetValue(HasTargetSizeProperty, false);
-            SetTargetSize(nNewType == TriggerOperandType::Value ? MemSize::ThirtyTwoBit : MemSize::Float);
+            SetTargetSize(nNewType == TriggerOperandType::Value || IsParameterlessType(nNewType) ? MemSize::ThirtyTwoBit : MemSize::Float);
         }
         else if (!IsAddressType(ra::itoe<TriggerOperandType>(args.tOldValue)))
         {
             SetTargetSize(GetSourceSize());
             SetValue(HasTargetSizeProperty, GetValue(HasTargetProperty));
         }
+
+        SetValue(HasTargetValueProperty, IsParameterlessType(nNewType) || !GetValue(HasTargetProperty) ? false : true);
     }
     else if (args.Property == OperatorProperty)
     {
@@ -609,6 +628,9 @@ std::wstring TriggerConditionViewModel::GetTooltip(const StringModelProperty& nP
         if (nType == TriggerOperandType::Float)
             return L"";
 
+        if (nType == TriggerOperandType::Recall)
+            return L"";
+
         if (IsIndirect())
         {
             ra::ByteAddress nPointerAddress = 0;
@@ -629,6 +651,9 @@ std::wstring TriggerConditionViewModel::GetTooltip(const StringModelProperty& nP
         if (nType == TriggerOperandType::Float)
             return L"";
 
+        if (nType == TriggerOperandType::Recall)
+            return L"";
+
         if (IsIndirect())
         {
             ra::ByteAddress nPointerAddress = 0;
@@ -847,6 +872,7 @@ bool TriggerConditionViewModel::IsModifying(TriggerConditionType nType) noexcept
         case TriggerConditionType::AddAddress:
         case TriggerConditionType::AddSource:
         case TriggerConditionType::SubSource:
+        case TriggerConditionType::Remember:
             return true;
 
         default:
@@ -860,6 +886,7 @@ bool TriggerConditionViewModel::IsAddressType(TriggerOperandType nType) noexcept
     {
         case TriggerOperandType::Value:
         case TriggerOperandType::Float:
+        case TriggerOperandType::Recall:
             return false;
 
         default:
@@ -867,6 +894,18 @@ bool TriggerConditionViewModel::IsAddressType(TriggerOperandType nType) noexcept
     }
 }
 
+/* Has no address or value */
+bool TriggerConditionViewModel::IsParameterlessType(TriggerOperandType nType) noexcept
+{
+    switch (nType)
+    {
+        case TriggerOperandType::Recall:
+            return true;
+        default:
+            return false;
+    }
+}
+
 bool TriggerConditionViewModel::IsComparisonVisible(const ViewModelBase& vmItem, int nValue)
 {
     const auto* vmCondition = dynamic_cast<const TriggerConditionViewModel*>(&vmItem);
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index dac7af23..415d9d3e 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -24,6 +24,7 @@ enum class TriggerConditionType : uint8_t
     SubSource = RC_CONDITION_SUB_SOURCE,
     AddHits = RC_CONDITION_ADD_HITS,
     SubHits = RC_CONDITION_SUB_HITS,
+    Remember = RC_CONDITION_REMEMBER,
     AndNext = RC_CONDITION_AND_NEXT,
     Measured = RC_CONDITION_MEASURED,
     AddAddress = RC_CONDITION_ADD_ADDRESS,
@@ -41,7 +42,8 @@ enum class TriggerOperandType : uint8_t
     Prior = RC_OPERAND_PRIOR,           // the last differing value at this address.
     BCD = RC_OPERAND_BCD,               // Address, but decoded from binary-coded-decimal
     Float = RC_OPERAND_FP,              // a 32-bit floating point value
-    Inverted = RC_OPERAND_INVERTED      // the bitwise compliment of the current value at the address
+    Inverted = RC_OPERAND_INVERTED,     // the bitwise compliment of the current value at the address
+    Recall = RC_OPERAND_RECALL          // the last value stored by RC_CONDITION_REMEMBER 
 };
 
 enum class TriggerOperatorType : uint8_t
@@ -82,6 +84,7 @@ public:
     MemSize GetSourceSize() const { return ra::itoe<MemSize>(GetValue(SourceSizeProperty)); }
     void SetSourceSize(MemSize nValue) { SetValue(SourceSizeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasSourceValueProperty;
     static const StringModelProperty SourceValueProperty;
     const std::wstring& GetSourceValue() const { return GetValue(SourceValueProperty); }
     void SetSourceValue(const std::wstring& sValue) { SetValue(SourceValueProperty, sValue); }
@@ -101,6 +104,7 @@ public:
     MemSize GetTargetSize() const { return ra::itoe<MemSize>(GetValue(TargetSizeProperty)); }
     void SetTargetSize(MemSize nValue) { SetValue(TargetSizeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasTargetValueProperty;
     static const StringModelProperty TargetValueProperty;
     const std::wstring& GetTargetValue() const { return GetValue(TargetValueProperty); }
     void SetTargetValue(const std::wstring& sValue) { SetValue(TargetValueProperty, sValue); }
@@ -165,6 +169,7 @@ private:
 
     static bool IsModifying(TriggerConditionType nType) noexcept;
     static bool IsAddressType(TriggerOperandType nType) noexcept;
+    static bool IsParameterlessType(TriggerOperandType nType) noexcept;
     void UpdateHasHits();
     bool IsForValue() const noexcept;
 
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index ae9eab7c..2619b833 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -41,6 +41,7 @@ TriggerViewModel::TriggerViewModel() noexcept
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Measured), L"Measured");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::MeasuredIf), L"Measured If");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Trigger), L"Trigger");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Remember), L"Remember");
 
     m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Address), L"Mem");
     m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Value), L"Value");
@@ -49,6 +50,7 @@ TriggerViewModel::TriggerViewModel() noexcept
     m_vOperandTypes.Add(ra::etoi(TriggerOperandType::BCD), L"BCD");
     m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Float), L"Float");
     m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Inverted), L"Invert");
+    m_vOperandTypes.Add(ra::etoi(TriggerOperandType::Recall), L"Recall");
 
     m_vOperandSizes.Add(ra::etoi(MemSize::Bit_0), L"Bit0");
     m_vOperandSizes.Add(ra::etoi(MemSize::Bit_1), L"Bit1");
@@ -945,6 +947,7 @@ void TriggerViewModel::UpdateTotalHits()
             case ra::ui::viewmodels::TriggerConditionType::AddAddress:
             case ra::ui::viewmodels::TriggerConditionType::AddSource:
             case ra::ui::viewmodels::TriggerConditionType::SubSource:
+            case ra::ui::viewmodels::TriggerConditionType::Remember:
             case ra::ui::viewmodels::TriggerConditionType::AndNext:
             case ra::ui::viewmodels::TriggerConditionType::OrNext:
             case ra::ui::viewmodels::TriggerConditionType::ResetNextIf:
@@ -1101,6 +1104,7 @@ bool TriggerViewModel::BuildHitChainTooltip(std::wstring& sTooltip,
             case ra::ui::viewmodels::TriggerConditionType::AddAddress:
             case ra::ui::viewmodels::TriggerConditionType::AddSource:
             case ra::ui::viewmodels::TriggerConditionType::SubSource:
+            case ra::ui::viewmodels::TriggerConditionType::Remember:
             case ra::ui::viewmodels::TriggerConditionType::AndNext:
             case ra::ui::viewmodels::TriggerConditionType::OrNext:
             case ra::ui::viewmodels::TriggerConditionType::ResetNextIf:
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 8aee808c..5b2a55e2 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -138,6 +138,14 @@ public:
 
     void SetMaximum(unsigned int nValue) noexcept { m_nMaximum = nValue; }
 
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return GridTextColumnBinding::GetText(vmItems, nIndex);
+    }
+
     bool SetText(ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex, const std::wstring& sValue) override
     {
         const auto nOperandType = ra::itoe<ra::ui::viewmodels::TriggerOperandType>(vmItems.GetItemValue(nIndex, *m_pTypeProperty));
@@ -159,6 +167,12 @@ public:
                 return true;
             }
 
+            case ra::ui::viewmodels::TriggerOperandType::Recall:
+            {
+                vmItems.SetItemValue(nIndex, *m_pBoundProperty, L"");
+                return true;
+            }
+
             case ra::ui::viewmodels::TriggerOperandType::Value:
             {
                 const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
@@ -209,6 +223,9 @@ public:
 
     std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
         const auto* vmConditions = dynamic_cast<const ViewModelCollection<TriggerConditionViewModel>*>(&vmItems);
         Expects(vmConditions != nullptr);
         const auto* vmCondition = vmConditions->GetItemAt(nIndex);
@@ -220,6 +237,9 @@ public:
 
     bool HandleRightClick(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) override
     {
+        if (IsHidden(vmItems, nIndex))
+            return false;
+
         if (IsAddressType(vmItems, nIndex))
         {
             unsigned int nAddress;
@@ -252,57 +272,84 @@ protected:
     {
         const auto nOperandType = ra::itoe<ra::ui::viewmodels::TriggerOperandType>(vmItems.GetItemValue(nIndex, *m_pTypeProperty));
         return (nOperandType != ra::ui::viewmodels::TriggerOperandType::Value &&
-            nOperandType != ra::ui::viewmodels::TriggerOperandType::Float);
+            nOperandType != ra::ui::viewmodels::TriggerOperandType::Float &&
+            nOperandType !=ra::ui::viewmodels::TriggerOperandType::Recall);
     }
 
+protected:
+    virtual bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const = 0;
+
     const IntModelProperty* m_pTypeProperty = nullptr;
     unsigned int m_nMaximum = 0xFFFFFFFF;
 };
 
-class TargetValueColumnBinding : public ValueColumnBinding
+class SourceValueColumnBinding : public ValueColumnBinding
 {
 public:
-    TargetValueColumnBinding(const StringModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
+    SourceValueColumnBinding(const StringModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
         : ValueColumnBinding(pBoundProperty, pTypeProperty)
     {
     }
 
-    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
     {
-        if (IsHidden(vmItems, nIndex))
-            return L"";
+        if (pProperty == TriggerConditionViewModel::HasSourceValueProperty)
+            return true;
 
-        return ValueColumnBinding::GetText(vmItems, nIndex);
+        return GridColumnBinding::DependsOn(pProperty);
     }
 
-    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
+    HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
     {
-        if (pProperty == TriggerConditionViewModel::HasTargetProperty)
-            return true;
+        auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
+        Expects(pGridBinding != nullptr);
+        if (!pGridBinding->GetItems().GetItemValue(pInfo.nItemIndex, TriggerConditionViewModel::HasSourceValueProperty))
+            return nullptr;
 
-        return GridColumnBinding::DependsOn(pProperty);
+        return ValueColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
 
-    std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+protected:
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        if (IsHidden(vmItems, nIndex))
-            return L"";
+        return !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasSourceValueProperty);
+    }
+};
 
-        return ValueColumnBinding::GetTooltip(vmItems, nIndex);
+class TargetValueColumnBinding : public ValueColumnBinding
+{
+public:
+    TargetValueColumnBinding(const StringModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
+        : ValueColumnBinding(pBoundProperty, pTypeProperty)
+    {
     }
 
-    bool HandleRightClick(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) override
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
     {
-        if (IsHidden(vmItems, nIndex))
-            return false;
+        if (pProperty == TriggerConditionViewModel::HasTargetProperty)
+            return true;
+        if (pProperty == TriggerConditionViewModel::HasTargetValueProperty)
+            return true;
+
+        return GridColumnBinding::DependsOn(pProperty);
+    }
 
-        return ValueColumnBinding::HandleRightClick(vmItems, nIndex);
+    HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
+    {
+        auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
+        Expects(pGridBinding != nullptr);
+        if (!pGridBinding->GetItems().GetItemValue(pInfo.nItemIndex, TriggerConditionViewModel::HasTargetProperty))
+            return nullptr;
+        if (!pGridBinding->GetItems().GetItemValue(pInfo.nItemIndex, TriggerConditionViewModel::HasTargetValueProperty))
+            return nullptr;
+
+        return ValueColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
 
 protected:
-    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        return !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasTargetProperty);
+        return !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasTargetProperty) || !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasTargetValueProperty);
     }
 };
 
@@ -844,7 +891,7 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
     pSourceSizeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(3, std::move(pSourceSizeColumn));
 
-    auto pSourceValueColumn = std::make_unique<ValueColumnBinding>(
+    auto pSourceValueColumn = std::make_unique<SourceValueColumnBinding>(
         TriggerConditionViewModel::SourceValueProperty, TriggerConditionViewModel::SourceTypeProperty);
     pSourceValueColumn->SetHeader(L"Memory");
     pSourceValueColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_VALUE);


Test patch:
diff --git a/tests/ui/viewmodels/TriggerConditionAsserts.hh b/tests/ui/viewmodels/TriggerConditionAsserts.hh
index 9d7f942a..a1517d89 100644
--- a/tests/ui/viewmodels/TriggerConditionAsserts.hh
+++ b/tests/ui/viewmodels/TriggerConditionAsserts.hh
@@ -33,6 +33,8 @@ std::wstring ToString<ra::ui::viewmodels::TriggerConditionType>(
             return L"AddHits";
         case ra::ui::viewmodels::TriggerConditionType::SubHits:
             return L"SubHits";
+        case ra::ui::viewmodels::TriggerConditionType::Remember:
+            return L"Remember";
         case ra::ui::viewmodels::TriggerConditionType::AndNext:
             return L"AndNext";
         case ra::ui::viewmodels::TriggerConditionType::Measured:
@@ -72,6 +74,8 @@ std::wstring ToString<ra::ui::viewmodels::TriggerOperandType>(
             return L"Float";
         case ra::ui::viewmodels::TriggerOperandType::Inverted:
             return L"Inverted";
+        case ra::ui::viewmodels::TriggerOperandType::Recall:
+            return L"Recall";
         default:
             return std::to_wstring(static_cast<int>(nOperandType));
     }
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index 70175b64..cee4568c 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -42,7 +42,9 @@ private:
         }
 
         bool HasSourceSize() const { return GetValue(HasSourceSizeProperty); }
+        bool HasSourceValue() const { return GetValue(HasSourceValueProperty); }
         bool HasTargetSize() const { return GetValue(HasTargetSizeProperty); }
+        bool HasTargetValue() const { return GetValue(HasTargetValueProperty); }
         bool HasTarget() const { return GetValue(HasTargetProperty); }
         bool HasHits() const { return GetValue(HasHitsProperty); }
         bool CanEditHits() const { return GetValue(CanEditHitsProperty); }
@@ -137,6 +139,11 @@ public:
         Assert::IsTrue(vmCondition.HasSourceSize());
         Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize());
 
+        vmCondition.SetSourceType(TriggerOperandType::Recall);
+        Assert::AreEqual(TriggerOperandType::Recall, vmCondition.GetSourceType());
+        Assert::IsFalse(vmCondition.HasSourceSize());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetSourceSize());
+
         vmCondition.SetSourceType(TriggerOperandType::Value);
         Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetSourceType());
         Assert::IsFalse(vmCondition.HasSourceSize());
@@ -148,6 +155,51 @@ public:
         Assert::AreEqual(MemSize::SixteenBit, vmCondition.GetSourceSize()); // copied from target size
     }
 
+    TEST_METHOD(TestHasSourceValue)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        vmCondition.SetTargetSize(MemSize::SixteenBit);
+
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize()); // initial size is 8-bit
+
+        vmCondition.SetSourceType(TriggerOperandType::BCD);
+        Assert::AreEqual(TriggerOperandType::BCD, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Delta);
+        Assert::AreEqual(TriggerOperandType::Delta, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Prior);
+        Assert::AreEqual(TriggerOperandType::Prior, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Inverted);
+        Assert::AreEqual(TriggerOperandType::Inverted, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Recall);
+        Assert::AreEqual(TriggerOperandType::Recall, vmCondition.GetSourceType());
+        Assert::IsFalse(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Value);
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetSourceSize()); // value always 32-bit
+
+        vmCondition.SetSourceType(TriggerOperandType::Address);
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceValue());
+        Assert::AreEqual(MemSize::SixteenBit, vmCondition.GetSourceSize()); // copied from target size
+    }
+
     TEST_METHOD(TestHasTarget)
     {
         TriggerConditionViewModelHarness vmCondition;
@@ -241,6 +293,11 @@ public:
         Assert::IsTrue(vmCondition.HasTarget());
         Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value always 32-bit
 
+        vmCondition.SetTargetType(TriggerOperandType::Recall);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value always 32-bit
+
         vmCondition.SetOperator(TriggerOperatorType::None);
         Assert::IsFalse(vmCondition.HasTargetSize());
         Assert::IsFalse(vmCondition.HasTarget());
@@ -267,6 +324,47 @@ public:
         Assert::AreEqual(MemSize::EightBit, vmCondition.GetTargetSize());
     }
 
+    TEST_METHOD(TestHasTargetValue)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::AreEqual(TriggerOperatorType::Equals, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value always 32-bit
+
+        vmCondition.SetTargetType(TriggerOperandType::Address);
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetTargetSize()); // size copied from source
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value always 32-bit
+
+        vmCondition.SetTargetType(TriggerOperandType::Delta);
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetTargetSize()); // size copied from source
+
+        vmCondition.SetTargetType(TriggerOperandType::Inverted);
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::BCD);
+        Assert::IsTrue(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::EightBit, vmCondition.GetTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Recall);
+        Assert::IsFalse(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value always 32-bit
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::IsFalse(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize()); // value still 32-bit
+
+        vmCondition.SetOperator(TriggerOperatorType::NotEquals);
+        Assert::IsFalse(vmCondition.HasTargetValue());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, vmCondition.GetTargetSize());
+    }
+
     TEST_METHOD(TestHasTargetSizeHasTarget)
     {
         TriggerConditionViewModelHarness vmCondition;
@@ -293,6 +391,10 @@ public:
         Assert::AreEqual(TriggerOperandType::Inverted, vmCondition.GetTargetType());
         Assert::IsTrue(vmCondition.HasTargetSize());
 
+        vmCondition.SetTargetType(TriggerOperandType::Recall);
+        Assert::AreEqual(TriggerOperandType::Recall, vmCondition.GetTargetType());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+
         vmCondition.SetTargetType(TriggerOperandType::Value);
         Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
         Assert::IsFalse(vmCondition.HasTargetSize());
@@ -364,6 +466,11 @@ public:
         Assert::AreEqual(TriggerConditionType::Trigger, vmCondition.GetType());
         Assert::IsTrue(vmCondition.HasHits());
         Assert::IsTrue(vmCondition.CanEditHits());
+
+        vmCondition.SetType(TriggerConditionType::Remember);
+        Assert::AreEqual(TriggerConditionType::Remember, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+        Assert::IsFalse(vmCondition.CanEditHits());
     }
 
     TEST_METHOD(TestHasHitsForValue)
@@ -450,6 +557,11 @@ public:
         vmCondition.SetType(TriggerConditionType::Standard);
         sSerialized = vmCondition.Serialize();
         Assert::AreEqual(std::string("0xH1234=0xH2345.10."), sSerialized);
+
+        // changing to a type that does not support hits should remove the hit count (and comparison)
+        vmCondition.SetType(TriggerConditionType::Remember);
+        sSerialized = vmCondition.Serialize();
+        Assert::AreEqual(std::string("K:0xH1234"), sSerialized);
     }
 
     TEST_METHOD(TestSizes)
@@ -484,6 +596,7 @@ public:
         ParseAndRegenerate("0xH1234=p0xH1234"); // prior
         ParseAndRegenerate("0xH1234=b0xH1234"); // bcd
         ParseAndRegenerate("0xH1234=~0xH1234"); // inverted
+        ParseAndRegenerate("{recall}={recall}"); // recall
         ParseAndRegenerate("0xH1234=1234"); // value
         ParseAndRegenerate("0xH1234=f12.34"); // float
     }
@@ -516,6 +629,7 @@ public:
         ParseAndRegenerate("C:0xH1234=5"); // add hits
         ParseAndRegenerate("D:0xH1234=5"); // sub hits
         ParseAndRegenerate("N:0xH1234=5"); // and next
+        ParseAndRegenerate("K:0xH1234"); // remember
         ParseAndRegenerate("O:0xH1234=5"); // or next
         ParseAndRegenerate("M:0xH1234=5"); // measured
         ParseAndRegenerate("Q:0xH1234=5"); // measured if
@@ -994,6 +1108,9 @@ public:
 
         condition.SetType(TriggerConditionType::Trigger);
         Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Remember);
+        Assert::IsTrue(condition.IsModifying());
     }
 
     TEST_METHOD(TestSwitchBetweenModifyingAndNonModifying)
@@ -1032,6 +1149,22 @@ public:
         Assert::AreEqual(TriggerOperatorType::Equals, condition.GetOperator());
         Assert::AreEqual(std::string("0xH1234=8"), condition.Serialize());
         Assert::IsFalse(condition.IsModifying());
+
+        // change to modifying switches operator to None. the 8 is unmodified, but ignored
+        condition.SetType(TriggerConditionType::Remember);
+        Assert::AreEqual(TriggerOperatorType::None, condition.GetOperator());
+        Assert::AreEqual(std::string("K:0xH1234"), condition.Serialize());
+        Assert::IsTrue(condition.IsModifying());
+
+        // changing operator "restores" the 8
+        condition.SetOperator(TriggerOperatorType::Multiply);
+        Assert::AreEqual(std::string("K:0xH1234*8"), condition.Serialize());
+
+        // change to non-modifying switches operator to Equals
+        condition.SetType(TriggerConditionType::ResetIf);
+        Assert::AreEqual(TriggerOperatorType::Equals, condition.GetOperator());
+        Assert::AreEqual(std::string("R:0xH1234=8"), condition.Serialize());
+        Assert::IsFalse(condition.IsModifying());
     }
 
     TEST_METHOD(TestSwitchBetweenValueAndAddress)
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 0f112a88..d15e05c4 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -118,7 +118,7 @@ public:
         Assert::AreEqual({ 0U }, vmTrigger.Groups().Count());
         Assert::AreEqual(std::string(), vmTrigger.Serialize());
 
-        Assert::AreEqual({ 14U }, vmTrigger.ConditionTypes().Count());
+        Assert::AreEqual({ 15U }, vmTrigger.ConditionTypes().Count());
         Assert::AreEqual((int)TriggerConditionType::Standard, vmTrigger.ConditionTypes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L""), vmTrigger.ConditionTypes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)TriggerConditionType::PauseIf, vmTrigger.ConditionTypes().GetItemAt(1)->GetId());
@@ -147,8 +147,10 @@ public:
         Assert::AreEqual(std::wstring(L"Measured If"), vmTrigger.ConditionTypes().GetItemAt(12)->GetLabel());
         Assert::AreEqual((int)TriggerConditionType::Trigger, vmTrigger.ConditionTypes().GetItemAt(13)->GetId());
         Assert::AreEqual(std::wstring(L"Trigger"), vmTrigger.ConditionTypes().GetItemAt(13)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::Remember, vmTrigger.ConditionTypes().GetItemAt(14)->GetId());
+        Assert::AreEqual(std::wstring(L"Remember"), vmTrigger.ConditionTypes().GetItemAt(14)->GetLabel());
 
-        Assert::AreEqual({ 7U }, vmTrigger.OperandTypes().Count());
+        Assert::AreEqual({ 8U }, vmTrigger.OperandTypes().Count());
         Assert::AreEqual((int)TriggerOperandType::Address, vmTrigger.OperandTypes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L"Mem"), vmTrigger.OperandTypes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)TriggerOperandType::Value, vmTrigger.OperandTypes().GetItemAt(1)->GetId());
@@ -163,6 +165,8 @@ public:
         Assert::AreEqual(std::wstring(L"Float"), vmTrigger.OperandTypes().GetItemAt(5)->GetLabel());
         Assert::AreEqual((int)TriggerOperandType::Inverted, vmTrigger.OperandTypes().GetItemAt(6)->GetId());
         Assert::AreEqual(std::wstring(L"Invert"), vmTrigger.OperandTypes().GetItemAt(6)->GetLabel());
+        Assert::AreEqual((int)TriggerOperandType::Recall, vmTrigger.OperandTypes().GetItemAt(7)->GetId());
+        Assert::AreEqual(std::wstring(L"Recall"), vmTrigger.OperandTypes().GetItemAt(7)->GetLabel());
 
         Assert::AreEqual({ 24U }, vmTrigger.OperandSizes().Count());
         Assert::AreEqual((int)MemSize::Bit_0, vmTrigger.OperandSizes().GetItemAt(0)->GetId());