Repository: RetroAchievements/RAIntegration
Pull request #660 (2021-01-04 02:27:14)

implement Revert for assets


Patch:
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index b7b1a26f..80a56006 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -246,8 +246,7 @@ BEGIN
 END
 
 IDD_RA_MEMBOOKMARK DIALOGEX 0, 0, 341, 154
-STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
-EXSTYLE WS_EX_NOACTIVATE
+STYLE DS_SETFONT | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
 CAPTION "Memory Bookmarker"
 FONT 8, "MS Shell Dlg", 400, 0, 0x0
 BEGIN
@@ -262,8 +261,7 @@ BEGIN
 END
 
 IDD_RA_CODENOTES DIALOGEX 0, 0, 287, 230
-STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
-EXSTYLE WS_EX_NOACTIVATE
+STYLE DS_SETFONT | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
 CAPTION "Code Notes"
 FONT 8, "MS Shell Dlg", 400, 0, 0x0
 BEGIN
diff --git a/src/data/context/GameAssets.cpp b/src/data/context/GameAssets.cpp
index 6e0e1ace..088a296a 100644
--- a/src/data/context/GameAssets.cpp
+++ b/src/data/context/GameAssets.cpp
@@ -244,6 +244,7 @@ void GameAssets::SaveAssets(const std::vector<ra::data::models::AssetModelBase*>
 
     pData->WriteLine(pGameContext.GameTitle());
 
+    bool bHasDeleted = false;
     for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(Count()); ++nIndex)
     {
         auto* pItem = GetItemAt(nIndex);
@@ -261,6 +262,11 @@ void GameAssets::SaveAssets(const std::vector<ra::data::models::AssetModelBase*>
                 // always write unpublished changes
                 break;
 
+            case ra::data::models::AssetChanges::Deleted:
+                // never write deleted objects - we'll remove them after we update the file
+                bHasDeleted = true;
+                continue;
+
             default:
                 // if the item is modified, check to see if it's selected
                 bool bUpdate = vAssetsToSave.empty();
@@ -308,6 +314,16 @@ void GameAssets::SaveAssets(const std::vector<ra::data::models::AssetModelBase*>
     }
 
     RA_LOG_INFO("Wrote user assets file");
+
+    if (bHasDeleted)
+    {
+        for (gsl::index nIndex = gsl::narrow_cast<gsl::index>(Count()) - 1; nIndex >= 0; --nIndex)
+        {
+            auto* pItem = GetItemAt(nIndex);
+            if (pItem != nullptr && pItem->GetChanges() == ra::data::models::AssetChanges::Deleted)
+                RemoveAt(nIndex);
+        }
+    }
 }
 
 } // namespace context
diff --git a/src/data/models/AssetModelBase.cpp b/src/data/models/AssetModelBase.cpp
index bd30beac..f78fee56 100644
--- a/src/data/models/AssetModelBase.cpp
+++ b/src/data/models/AssetModelBase.cpp
@@ -237,6 +237,11 @@ void AssetModelBase::SetNew()
     SetValue(ChangesProperty, ra::etoi(AssetChanges::New));
 }
 
+void AssetModelBase::SetDeleted()
+{
+    SetValue(ChangesProperty, ra::etoi(AssetChanges::Deleted));
+}
+
 bool AssetModelBase::HasUnpublishedChanges() const noexcept
 {
     return (m_pTransaction && m_pTransaction->m_pNext && m_pTransaction->m_pNext->IsModified());
diff --git a/src/data/models/AssetModelBase.hh b/src/data/models/AssetModelBase.hh
index 5d5f046f..bbb6f572 100644
--- a/src/data/models/AssetModelBase.hh
+++ b/src/data/models/AssetModelBase.hh
@@ -43,6 +43,7 @@ enum class AssetChanges
     Modified,    // differs from local data
     Unpublished, // local data differs from server data
     New,         // asset not in local or server data
+    Deleted,     // asset to be removed from local data
 };
 
 class AssetModelBase : public DataModelBase
@@ -186,6 +187,11 @@ public:
     /// </summary>
     void SetNew();
 
+    /// <summary>
+    /// Marks the asset as to be removed from local.
+    /// </summary>
+    void SetDeleted();
+
     /// <summary>
     /// Determines if the local checkpoint differs from the server checkpoint. Ignores any changes
     /// made after the local checkpoint - use GetChanges() for overall modification state.
diff --git a/src/ui/ViewModelCollection.hh b/src/ui/ViewModelCollection.hh
index f3326930..e4282e76 100644
--- a/src/ui/ViewModelCollection.hh
+++ b/src/ui/ViewModelCollection.hh
@@ -74,6 +74,16 @@ public:
         }
     }
 
+    /// <summary>
+    /// Gets the <see cref="ViewModelBase" /> at the specified index.
+    /// </summary>
+    ViewModelBase* GetViewModelAt(gsl::index nIndex) { return dynamic_cast<ViewModelBase*>(GetModelAt(nIndex)); }
+
+    /// <summary>
+    /// Gets the <see cref="ViewModelBase" /> at the specified index.
+    /// </summary>
+    const ViewModelBase* GetViewModelAt(gsl::index nIndex) const { return dynamic_cast<const ViewModelBase*>(GetModelAt(nIndex)); }
+
 protected:
     void OnModelValueChanged(gsl::index nIndex, const BoolModelProperty::ChangeArgs& args) override;
     void OnModelValueChanged(gsl::index nIndex, const StringModelProperty::ChangeArgs& args) override;
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 0aef9112..e760afde 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -4,6 +4,7 @@
 #include "data\context\UserContext.hh"
 
 #include "services\AchievementRuntime.hh"
+#include "services\IConfiguration.hh"
 #include "services\ILocalStorage.hh"
 #include "services\IThreadPool.hh"
 #include "services\ServiceLocator.hh"
@@ -13,6 +14,7 @@
 #include "ui\viewmodels\WindowManager.hh"
 
 #include "Exports.hh"
+#include "RA_BuildVer.h"
 #include "RA_Log.h"
 
 namespace ra {
@@ -54,6 +56,7 @@ AssetListViewModel::AssetListViewModel() noexcept
     m_vChanges.Add(ra::etoi(ra::data::models::AssetChanges::Modified), L"Modified");
     m_vChanges.Add(ra::etoi(ra::data::models::AssetChanges::Unpublished), L"Unpublished");
     m_vChanges.Add(ra::etoi(ra::data::models::AssetChanges::New), L"New");
+    m_vChanges.Add(ra::etoi(ra::data::models::AssetChanges::Deleted), L"Deleted");
 
     // have to use separate monitor for capturing selection changes in filtered list
     // so EndUpdate for filtered list doesn't trigger another call to ApplyFilter.
@@ -724,6 +727,9 @@ void AssetListViewModel::SaveSelected()
         if (vSelectedAssets.empty())
             return;
 
+        if (!ValidateAssetsForCore(vSelectedAssets, true))
+            return;
+
         ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
         vmMessageBox.SetMessage(ra::StringPrintf(L"Are you sure you want to publish %d items?", vSelectedAssets.size()));
         vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
@@ -745,6 +751,9 @@ void AssetListViewModel::SaveSelected()
         if (vSelectedAssets.empty())
             return;
 
+        if (!ValidateAssetsForCore(vSelectedAssets, false))
+            return;
+
         ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
         vmMessageBox.SetHeader(ra::StringPrintf(L"Are you sure you want to promote %d items to core?", vSelectedAssets.size()));
         vmMessageBox.SetMessage(L"Items in core are officially available for players to earn.");
@@ -760,7 +769,7 @@ void AssetListViewModel::SaveSelected()
     }
     else if (sSaveButtonText.at(0) == 'D') // "De&mote"
     {
-        // promote - find the selected unpublished items
+        // demote - find the selected published items
         GetSelectedAssets(vSelectedAssets, [](const ra::data::models::AssetModelBase& pModel)
         {
             return (pModel.GetChanges() == ra::data::models::AssetChanges::None &&
@@ -790,6 +799,87 @@ void AssetListViewModel::SaveSelected()
     UpdateButtons();
 }
 
+#if RA_INTEGRATION_VERSION_MINOR < 79
+static std::wstring ValidateCondSet(const rc_condset_t* pCondSet)
+{
+    for (const auto* pCondition = pCondSet->conditions; pCondition != nullptr; pCondition = pCondition->next)
+    {
+        if (pCondition->type == RC_CONDITION_RESET_NEXT_IF)
+            return L"ResetNextIf is pre-release functionality";
+        if (pCondition->type == RC_CONDITION_TRIGGER)
+            return L"Trigger is pre-release functionality";
+        if (pCondition->oper == RC_OPERATOR_MULT)
+            return L"Multiplication is pre-release functionality";
+        if (pCondition->oper == RC_OPERATOR_DIV)
+            return L"Division is pre-release functionality";
+        if (pCondition->oper == RC_OPERATOR_AND)
+            return L"Bitwise And is pre-release functionality";
+    }
+
+    return L"";
+}
+
+static std::wstring ValidateTriggerLogic(const std::string& sTrigger)
+{
+    const auto nSize = rc_trigger_size(sTrigger.c_str());
+    if (nSize < 0)
+        return ra::StringPrintf(L"Parse Error %d: %s", nSize, rc_error_str(nSize));
+
+    std::string sBuffer;
+    sBuffer.resize(nSize);
+    const auto* pTrigger = rc_parse_trigger(sBuffer.data(), sTrigger.c_str(), nullptr, 0);
+
+    std::wstring sError = ValidateCondSet(pTrigger->requirement);
+    if (sError.empty())
+    {
+        const auto* pAlt = pTrigger->alternative;
+        while (pAlt != nullptr)
+        {
+            sError = ValidateCondSet(pAlt);
+            if (!sError.empty())
+                break;
+
+            pAlt = pAlt->next;
+        }
+    }
+
+    return sError;
+}
+#endif
+
+void AssetListViewModel::ValidateAchievementForCore(_UNUSED std::wstring& sError, _UNUSED const ra::data::models::AchievementModel& pAchievement) const
+{
+#if RA_INTEGRATION_VERSION_MINOR < 79
+    if (!ra::services::ServiceLocator::Get<ra::services::IConfiguration>().IsCustomHost())
+    {
+        std::wstring sTriggerError = ValidateTriggerLogic(pAchievement.GetTrigger());
+        if (!sTriggerError.empty())
+            sError.append(ra::StringPrintf(L"\n* %s: %s", pAchievement.GetName(), sTriggerError));
+    }
+#endif
+}
+
+bool AssetListViewModel::ValidateAssetsForCore(std::vector<ra::data::models::AssetModelBase*>& vAssets, bool bCoreOnly)
+{
+    std::wstring sError;
+
+    for (const auto* pAsset : vAssets)
+    {
+        const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(pAsset);
+        if (pAchievement != nullptr)
+            ValidateAchievementForCore(sError, *pAchievement);
+    }
+
+    if (!sError.empty())
+    {
+        sError.insert(0, L"The following items could not be published:");
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(bCoreOnly ? L"Publish aborted." : L"Promote to core aborted.", sError);
+        return false;
+    }
+
+    return true;
+}
+
 void AssetListViewModel::Publish(std::vector<ra::data::models::AssetModelBase*>& vAssets)
 {
     AssetUploadViewModel vmAssetUpload;
@@ -879,6 +969,79 @@ void AssetListViewModel::RevertSelected()
 {
     if (!CanRevert())
         return;
+
+    std::vector<ra::data::models::AssetModelBase*> vAssetsToReset;
+    GetSelectedAssets(vAssetsToReset);
+
+    bool bHasLocal = false;
+    for (const auto* pAsset : vAssetsToReset)
+    {
+        if (pAsset != nullptr && pAsset->GetCategory() == ra::data::models::AssetCategory::Local)
+        {
+            bHasLocal = true;
+            break;
+        }
+    }
+
+    ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
+    vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
+    vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
+
+    std::wstring sWarningMessage;
+
+    if (GetRevertButtonText().at(0) == 'R') // Revert
+    {
+        vmMessageBox.SetHeader(L"Revert from server?");
+
+        if (vAssetsToReset.size() == m_vFilteredAssets.Count())
+            sWarningMessage = L"This will discard all local changes and revert the assets to the last state retrieved from the server.";
+        else
+            sWarningMessage = L"This will discard any local changes to the selected assets and revert them to the last state retrieved from the server.";
+
+        if (bHasLocal)
+            sWarningMessage += L"\n\n";
+    }
+    else // Delete
+    {
+        vmMessageBox.SetHeader(L"Permanently delete?");
+
+        Expects(bHasLocal); // message is shared with text appended to Revert message
+    }
+
+    if (bHasLocal)
+        sWarningMessage += L"Assets not available on the server will be permanently deleted.";
+    vmMessageBox.SetMessage(sWarningMessage);
+
+    if (vmMessageBox.ShowModal() == DialogResult::No)
+        return;
+
+    auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
+    auto& pAssets = pGameContext.Assets();
+    pAssets.BeginUpdate();
+
+    for (auto* pAsset : vAssetsToReset)
+    {
+        Expects(pAsset != nullptr);
+        if (pAsset->GetCategory() == ra::data::models::AssetCategory::Local)
+        {
+            // reverting a local item deletes it
+            pAsset->SetDeleted();
+        }
+        else
+        {
+            // Assert: New items should always be in the Local category
+            Expects(pAsset->GetChanges() != ra::data::models::AssetChanges::New);
+
+            // reverting a non-local item restores the server state
+            pAsset->RestoreServerCheckpoint();
+        }
+    }
+
+    pAssets.EndUpdate();
+
+    // update the local file
+    pAssets.SaveAssets(vAssetsToReset);
+    UpdateButtons();
 }
 
 void AssetListViewModel::CreateNew()
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index 2c24772d..be8d6ae4 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -185,6 +185,8 @@ public:
 
 protected:
     virtual void Publish(std::vector<ra::data::models::AssetModelBase*>& vAssets);
+    bool ValidateAssetsForCore(std::vector<ra::data::models::AssetModelBase*>& vAssets, bool bCoreOnly);
+    virtual void ValidateAchievementForCore(std::wstring& sError, const ra::data::models::AchievementModel& pAchievement) const;
 
 private:
     // DataModelCollectionBase::NotifyTarget
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index 125cd1e9..0f92506a 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -24,6 +24,10 @@ const IntModelProperty TriggerConditionViewModel::TargetValueProperty("TriggerCo
 const IntModelProperty TriggerConditionViewModel::CurrentHitsProperty("TriggerConditionViewModel", "CurrentHits", 0);
 const IntModelProperty TriggerConditionViewModel::RequiredHitsProperty("TriggerConditionViewModel", "RequiredHits", 0);
 const BoolModelProperty TriggerConditionViewModel::IsSelectedProperty("TriggerConditionViewModel", "IsSelected", false);
+const BoolModelProperty TriggerConditionViewModel::HasSourceSizeProperty("TriggerConditionViewModel", "HasSourceSize", true);
+const BoolModelProperty TriggerConditionViewModel::HasTargetProperty("TriggerConditionViewModel", "HasTarget", true);
+const BoolModelProperty TriggerConditionViewModel::HasTargetSizeProperty("TriggerConditionViewModel", "HasTargetSize", false);
+const BoolModelProperty TriggerConditionViewModel::HasHitsProperty("TriggerConditionViewModel", "HasHits", true);
 
 std::string TriggerConditionViewModel::Serialize() const
 {
@@ -50,6 +54,7 @@ void TriggerConditionViewModel::SerializeAppend(std::string& sBuffer) const
             case TriggerConditionType::MeasuredIf:  sBuffer.push_back('Q'); break;
             case TriggerConditionType::AddAddress:  sBuffer.push_back('I'); break;
             case TriggerConditionType::Trigger:     sBuffer.push_back('T'); break;
+            case TriggerConditionType::ResetNextIf: sBuffer.push_back('Z'); break;
             default:
                 assert(!"Unknown condition type");
                 break;
@@ -240,6 +245,51 @@ void TriggerConditionViewModel::InitializeFrom(const rc_condition_t& pCondition)
     SetRequiredHits(pCondition.required_hits);
 }
 
+void TriggerConditionViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
+{
+    if (args.Property == SourceTypeProperty)
+    {
+        SetValue(HasSourceSizeProperty, ra::itoe<TriggerOperandType>(args.tNewValue) != TriggerOperandType::Value);
+    }
+    else if (args.Property == TargetTypeProperty)
+    {
+        SetValue(HasTargetSizeProperty, ra::itoe<TriggerOperandType>(args.tNewValue) != TriggerOperandType::Value && GetValue(HasTargetProperty));
+    }
+    else if (args.Property == OperatorProperty)
+    {
+        SetValue(HasTargetProperty, ra::itoe<TriggerOperatorType>(args.tNewValue) != TriggerOperatorType::None);
+    }
+    else if (args.Property == TypeProperty)
+    {
+        const auto nOldType = ra::itoe<TriggerConditionType>(args.tOldValue);
+        const auto nNewType = ra::itoe<TriggerConditionType>(args.tNewValue);
+        const auto bIsModifying = IsModifying(nNewType);
+        if (bIsModifying != IsModifying(nOldType))
+        {
+            if (bIsModifying)
+            {
+                SetValue(HasHitsProperty, false);
+                SetOperator(TriggerOperatorType::None);
+            }
+            else
+            {
+                SetValue(HasHitsProperty, true);
+                SetOperator(TriggerOperatorType::Equals);
+            }
+        }
+    }
+
+    ViewModelBase::OnValueChanged(args);
+}
+
+void TriggerConditionViewModel::OnValueChanged(const BoolModelProperty::ChangeArgs& args)
+{
+    if (args.Property == HasTargetProperty)
+        SetValue(HasTargetSizeProperty, args.tNewValue && GetTargetType() != TriggerOperandType::Value);
+
+    ViewModelBase::OnValueChanged(args);
+}
+
 std::wstring TriggerConditionViewModel::GetTooltip(const IntModelProperty& nProperty) const
 {
     if (nProperty == SourceValueProperty)
@@ -263,7 +313,7 @@ std::wstring TriggerConditionViewModel::GetValueTooltip(TriggerOperandType nType
     return std::to_wstring(nValue);
 }
 
-std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned nAddress) const
+std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned int nAddress) const
 {
     const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
     const auto* pNote = pGameContext.FindCodeNote(nAddress);
@@ -292,6 +342,40 @@ std::wstring TriggerConditionViewModel::GetAddressTooltip(unsigned nAddress) con
     return ra::StringPrintf(L"%s\r\n%s", ra::ByteAddressToString(nAddress), *pNote);
 }
 
+bool TriggerConditionViewModel::IsModifying(TriggerConditionType nType) noexcept
+{
+    switch (nType)
+    {
+        case TriggerConditionType::AddAddress:
+        case TriggerConditionType::AddSource:
+        case TriggerConditionType::SubSource:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+bool TriggerConditionViewModel::IsComparisonVisible(const ViewModelBase& vmItem, int nValue)
+{
+    const auto* vmCondition = dynamic_cast<const TriggerConditionViewModel*>(&vmItem);
+    if (vmCondition == nullptr)
+        return false;
+
+    const auto nComparison = ra::itoe<TriggerOperatorType>(nValue);
+    switch (nComparison)
+    {
+        case TriggerOperatorType::None:
+        case TriggerOperatorType::Multiply:
+        case TriggerOperatorType::Divide:
+        case TriggerOperatorType::BitwiseAnd:
+            return vmCondition->IsModifying();
+
+        default:
+            return !vmCondition->IsModifying();
+    }
+}
+
 } // namespace viewmodels
 } // namespace ui
 } // namespace ra
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.hh b/src/ui/viewmodels/TriggerConditionViewModel.hh
index baf3fe85..40a5aa23 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.hh
+++ b/src/ui/viewmodels/TriggerConditionViewModel.hh
@@ -27,7 +27,8 @@ enum class TriggerConditionType : uint8_t
     AddAddress = RC_CONDITION_ADD_ADDRESS,
     OrNext = RC_CONDITION_OR_NEXT,
     Trigger = RC_CONDITION_TRIGGER,
-    MeasuredIf = RC_CONDITION_MEASURED_IF
+    MeasuredIf = RC_CONDITION_MEASURED_IF,
+    ResetNextIf = RC_CONDITION_RESET_NEXT_IF
 };
 
 enum class TriggerOperandType : uint8_t
@@ -68,6 +69,7 @@ public:
     TriggerOperandType GetSourceType() const { return ra::itoe<TriggerOperandType>(GetValue(SourceTypeProperty)); }
     void SetSourceType(TriggerOperandType nValue) { SetValue(SourceTypeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasSourceSizeProperty;
     static const IntModelProperty SourceSizeProperty;
     MemSize GetSourceSize() const { return ra::itoe<MemSize>(GetValue(SourceSizeProperty)); }
     void SetSourceSize(MemSize nValue) { SetValue(SourceSizeProperty, ra::etoi(nValue)); }
@@ -80,10 +82,12 @@ public:
     TriggerOperatorType GetOperator() const { return ra::itoe<TriggerOperatorType>(GetValue(OperatorProperty)); }
     void SetOperator(TriggerOperatorType nValue) { SetValue(OperatorProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasTargetProperty;
     static const IntModelProperty TargetTypeProperty;
     TriggerOperandType GetTargetType() const { return ra::itoe<TriggerOperandType>(GetValue(TargetTypeProperty)); }
     void SetTargetType(TriggerOperandType nValue) { SetValue(TargetTypeProperty, ra::etoi(nValue)); }
 
+    static const BoolModelProperty HasTargetSizeProperty;
     static const IntModelProperty TargetSizeProperty;
     MemSize GetTargetSize() const { return ra::itoe<MemSize>(GetValue(TargetSizeProperty)); }
     void SetTargetSize(MemSize nValue) { SetValue(TargetSizeProperty, ra::etoi(nValue)); }
@@ -92,6 +96,7 @@ public:
     unsigned int GetTargetValue() const { return ra::to_unsigned(GetValue(TargetValueProperty)); }
     void SetTargetValue(unsigned int nValue) { SetValue(TargetValueProperty, ra::to_signed(nValue)); }
 
+    static const BoolModelProperty HasHitsProperty;
     static const IntModelProperty CurrentHitsProperty;
     unsigned int GetCurrentHits() const { return ra::to_unsigned(GetValue(CurrentHitsProperty)); }
     void SetCurrentHits(unsigned int nValue) { SetValue(CurrentHitsProperty, ra::to_signed(nValue)); }
@@ -111,14 +116,23 @@ public:
 
     std::wstring GetTooltip(const IntModelProperty& nProperty) const;
 
+    bool IsModifying() const { return IsModifying(GetType()); }
+
+    static bool IsComparisonVisible(const ViewModelBase& vmItem, int nValue);
+
 private:
+    void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
+    void OnValueChanged(const BoolModelProperty::ChangeArgs& args) override;
+
     void SerializeAppendOperand(std::string& sBuffer, TriggerOperandType nType, MemSize nSize, unsigned int nValue) const;
 
-    std::wstring GetValueTooltip(TriggerOperandType nType, unsigned nAddress) const;
-    std::wstring GetAddressTooltip(unsigned nAddress) const;
+    std::wstring GetValueTooltip(TriggerOperandType nType, unsigned int nValue) const;
+    std::wstring GetAddressTooltip(unsigned int nAddress) const;
 
     void SetOperand(const IntModelProperty& pTypeProperty, const IntModelProperty& pSizeProperty,
         const IntModelProperty& pValueProperty, const rc_operand_t& operand);
+
+    static bool IsModifying(TriggerConditionType nType) noexcept;
 };
 
 } // namespace viewmodels
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index 7c9749c9..24e6bdf4 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -26,6 +26,7 @@ TriggerViewModel::TriggerViewModel() noexcept
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::Standard), L"");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::PauseIf), L"Pause If");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::ResetIf), L"Reset If");
+    m_vConditionTypes.Add(ra::etoi(TriggerConditionType::ResetNextIf), L"Reset Next If");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AddSource), L"Add Source");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::SubSource), L"Sub Source");
     m_vConditionTypes.Add(ra::etoi(TriggerConditionType::AddHits), L"Add Hits");
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 627c74d2..8f8da680 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -28,7 +28,7 @@ namespace ui {
 namespace win32 {
 
 static constexpr int COLUMN_WIDTH_ID = 30;
-static constexpr int COLUMN_WIDTH_FLAG = 75;
+static constexpr int COLUMN_WIDTH_FLAG = 78;
 static constexpr int COLUMN_WIDTH_TYPE = 42;
 static constexpr int COLUMN_WIDTH_SIZE = 54;
 static constexpr int COLUMN_WIDTH_VALUE = 72;
@@ -64,28 +64,37 @@ void AssetEditorDialog::Presenter::OnClosed() noexcept { m_pDialog.reset(); }
 
 // ------------------------------------
 
-class SizeColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding
+class HideableGridLookupColumnBinding : public ra::ui::win32::bindings::GridLookupColumnBinding
 {
 public:
-    SizeColumnBinding(const IntModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty, const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept
-        : ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems), m_pTypeProperty(&pTypeProperty)
+    HideableGridLookupColumnBinding(const IntModelProperty& pBoundProperty, const BoolModelProperty& pIsHiddenProperty,
+        const ra::ui::viewmodels::LookupItemViewModelCollection& vmItems) noexcept
+        : ra::ui::win32::bindings::GridLookupColumnBinding(pBoundProperty, vmItems), m_pIsHiddenProperty(pIsHiddenProperty)
     {
     }
 
     std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        if (IsValueHidden(vmItems, nIndex))
+        if (IsHidden(vmItems, nIndex))
             return L"";
 
         return GridLookupColumnBinding::GetText(vmItems, nIndex);
     }
 
-    bool DependsOn(const ra::ui::IntModelProperty& pProperty) const noexcept override
+    std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
-        if (pProperty == *m_pTypeProperty)
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return GridLookupColumnBinding::GetTooltip(vmItems, nIndex);
+    }
+
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
+    {
+        if (pProperty == m_pIsHiddenProperty)
             return true;
 
-        return GridLookupColumnBinding::DependsOn(pProperty);
+        return GridColumnBinding::DependsOn(pProperty);
     }
 
     HWND CreateInPlaceEditor(HWND hParent, InPlaceEditorInfo& pInfo) override
@@ -93,20 +102,19 @@ public:
         auto* pGridBinding = static_cast<ra::ui::win32::bindings::GridBinding*>(pInfo.pGridBinding);
         Expects(pGridBinding != nullptr);
 
-        if (IsValueHidden(pGridBinding->GetItems(), pInfo.nItemIndex))
+        if (IsHidden(pGridBinding->GetItems(), pInfo.nItemIndex))
             return nullptr;
 
         return GridLookupColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
 
 protected:
-    bool IsValueHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
     {
-        const auto nOperandType = ra::itoe<ra::ui::viewmodels::TriggerOperandType>(vmItems.GetItemValue(nIndex, *m_pTypeProperty));
-        return (nOperandType == ra::ui::viewmodels::TriggerOperandType::Value);
+        return !vmItems.GetItemValue(nIndex, m_pIsHiddenProperty);
     }
 
-    const IntModelProperty* m_pTypeProperty = nullptr;
+    const BoolModelProperty& m_pIsHiddenProperty;
 };
 
 class ValueColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
@@ -198,6 +206,53 @@ protected:
     const IntModelProperty* m_pTypeProperty = nullptr;
 };
 
+class TargetValueColumnBinding : public ValueColumnBinding
+{
+public:
+    TargetValueColumnBinding(const IntModelProperty& pBoundProperty, const IntModelProperty& pTypeProperty) noexcept
+        : ValueColumnBinding(pBoundProperty, pTypeProperty)
+    {
+    }
+
+    std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return ValueColumnBinding::GetText(vmItems, nIndex);
+    }
+
+    bool DependsOn(const ra::ui::BoolModelProperty& pProperty) const override
+    {
+        if (pProperty == TriggerConditionViewModel::HasTargetProperty)
+            return true;
+
+        return GridColumnBinding::DependsOn(pProperty);
+    }
+
+    std::wstring GetTooltip(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return L"";
+
+        return ValueColumnBinding::GetTooltip(vmItems, nIndex);
+    }
+
+    bool HandleRightClick(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) override
+    {
+        if (IsHidden(vmItems, nIndex))
+            return false;
+
+        return ValueColumnBinding::HandleRightClick(vmItems, nIndex);
+    }
+
+protected:
+    bool IsHidden(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
+    {
+        return !vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasTargetProperty);
+    }
+};
+
 class HitsColumnBinding : public ra::ui::win32::bindings::GridNumberColumnBinding
 {
 public:
@@ -208,6 +263,9 @@ public:
 
     std::wstring GetText(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const override
     {
+        if (!vmItems.GetItemValue(nIndex, TriggerConditionViewModel::HasHitsProperty))
+            return L"";
+
         const auto nCurrentHits = vmItems.GetItemValue(nIndex, TriggerConditionViewModel::CurrentHitsProperty);
         const auto nRequiredHits = vmItems.GetItemValue(nIndex, *m_pBoundProperty);
 
@@ -224,12 +282,19 @@ public:
     {
         if (pProperty == TriggerConditionViewModel::CurrentHitsProperty)
             return true;
+        if (pProperty == TriggerConditionViewModel::HasHitsProperty)
+            return true;
 
         return GridNumberColumnBinding::DependsOn(pProperty);
     }
 
     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::HasHitsProperty))
+            return nullptr;
+
         m_nEditingItemIndex = pInfo.nItemIndex;
         return GridNumberColumnBinding::CreateInPlaceEditor(hParent, pInfo);
     }
@@ -416,8 +481,8 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
     pSourceTypeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(2, std::move(pSourceTypeColumn));
 
-    auto pSourceSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::SourceSizeProperty,
-        TriggerConditionViewModel::SourceTypeProperty, vmAssetEditor.Trigger().OperandSizes());
+    auto pSourceSizeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::SourceSizeProperty,
+        TriggerConditionViewModel::HasSourceSizeProperty, vmAssetEditor.Trigger().OperandSizes());
     pSourceSizeColumn->SetHeader(L"Size");
     pSourceSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
     pSourceSizeColumn->SetReadOnly(false);
@@ -434,24 +499,25 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
         TriggerConditionViewModel::OperatorProperty, vmAssetEditor.Trigger().OperatorTypes());
     pOperatorColumn->SetHeader(L"Cmp");
     pOperatorColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_OPERATOR);
+    pOperatorColumn->SetVisibilityFilter(TriggerConditionViewModel::IsComparisonVisible);
     pOperatorColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(5, std::move(pOperatorColumn));
 
-    auto pTargetTypeColumn = std::make_unique<ra::ui::win32::bindings::GridLookupColumnBinding>(
-        TriggerConditionViewModel::TargetTypeProperty, vmAssetEditor.Trigger().OperandTypes());
+    auto pTargetTypeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::TargetTypeProperty,
+        TriggerConditionViewModel::HasTargetProperty, vmAssetEditor.Trigger().OperandTypes());
     pTargetTypeColumn->SetHeader(L"Type");
     pTargetTypeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_TYPE);
     pTargetTypeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(6, std::move(pTargetTypeColumn));
 
-    auto pTargetSizeColumn = std::make_unique<SizeColumnBinding>(TriggerConditionViewModel::TargetSizeProperty,
-        TriggerConditionViewModel::TargetTypeProperty, vmAssetEditor.Trigger().OperandSizes());
+    auto pTargetSizeColumn = std::make_unique<HideableGridLookupColumnBinding>(TriggerConditionViewModel::TargetSizeProperty,
+        TriggerConditionViewModel::HasTargetSizeProperty, vmAssetEditor.Trigger().OperandSizes());
     pTargetSizeColumn->SetHeader(L"Size");
     pTargetSizeColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_SIZE);
     pTargetSizeColumn->SetReadOnly(false);
     m_bindConditions.BindColumn(7, std::move(pTargetSizeColumn));
 
-    auto pTargetValueColumn = std::make_unique<ValueColumnBinding>(
+    auto pTargetValueColumn = std::make_unique<TargetValueColumnBinding>(
         TriggerConditionViewModel::TargetValueProperty, TriggerConditionViewModel::TargetTypeProperty);
     pTargetValueColumn->SetHeader(L"Mem/Val");
     pTargetValueColumn->SetWidth(ra::ui::win32::bindings::GridColumnBinding::WidthType::Pixels, COLUMN_WIDTH_VALUE);
diff --git a/src/ui/win32/bindings/GridLookupColumnBinding.cpp b/src/ui/win32/bindings/GridLookupColumnBinding.cpp
index 7a442e0a..a5096f3c 100644
--- a/src/ui/win32/bindings/GridLookupColumnBinding.cpp
+++ b/src/ui/win32/bindings/GridLookupColumnBinding.cpp
@@ -90,13 +90,30 @@ HWND GridLookupColumnBinding::CreateInPlaceEditor(HWND hParent, InPlaceEditorInf
 
     const auto& pItems = static_cast<GridBinding*>(pInfo.pGridBinding)->GetItems();
     const auto nValue = pItems.GetItemValue(pInfo.nItemIndex, *m_pBoundProperty);
+
+    const ViewModelBase* vmItem = nullptr;
+    if (m_fIsVisible)
+        vmItem = pItems.GetViewModelAt(pInfo.nItemIndex);
+
+    m_vVisibleItems.clear();
+
     for (size_t i = 0; i < m_vmItems.Count(); ++i)
     {
         const auto* pItem = m_vmItems.GetItemAt(i);
         Expects(pItem != nullptr);
+        const auto nItemValue = pItem->GetId();
+
+        if (vmItem != nullptr)
+        {
+            if (nItemValue != nValue && !m_fIsVisible(*vmItem, nItemValue))
+                continue;
+
+            m_vVisibleItems.push_back(nItemValue);
+        }
+
         const auto nIndex = ComboBox_AddString(hInPlaceEditor, NativeStr(pItem->GetLabel()).c_str());
 
-        if (pItem->GetId() == nValue)
+        if (nItemValue == nValue)
             ComboBox_SetCurSel(hInPlaceEditor, nIndex);
     }
 
@@ -110,6 +127,9 @@ HWND GridLookupColumnBinding::CreateInPlaceEditor(HWND hParent, InPlaceEditorInf
 
 int GridLookupColumnBinding::GetValueFromIndex(gsl::index nIndex) const
 {
+    if (m_fIsVisible != nullptr)
+        return m_vVisibleItems.at(nIndex);
+
     const auto* pItem = m_vmItems.GetItemAt(nIndex);
     return (pItem != nullptr) ? pItem->GetId() : 0;        
 }
diff --git a/src/ui/win32/bindings/GridLookupColumnBinding.hh b/src/ui/win32/bindings/GridLookupColumnBinding.hh
index e211d1a0..9e818f86 100644
--- a/src/ui/win32/bindings/GridLookupColumnBinding.hh
+++ b/src/ui/win32/bindings/GridLookupColumnBinding.hh
@@ -34,9 +34,15 @@ public:
     int GetValueFromIndex(gsl::index nIndex) const;
     const IntModelProperty& GetBoundProperty() const noexcept { return *m_pBoundProperty; }
 
+    typedef bool (*IsVisibleFunction)(const ViewModelBase& vmItem, int nValue);
+    void SetVisibilityFilter(IsVisibleFunction fIsVisible) noexcept { m_fIsVisible = fIsVisible; }
+
 protected:
     const IntModelProperty* m_pBoundProperty = nullptr;
     const ra::ui::viewmodels::LookupItemViewModelCollection& m_vmItems;
+
+    IsVisibleFunction m_fIsVisible = nullptr;
+    std::vector<int> m_vVisibleItems;
 };
 
 } // namespace bindings


Test patch:
diff --git a/tests/data/DataAsserts.hh b/tests/data/DataAsserts.hh
index 92db5ebe..1decf744 100644
--- a/tests/data/DataAsserts.hh
+++ b/tests/data/DataAsserts.hh
@@ -113,6 +113,8 @@ std::wstring ToString<ra::data::models::AssetChanges>(
             return L"Unpublished";
         case ra::data::models::AssetChanges::New:
             return L"New";
+        case ra::data::models::AssetChanges::Deleted:
+            return L"Deleted";
         default:
             return std::to_wstring(static_cast<int>(nAssetChanges));
     }
diff --git a/tests/data/context/GameAssets_Tests.cpp b/tests/data/context/GameAssets_Tests.cpp
index 6edfb1b4..30d85acd 100644
--- a/tests/data/context/GameAssets_Tests.cpp
+++ b/tests/data/context/GameAssets_Tests.cpp
@@ -191,6 +191,41 @@ public:
         Assert::AreEqual(sExpected, gameAssets.GetUserFile());
     }
 
+    TEST_METHOD(TestSaveDeleted)
+    {
+        GameAssetsHarness gameAssets;
+        gameAssets.MockUserFileContents(
+            "111000001:\"0xH1234=0\":Test:::::User:0:0:0:::00000\n"
+            "111000002:\"0xH2345=0\":Test2:::::User:0:0:0:::00000\n");
+
+        gameAssets.ReloadAllAssets();
+        Assert::AreEqual({ 2U }, gameAssets.Count());
+
+        auto* pAsset = gameAssets.FindAchievement({ 111000001U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        Assert::AreEqual(std::string("0xH1234=0"), pAsset->GetTrigger());
+        Assert::AreEqual(AssetCategory::Local, pAsset->GetCategory());
+        Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
+
+        pAsset->SetDeleted();
+        Assert::AreEqual(AssetChanges::Deleted, pAsset->GetChanges());
+
+        gameAssets.SaveAllAssets();
+
+        const std::string sExpected = "0.0.0.0\nGameName\n"
+            "111000002:\"0xH2345=0\":Test2:::::User:0:::::00000\n";
+        Assert::AreEqual(sExpected, gameAssets.GetUserFile());
+
+        Assert::AreEqual({ 1U }, gameAssets.Count());
+
+        pAsset = gameAssets.FindAchievement({ 111000001U });
+        Assert::IsNull(pAsset);
+
+        pAsset = gameAssets.FindAchievement({ 111000002U });
+        Assert::IsNotNull(pAsset);
+    }
+
     TEST_METHOD(TestMergeLocalAssetsNoFile)
     {
         GameAssetsHarness gameAssets;
diff --git a/tests/ui/ViewModelCollection_Tests.cpp b/tests/ui/ViewModelCollection_Tests.cpp
index c3079f6f..25b4d79b 100644
--- a/tests/ui/ViewModelCollection_Tests.cpp
+++ b/tests/ui/ViewModelCollection_Tests.cpp
@@ -249,6 +249,28 @@ public:
         Assert::AreEqual(TestViewModel::BoolProperty.GetDefaultValue(), vmCollection.GetItemValue(1, TestViewModel::BoolProperty));
     }
 
+    TEST_METHOD(TestGetViewModelAt)
+    {
+        ViewModelCollection<TestViewModel> vmCollection;
+        auto& pItem1 = vmCollection.Add(1, L"Test1");
+        pItem1.SetBool(false);
+
+        auto& pItem2 = vmCollection.Add(2, L"Test2");
+        pItem2.SetBool(true);
+
+        const auto* vm1 = vmCollection.GetViewModelAt(0);
+        Assert::IsTrue(vm1 == &pItem1);
+
+        const auto* vm2 = vmCollection.GetViewModelAt(1);
+        Assert::IsTrue(vm2 == &pItem2);
+
+        const auto* vm3 = vmCollection.GetViewModelAt(2);
+        Assert::IsNull(vm3);
+
+        const auto* vm0 = vmCollection.GetViewModelAt(-1);
+        Assert::IsNull(vm0);
+    }
+
     TEST_METHOD(TestStringPropertyNotification)
     {
         ViewModelCollection<TestViewModel> vmCollection;
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index 90a9448a..124b893b 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -353,6 +353,21 @@ private:
         }
 
         std::vector<ra::data::models::AssetModelBase*> PublishedAssets;
+
+        void SetPublishError(ra::AchievementID nId, const std::wstring& sError)
+        {
+            m_mPublishErrors.insert_or_assign(nId, sError);
+        }
+
+        void ValidateAchievementForCore(std::wstring& sError, const ra::data::models::AchievementModel& pAchievement) const override
+        {
+            const auto pIter = m_mPublishErrors.find(pAchievement.GetID());
+            if (pIter != m_mPublishErrors.end())
+                sError.append(ra::StringPrintf(L"\n* %s: %s", pAchievement.GetName(), pIter->second));
+        }
+
+    private:
+        std::map<ra::AchievementID, std::wstring> m_mPublishErrors;
     };
 
 public:
@@ -386,7 +401,7 @@ public:
         Assert::AreEqual((int)AssetCategory::Local, vmAssetList.Categories().GetItemAt(2)->GetId());
         Assert::AreEqual(std::wstring(L"Local"), vmAssetList.Categories().GetItemAt(2)->GetLabel());
 
-        Assert::AreEqual({ 4U }, vmAssetList.Changes().Count());
+        Assert::AreEqual({ 5U }, vmAssetList.Changes().Count());
         Assert::AreEqual((int)AssetChanges::None, vmAssetList.Changes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L""), vmAssetList.Changes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)AssetChanges::Modified, vmAssetList.Changes().GetItemAt(1)->GetId());
@@ -395,6 +410,8 @@ public:
         Assert::AreEqual(std::wstring(L"Unpublished"), vmAssetList.Changes().GetItemAt(2)->GetLabel());
         Assert::AreEqual((int)AssetChanges::New, vmAssetList.Changes().GetItemAt(3)->GetId());
         Assert::AreEqual(std::wstring(L"New"), vmAssetList.Changes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)AssetChanges::Deleted, vmAssetList.Changes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L"Deleted"), vmAssetList.Changes().GetItemAt(4)->GetLabel());
     }
 
     TEST_METHOD(TestAddRemoveCoreAchievement)
@@ -1525,6 +1542,42 @@ public:
         vmAssetList.AssertButtonState(SaveButtonState::Publish);
     }
 
+    TEST_METHOD(TestSaveSelectedPublishCoreModifiedAbort)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetPublishError(1U, L"NewFeature is pre-release functionality");
+
+        bool bDialogSeen = false;
+        vmAssetList.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            bDialogSeen = true;
+            Assert::AreEqual(std::wstring(L"Publish aborted."), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"The following items could not be published:\n* Test1b: NewFeature is pre-release functionality"), vmMessageBox.GetMessage());
+            return DialogResult::OK;
+        });
+
+        auto* pItem = dynamic_cast<ra::data::models::AchievementModel*>(vmAssetList.mockGameContext.Assets().GetItemAt(0));
+        Expects(pItem != nullptr);
+        pItem->SetName(L"Test1b");
+        pItem->UpdateLocalCheckpoint();
+        Assert::AreEqual(AssetChanges::Unpublished, pItem->GetChanges());
+
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(SaveButtonState::Publish);
+        vmAssetList.SaveSelected();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({ 0U }, vmAssetList.PublishedAssets.size());
+        Assert::AreEqual(AssetChanges::Unpublished, pItem->GetChanges());
+
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(SaveButtonState::Publish);
+    }
+
     TEST_METHOD(TestSaveSelectedPublishCoreAll)
     {
         AssetListViewModelHarness vmAssetList;
@@ -1717,6 +1770,39 @@ public:
         vmAssetList.AssertButtonState(SaveButtonState::SaveAllDisabled);
     }
 
+    TEST_METHOD(TestSaveSelectedPromoteToCoreAbort)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Unofficial, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetFilterCategory(AssetCategory::Unofficial);
+        vmAssetList.SetPublishError(1U, L"Test excuse");
+
+        bool bDialogSeen = false;
+        vmAssetList.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bDialogSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            bDialogSeen = true;
+            Assert::AreEqual(std::wstring(L"Promote to core aborted."), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"The following items could not be published:\n* Test1: Test excuse"), vmMessageBox.GetMessage());
+            return DialogResult::OK;
+        });
+
+        const auto* pItem = dynamic_cast<ra::data::models::AchievementModel*>(vmAssetList.mockGameContext.Assets().GetItemAt(0));
+        Expects(pItem != nullptr);
+        Assert::AreEqual(AssetChanges::None, pItem->GetChanges());
+
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(SaveButtonState::Promote);
+        vmAssetList.SaveSelected();
+
+        Assert::IsTrue(bDialogSeen);
+
+        Assert::AreEqual({ 0U }, vmAssetList.PublishedAssets.size());
+        Assert::AreEqual(AssetChanges::None, pItem->GetChanges());
+        Assert::AreEqual(AssetCategory::Unofficial, pItem->GetCategory());
+    }
+
     TEST_METHOD(TestSaveSelectedDemoteToUnofficial)
     {
         AssetListViewModelHarness vmAssetList;
@@ -2301,6 +2387,167 @@ public:
         Assert::IsNull(vmAssetList.mockGameContext.Assets().FindAchievement({ 111000001U }));
     }
 
+    TEST_METHOD(TestRevertSelectedAllUnmodified)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        bool bDialogShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([&bDialogShown](MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Revert from server?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"This will discard all local changes and revert the assets to the last state retrieved from the server."), vmMessageBox.GetMessage());
+
+            bDialogShown = true;
+            return DialogResult::Yes;
+        });
+
+        vmAssetList.RevertSelected();
+
+        Assert::IsTrue(bDialogShown);
+
+        const auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        Assert::AreEqual(AssetCategory::Core, pAsset->GetCategory());
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestRevertSelectedAllUncommitted)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        bool bDialogShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([&bDialogShown](MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Revert from server?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"This will discard all local changes and revert the assets to the last state retrieved from the server."), vmMessageBox.GetMessage());
+
+            bDialogShown = true;
+            return DialogResult::Yes;
+        });
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+
+        vmAssetList.RevertSelected();
+
+        Assert::IsTrue(bDialogShown);
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestRevertSelectedAllUnpublished)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        bool bDialogShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([&bDialogShown](MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Revert from server?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"This will discard all local changes and revert the assets to the last state retrieved from the server."), vmMessageBox.GetMessage());
+
+            bDialogShown = true;
+            return DialogResult::Yes;
+        });
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        pAsset->UpdateLocalCheckpoint();
+        Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
+
+        vmAssetList.RevertSelected();
+
+        Assert::IsTrue(bDialogShown);
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestRevertSelectedAllLocalUnpublished)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Local, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetFilterCategory(AssetCategory::Local);
+        vmAssetList.ForceUpdateButtons();
+
+        // when nothing is selected, the button defaults to revert all, even if only local achievements are present
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        bool bDialogShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([&bDialogShown](MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Revert from server?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"This will discard all local changes and revert the assets to the last state retrieved from the server.\n\nAssets not available on the server will be permanently deleted."), vmMessageBox.GetMessage());
+
+            bDialogShown = true;
+            return DialogResult::Yes;
+        });
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        pAsset->UpdateLocalCheckpoint();
+        Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
+
+        vmAssetList.RevertSelected();
+
+        Assert::IsTrue(bDialogShown);
+
+        pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNull(pAsset);
+    }
+
+    TEST_METHOD(TestRevertSelectedLocalUnpublished)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Local, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetFilterCategory(AssetCategory::Local);
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::Delete);
+
+        bool bDialogShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([&bDialogShown](MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Permanently delete?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"Assets not available on the server will be permanently deleted."), vmMessageBox.GetMessage());
+
+            bDialogShown = true;
+            return DialogResult::Yes;
+        });
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        pAsset->UpdateLocalCheckpoint();
+        Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
+
+        vmAssetList.RevertSelected();
+
+        Assert::IsTrue(bDialogShown);
+
+        pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNull(pAsset);
+    }
+
     TEST_METHOD(TestIsProcessingActive)
     {
         AssetListViewModelHarness vmAssetList;
diff --git a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
index a2dfbc48..b86bedd0 100644
--- a/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerConditionViewModel_Tests.cpp
@@ -43,6 +43,8 @@ std::wstring ToString<ra::ui::viewmodels::TriggerConditionType>(
             return L"Trigger";
         case ra::ui::viewmodels::TriggerConditionType::MeasuredIf:
             return L"MeasuredIf";
+        case ra::ui::viewmodels::TriggerConditionType::ResetNextIf:
+            return L"ResetNextIf";
         default:
             return std::to_wstring(static_cast<int>(nConditionType));
     }
@@ -130,6 +132,11 @@ private:
             TriggerConditionViewModel::InitializeFrom(*pTrigger->requirement->conditions);
         }
 
+        bool HasSourceSize() const { return GetValue(HasSourceSizeProperty); }
+        bool HasTargetSize() const { return GetValue(HasTargetSizeProperty); }
+        bool HasTarget() const { return GetValue(HasTargetProperty); }
+        bool HasHits() const { return GetValue(HasHitsProperty); }
+
     private:
         std::string sBuffer;
     };
@@ -154,7 +161,7 @@ private:
 public:
     TEST_METHOD(TestInitialState)
     {
-        TriggerConditionViewModel vmCondition;
+        TriggerConditionViewModelHarness vmCondition;
         Assert::AreEqual(1, vmCondition.GetIndex());
         Assert::AreEqual(TriggerConditionType::Standard, vmCondition.GetType());
         Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
@@ -166,6 +173,204 @@ public:
         Assert::AreEqual({ 0U }, vmCondition.GetTargetValue());
         Assert::AreEqual(0U, vmCondition.GetCurrentHits());
         Assert::AreEqual(0U, vmCondition.GetRequiredHits());
+
+        Assert::IsTrue(vmCondition.HasSourceSize());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+        Assert::IsTrue(vmCondition.HasHits());
+    }
+
+    TEST_METHOD(TestHasSourceSize)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::BCD);
+        Assert::AreEqual(TriggerOperandType::BCD, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Delta);
+        Assert::AreEqual(TriggerOperandType::Delta, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Prior);
+        Assert::AreEqual(TriggerOperandType::Prior, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Value);
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetSourceType());
+        Assert::IsFalse(vmCondition.HasSourceSize());
+
+        vmCondition.SetSourceType(TriggerOperandType::Address);
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetSourceType());
+        Assert::IsTrue(vmCondition.HasSourceSize());
+    }
+
+    TEST_METHOD(TestHasTarget)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperatorType::Equals, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::NotEquals);
+        Assert::AreEqual(TriggerOperatorType::NotEquals, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::LessThan);
+        Assert::AreEqual(TriggerOperatorType::LessThan, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::LessThanOrEqual);
+        Assert::AreEqual(TriggerOperatorType::LessThanOrEqual, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::GreaterThan);
+        Assert::AreEqual(TriggerOperatorType::GreaterThan, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::GreaterThanOrEqual);
+        Assert::AreEqual(TriggerOperatorType::GreaterThanOrEqual, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::AreEqual(TriggerOperatorType::None, vmCondition.GetOperator());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Multiply);
+        Assert::AreEqual(TriggerOperatorType::Multiply, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Divide);
+        Assert::AreEqual(TriggerOperatorType::Divide, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::BitwiseAnd);
+        Assert::AreEqual(TriggerOperatorType::BitwiseAnd, vmCondition.GetOperator());
+        Assert::IsTrue(vmCondition.HasTarget());
+    }
+
+    TEST_METHOD(TestHasTargetSize)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::AreEqual(TriggerOperatorType::Equals, vmCondition.GetOperator());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Address);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Delta);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::Equals);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::None);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetTargetType(TriggerOperandType::Prior);
+        Assert::IsFalse(vmCondition.HasTargetSize());
+        Assert::IsFalse(vmCondition.HasTarget());
+
+        vmCondition.SetOperator(TriggerOperatorType::NotEquals);
+        Assert::IsTrue(vmCondition.HasTargetSize());
+        Assert::IsTrue(vmCondition.HasTarget());
+    }
+
+    TEST_METHOD(TestHasTargetSizeHasTarget)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Address);
+        Assert::AreEqual(TriggerOperandType::Address, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::BCD);
+        Assert::AreEqual(TriggerOperandType::BCD, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Delta);
+        Assert::AreEqual(TriggerOperandType::Delta, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Prior);
+        Assert::AreEqual(TriggerOperandType::Prior, vmCondition.GetTargetType());
+        Assert::IsTrue(vmCondition.HasTargetSize());
+
+        vmCondition.SetTargetType(TriggerOperandType::Value);
+        Assert::AreEqual(TriggerOperandType::Value, vmCondition.GetTargetType());
+        Assert::IsFalse(vmCondition.HasTargetSize());
+    }
+
+    TEST_METHOD(TestHasHits)
+    {
+        TriggerConditionViewModelHarness vmCondition;
+        Assert::AreEqual(TriggerConditionType::Standard, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddAddress);
+        Assert::AreEqual(TriggerConditionType::AddAddress, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddHits);
+        Assert::AreEqual(TriggerConditionType::AddHits, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AddSource);
+        Assert::AreEqual(TriggerConditionType::AddSource, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::AndNext);
+        Assert::AreEqual(TriggerConditionType::AndNext, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::Measured);
+        Assert::AreEqual(TriggerConditionType::Measured, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::MeasuredIf);
+        Assert::AreEqual(TriggerConditionType::MeasuredIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::OrNext);
+        Assert::AreEqual(TriggerConditionType::OrNext, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::PauseIf);
+        Assert::AreEqual(TriggerConditionType::PauseIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::ResetIf);
+        Assert::AreEqual(TriggerConditionType::ResetIf, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::SubSource);
+        Assert::AreEqual(TriggerConditionType::SubSource, vmCondition.GetType());
+        Assert::IsFalse(vmCondition.HasHits());
+
+        vmCondition.SetType(TriggerConditionType::Trigger);
+        Assert::AreEqual(TriggerConditionType::Trigger, vmCondition.GetType());
+        Assert::IsTrue(vmCondition.HasHits());
     }
 
     TEST_METHOD(TestSizes)
@@ -204,6 +409,7 @@ public:
         ParseAndRegenerate("0xH1234<=5"); // less than or equal
         ParseAndRegenerate("0xH1234>5"); // greater than
         ParseAndRegenerate("0xH1234>=5"); // greater than or equal
+        ParseAndRegenerate("A:0xH1234"); // none
         ParseAndRegenerate("A:0xH1234*5"); // multiply
         ParseAndRegenerate("A:0xH1234/5"); // divide
         ParseAndRegenerate("A:0xH1234&5"); // bitwise and
@@ -213,6 +419,7 @@ public:
     {
         ParseAndRegenerate("0xH1234=5"); // none
         ParseAndRegenerate("R:0xH1234=5"); // reset if
+        ParseAndRegenerate("Z:0xH1234=5"); // reset next if
         ParseAndRegenerate("P:0xH1234=5"); // pause if
         ParseAndRegenerate("A:0xH1234"); // add source
         ParseAndRegenerate("B:0xH1234"); // sub source
@@ -297,6 +504,114 @@ public:
         Assert::AreEqual(std::wstring(L"255"), condition.GetTooltip(TriggerConditionViewModel::SourceValueProperty));
         Assert::AreEqual(std::wstring(L"99"), condition.GetTooltip(TriggerConditionViewModel::TargetValueProperty));
     }
+
+    TEST_METHOD(TestIsModifying)
+    {
+        TriggerConditionViewModelHarness condition;
+
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AddHits);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AddSource);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::AndNext);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Measured);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::MeasuredIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::OrNext);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::PauseIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::ResetIf);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::IsFalse(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::SubSource);
+        Assert::IsTrue(condition.IsModifying());
+
+        condition.SetType(TriggerConditionType::Trigger);
+        Assert::IsFalse(condition.IsModifying());
+    }
+
+    TEST_METHOD(TestSwitchBetweenModifyingAndNonModifying)
+    {
+        TriggerConditionViewModelHarness condition;
+        condition.SetSourceValue(0x1234U);
+        condition.SetOperator(TriggerOperatorType::NotEquals);
+        condition.SetTargetValue(8);
+        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::AddSource);
+        Assert::AreEqual(TriggerOperatorType::None, condition.GetOperator());
+        Assert::AreEqual(std::string("A:0xH1234"), condition.Serialize());
+        Assert::IsTrue(condition.IsModifying());
+
+        // changing operator "restores" the 8
+        condition.SetOperator(TriggerOperatorType::Multiply);
+        Assert::AreEqual(std::string("A: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());
+
+        // change to modifying switches operator to None. the 8 is unmodified, but ignored
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::AreEqual(TriggerOperatorType::None, condition.GetOperator());
+        Assert::AreEqual(std::string("I:0xH1234"), condition.Serialize());
+        Assert::IsTrue(condition.IsModifying());
+
+        // change to non-modifying switches operator to Equals
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::AreEqual(TriggerOperatorType::Equals, condition.GetOperator());
+        Assert::AreEqual(std::string("0xH1234=8"), condition.Serialize());
+        Assert::IsFalse(condition.IsModifying());
+    }
+
+    TEST_METHOD(TestIsComparisonVisible)
+    {
+        TriggerConditionViewModelHarness condition;
+
+        condition.SetType(TriggerConditionType::Standard);
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Equals)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::NotEquals)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThan)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThanOrEqual)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThan)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThanOrEqual)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::None)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Multiply)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
+
+        condition.SetType(TriggerConditionType::AddAddress);
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Equals)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::NotEquals)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThan)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::LessThanOrEqual)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThan)));
+        Assert::IsFalse(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::GreaterThanOrEqual)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::None)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Multiply)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::Divide)));
+        Assert::IsTrue(TriggerConditionViewModel::IsComparisonVisible(condition, ra::etoi(TriggerOperatorType::BitwiseAnd)));
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 93f4a9ae..14f48d66 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -97,31 +97,33 @@ public:
         Assert::AreEqual({ 0U }, vmTrigger.Groups().Count());
         Assert::AreEqual(std::string(), vmTrigger.Serialize());
 
-        Assert::AreEqual({ 12U }, vmTrigger.ConditionTypes().Count());
+        Assert::AreEqual({ 13U }, 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());
         Assert::AreEqual(std::wstring(L"Pause If"), vmTrigger.ConditionTypes().GetItemAt(1)->GetLabel());
         Assert::AreEqual((int)TriggerConditionType::ResetIf, vmTrigger.ConditionTypes().GetItemAt(2)->GetId());
         Assert::AreEqual(std::wstring(L"Reset If"), vmTrigger.ConditionTypes().GetItemAt(2)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::AddSource, vmTrigger.ConditionTypes().GetItemAt(3)->GetId());
-        Assert::AreEqual(std::wstring(L"Add Source"), vmTrigger.ConditionTypes().GetItemAt(3)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::SubSource, vmTrigger.ConditionTypes().GetItemAt(4)->GetId());
-        Assert::AreEqual(std::wstring(L"Sub Source"), vmTrigger.ConditionTypes().GetItemAt(4)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::AddHits, vmTrigger.ConditionTypes().GetItemAt(5)->GetId());
-        Assert::AreEqual(std::wstring(L"Add Hits"), vmTrigger.ConditionTypes().GetItemAt(5)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::AddAddress, vmTrigger.ConditionTypes().GetItemAt(6)->GetId());
-        Assert::AreEqual(std::wstring(L"Add Address"), vmTrigger.ConditionTypes().GetItemAt(6)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::AndNext, vmTrigger.ConditionTypes().GetItemAt(7)->GetId());
-        Assert::AreEqual(std::wstring(L"And Next"), vmTrigger.ConditionTypes().GetItemAt(7)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::OrNext, vmTrigger.ConditionTypes().GetItemAt(8)->GetId());
-        Assert::AreEqual(std::wstring(L"Or Next"), vmTrigger.ConditionTypes().GetItemAt(8)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::Measured, vmTrigger.ConditionTypes().GetItemAt(9)->GetId());
-        Assert::AreEqual(std::wstring(L"Measured"), vmTrigger.ConditionTypes().GetItemAt(9)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::MeasuredIf, vmTrigger.ConditionTypes().GetItemAt(10)->GetId());
-        Assert::AreEqual(std::wstring(L"Measured If"), vmTrigger.ConditionTypes().GetItemAt(10)->GetLabel());
-        Assert::AreEqual((int)TriggerConditionType::Trigger, vmTrigger.ConditionTypes().GetItemAt(11)->GetId());
-        Assert::AreEqual(std::wstring(L"Trigger"), vmTrigger.ConditionTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::ResetNextIf, vmTrigger.ConditionTypes().GetItemAt(3)->GetId());
+        Assert::AreEqual(std::wstring(L"Reset Next If"), vmTrigger.ConditionTypes().GetItemAt(3)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddSource, vmTrigger.ConditionTypes().GetItemAt(4)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Source"), vmTrigger.ConditionTypes().GetItemAt(4)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::SubSource, vmTrigger.ConditionTypes().GetItemAt(5)->GetId());
+        Assert::AreEqual(std::wstring(L"Sub Source"), vmTrigger.ConditionTypes().GetItemAt(5)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddHits, vmTrigger.ConditionTypes().GetItemAt(6)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Hits"), vmTrigger.ConditionTypes().GetItemAt(6)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AddAddress, vmTrigger.ConditionTypes().GetItemAt(7)->GetId());
+        Assert::AreEqual(std::wstring(L"Add Address"), vmTrigger.ConditionTypes().GetItemAt(7)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::AndNext, vmTrigger.ConditionTypes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"And Next"), vmTrigger.ConditionTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::OrNext, vmTrigger.ConditionTypes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"Or Next"), vmTrigger.ConditionTypes().GetItemAt(9)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::Measured, vmTrigger.ConditionTypes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"Measured"), vmTrigger.ConditionTypes().GetItemAt(10)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::MeasuredIf, vmTrigger.ConditionTypes().GetItemAt(11)->GetId());
+        Assert::AreEqual(std::wstring(L"Measured If"), vmTrigger.ConditionTypes().GetItemAt(11)->GetLabel());
+        Assert::AreEqual((int)TriggerConditionType::Trigger, vmTrigger.ConditionTypes().GetItemAt(12)->GetId());
+        Assert::AreEqual(std::wstring(L"Trigger"), vmTrigger.ConditionTypes().GetItemAt(12)->GetLabel());
 
         Assert::AreEqual({ 5U }, vmTrigger.OperandTypes().Count());
         Assert::AreEqual((int)TriggerOperandType::Address, vmTrigger.OperandTypes().GetItemAt(0)->GetId());