Repository: RetroAchievements/RAIntegration
Pull request #697 (2021-04-06 00:10:49)

move hardcore warning logic into EmulatorContext; add unit tests


Patch:
diff --git a/src/Exports.cpp b/src/Exports.cpp
index 3da79586..1b5f6758 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -57,31 +57,12 @@ API int CCONV _RA_HardcoreModeIsActive()
 
 API bool CCONV _RA_WarnDisableHardcore(const char* sActivity)
 {
-    // already disabled, just return success
-    auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
-    if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
-        return true;
-
-    // if no activity specified, just proceed to disabling without prompting
-    if (sActivity && *sActivity)
-    {
-        // prompt. if user doesn't consent, return failure - caller should not continue
-        ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
-        vmMessageBox.SetHeader(L"Disable Hardcore mode?");
-        vmMessageBox.SetMessage(L"You cannot " + ra::Widen(sActivity) + L" while Hardcore mode is active.");
-        vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
-        vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
-        if (vmMessageBox.ShowModal() != ra::ui::DialogResult::Yes)
-            return false;
-    }
+    std::string sActivityString;
+    if (sActivity)
+        sActivityString = sActivity;
 
-    RA_LOG_INFO("User chose to disable hardcore to %s", sActivity);
-
-    // user consented, switch to non-hardcore mode
-    ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>().DisableHardcoreMode();
-
-    // return success
-    return true;
+    auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+    return pEmulatorContext.WarnDisableHardcoreMode(sActivityString);
 }
 
 #ifndef RA_UTEST
diff --git a/src/data/context/EmulatorContext.cpp b/src/data/context/EmulatorContext.cpp
index e0d32cc0..b42e0d7d 100644
--- a/src/data/context/EmulatorContext.cpp
+++ b/src/data/context/EmulatorContext.cpp
@@ -363,6 +363,39 @@ bool EmulatorContext::ValidateClientVersion(bool& bHardcore)
     return bResult;
 }
 
+bool EmulatorContext::WarnDisableHardcoreMode(const std::string& sActivity)
+{
+    // already disabled, just return success
+    auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
+    if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
+        return true;
+
+    if (!sActivity.empty())
+    {
+        // prompt. if user doesn't consent, return failure - caller should not continue
+        ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
+        vmMessageBox.SetHeader(L"Disable Hardcore mode?");
+        vmMessageBox.SetMessage(ra::StringPrintf(L"You cannot %s while Hardcore mode is active.", sActivity));
+        vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
+        vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
+        if (vmMessageBox.ShowModal() != ra::ui::DialogResult::Yes)
+            return false;
+
+        RA_LOG_INFO("User chose to disable hardcore to %s", sActivity);
+    }
+    else
+    {
+        // no activity specified. just proceed to disabling without prompting.
+        RA_LOG_INFO("Disabling hardcore. No activity provided to WarnDisableHardcore");
+    }
+
+    // user consented, switch to non-hardcore mode
+    DisableHardcoreMode();
+
+    // return success
+    return true;
+}
+
 void EmulatorContext::DisableHardcoreMode()
 {
     auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
diff --git a/src/data/context/EmulatorContext.hh b/src/data/context/EmulatorContext.hh
index a139056f..617e35d3 100644
--- a/src/data/context/EmulatorContext.hh
+++ b/src/data/context/EmulatorContext.hh
@@ -66,6 +66,21 @@ public:
     /// </summary>
     void SetClientUserAgentDetail(const std::string& sDetail) { m_sClientUserAgentDetail = sDetail; UpdateUserAgent(); }
 
+
+    /// <summary>
+    /// Prompts the user to confirm disabling hardcore mode to perform some activity.
+    /// </summary>
+    /// <param name="sActivity">The activity that is not allowed in hardcore mode.</param>
+    /// <returns>
+    /// <c>false</c> if the user does not want to switch out of hardcore mode - the calling code should not allow
+    /// the activity to occur. <c>true</c> if hardcore mode was not enabled, or the user acknowledged the switch to
+    /// non-hardcore mode, in which case hardcore mode will have been disabled before returning.
+    /// </returns>
+    /// <remarks>
+    /// If <paramref="sActivity" /> is empty, just calls <see cref="DisableHardcoreMode" /> without prompting.
+    /// </remarks>
+    virtual bool WarnDisableHardcoreMode(const std::string& sActivity);
+
     /// <summary>
     /// Disables hardcore mode and notifies other services of that fact.
     /// </summary>
diff --git a/src/data/context/GameAssets.cpp b/src/data/context/GameAssets.cpp
index 344bcb5b..31aae6d3 100644
--- a/src/data/context/GameAssets.cpp
+++ b/src/data/context/GameAssets.cpp
@@ -113,7 +113,18 @@ void GameAssets::ReloadAssets(const std::vector<ra::data::models::AssetModelBase
     auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
     auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::UserAchievements, std::to_wstring(pGameContext.GameId()));
     if (pData == nullptr)
+    {
+        // no local file found. reset non-local items to their server state
+        BeginUpdate();
+        for (auto* pAsset : vAssetsToReload)
+        {
+            if (pAsset->GetCategory() != ra::data::models::AssetCategory::Local)
+                pAsset->RestoreServerCheckpoint();
+        }
+        EndUpdate();
+
         return;
+    }
 
     std::vector<ra::data::models::AssetModelBase*> vRemainingAssetsToReload(vAssetsToReload);
     const bool bReloadAll = vAssetsToReload.empty();
diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp
index cf41afee..e7bb25d7 100644
--- a/src/data/context/GameContext.cpp
+++ b/src/data/context/GameContext.cpp
@@ -318,7 +318,13 @@ void GameContext::UpdateUnlocks(const std::set<unsigned int>& vUnlockedAchieveme
     }
 
     if (bUnpause)
+    {
         ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>().SetPaused(false);
+#ifndef RA_UTEST
+        auto& pAssetList = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetList;
+        pAssetList.SetProcessingActive(true);
+#endif
+    }
 
     if (nPopup)
     {
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 0675a5d5..0de90632 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -445,7 +445,8 @@ gsl::index AssetListViewModel::GetFilteredAssetIndex(const ra::data::models::Ass
 
 void AssetListViewModel::OpenEditor(const AssetSummaryViewModel* pAsset)
 {
-    if (!_RA_WarnDisableHardcore("edit assets"))
+    auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+    if (!pEmulatorContext.WarnDisableHardcoreMode("edit assets"))
         return;
 
     ra::data::models::AssetModelBase* vmAsset = nullptr;
@@ -784,6 +785,23 @@ void AssetListViewModel::ActivateSelected()
 
     if (GetActivateButtonText().at(0) == 'D') // Deactivate
     {
+        bool bCoreAssetSelected = false;
+        for (const auto* vmItem : vSelectedAssets)
+        {
+            if (vmItem && vmItem->GetCategory() == ra::data::models::AssetCategory::Core)
+            {
+                bCoreAssetSelected = true;
+                break;
+            }
+        }
+
+        if (bCoreAssetSelected)
+        {
+            auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+            if (!pEmulatorContext.WarnDisableHardcoreMode("deactivate core achievements"))
+                return;
+        }
+
         for (auto* vmItem : vSelectedAssets)
         {
             if (vmItem)
@@ -1018,12 +1036,24 @@ void AssetListViewModel::ResetSelected()
     if (!CanReset())
         return;
 
+    bool bCoreAssetSelected = false;
     std::vector<const AssetSummaryViewModel*> vSelectedAssets;
     for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(m_vFilteredAssets.Count()); ++nIndex)
     {
         const auto* pItem = m_vFilteredAssets.GetItemAt(nIndex);
         if (pItem != nullptr && pItem->IsSelected())
+        {
             vSelectedAssets.push_back(pItem);
+
+            bCoreAssetSelected |= (pItem->GetCategory() == ra::data::models::AssetCategory::Core);
+        }
+    }
+
+    if (bCoreAssetSelected)
+    {
+        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+        if (!pEmulatorContext.WarnDisableHardcoreMode("reset core achievements"))
+            return;
     }
 
     ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
@@ -1093,16 +1123,32 @@ void AssetListViewModel::RevertSelected()
     std::vector<ra::data::models::AssetModelBase*> vAssetsToReset;
     GetSelectedAssets(vAssetsToReset);
 
-    bool bHasLocal = false;
+    bool bLocalAssetSelected = false;
+    bool bCoreAssetSelected = false;
     for (const auto* pAsset : vAssetsToReset)
     {
-        if (pAsset != nullptr && pAsset->GetCategory() == ra::data::models::AssetCategory::Local)
+        if (pAsset == nullptr)
+            continue;
+
+        switch (pAsset->GetCategory())
         {
-            bHasLocal = true;
-            break;
+            case ra::data::models::AssetCategory::Local:
+                bLocalAssetSelected = true;
+                break;
+
+            case ra::data::models::AssetCategory::Core:
+                bCoreAssetSelected = true;
+                break;
         }
     }
 
+    if (bCoreAssetSelected)
+    {
+        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+        if (!pEmulatorContext.WarnDisableHardcoreMode("revert core achievements"))
+            return;
+    }
+
     ra::ui::viewmodels::MessageBoxViewModel vmMessageBox;
     vmMessageBox.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning);
     vmMessageBox.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo);
@@ -1118,17 +1164,17 @@ void AssetListViewModel::RevertSelected()
         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)
+        if (bLocalAssetSelected)
             sWarningMessage += L"\n\n";
     }
     else // Delete
     {
         vmMessageBox.SetHeader(L"Permanently delete?");
 
-        Expects(bHasLocal); // message is shared with text appended to Revert message
+        Expects(bLocalAssetSelected); // message is shared with text appended to Revert message
     }
 
-    if (bHasLocal)
+    if (bLocalAssetSelected)
         sWarningMessage += L"Assets not available on the server will be permanently deleted.";
     vmMessageBox.SetMessage(sWarningMessage);
 


Test patch:
diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp
index 18500266..a99e3887 100644
--- a/tests/Exports_Tests.cpp
+++ b/tests/Exports_Tests.cpp
@@ -90,69 +90,6 @@ public:
         Assert::AreEqual(0, _RA_HardcoreModeIsActive());
     }
 
-private:
-    class WarnDisableHardcoreHarness
-    {
-    public:
-        MockConfiguration mockConfiguration;
-        MockDesktop mockDesktop;
-        MockEmulatorContext mockEmulatorContext;
-    };
-
-    TEST_METHOD(TestWarnDisableHardcoreNotEnabled)
-    {
-        WarnDisableHardcoreHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
-
-        Assert::IsTrue(_RA_WarnDisableHardcore("test"));
-        Assert::IsFalse(harness.mockDesktop.WasDialogShown());
-        Assert::IsFalse(harness.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
-    }
-
-    TEST_METHOD(TestWarnDisableHardcoreAborted)
-    {
-        WarnDisableHardcoreHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-
-        bool bWasShown = false;
-        harness.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWasShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
-        {
-            Assert::AreEqual(std::wstring(L"Disable Hardcore mode?"), vmMessageBox.GetHeader());
-            Assert::AreEqual(std::wstring(L"You cannot test while Hardcore mode is active."), vmMessageBox.GetMessage());
-            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
-            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning, vmMessageBox.GetIcon());
-            bWasShown = true;
-
-            return ra::ui::DialogResult::No;
-        });
-
-        Assert::IsFalse(_RA_WarnDisableHardcore("test"));
-        Assert::IsTrue(bWasShown);
-        Assert::IsTrue(harness.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
-    }
-
-    TEST_METHOD(TestWarnDisableHardcoreAllowed)
-    {
-        WarnDisableHardcoreHarness harness;
-        harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-
-        bool bWasShown = false;
-        harness.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWasShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
-        {
-            Assert::AreEqual(std::wstring(L"Disable Hardcore mode?"), vmMessageBox.GetHeader());
-            Assert::AreEqual(std::wstring(L"You cannot test while Hardcore mode is active."), vmMessageBox.GetMessage());
-            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
-            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning, vmMessageBox.GetIcon());
-            bWasShown = true;
-
-            return ra::ui::DialogResult::Yes;
-        });
-
-        Assert::IsTrue(_RA_WarnDisableHardcore("test"));
-        Assert::IsTrue(bWasShown);
-        Assert::IsFalse(harness.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
-    }
-
 private:
     class AttemptLoginHarness
     {
diff --git a/tests/data/context/EmulatorContext_Tests.cpp b/tests/data/context/EmulatorContext_Tests.cpp
index 784205f8..2472274a 100644
--- a/tests/data/context/EmulatorContext_Tests.cpp
+++ b/tests/data/context/EmulatorContext_Tests.cpp
@@ -532,6 +532,70 @@ public:
         Assert::AreEqual(std::string("RALibRetro/1.0.15 (UnitTests) Integration/VERSION fceumm_libretro/(SVN)_0a0fdb8"), sVersion);
     }
 
+    TEST_METHOD(TestWarnDisableHardcoreNotEnabled)
+    {
+        EmulatorContextHarness emulator;
+        emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
+
+        Assert::IsTrue(emulator.WarnDisableHardcoreMode("test"));
+        Assert::IsFalse(emulator.mockDesktop.WasDialogShown());
+        Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
+    TEST_METHOD(TestWarnDisableHardcoreAborted)
+    {
+        EmulatorContextHarness emulator;
+        emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+
+        bool bWasShown = false;
+        emulator.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWasShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Disable Hardcore mode?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"You cannot test while Hardcore mode is active."), vmMessageBox.GetMessage());
+            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
+            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning, vmMessageBox.GetIcon());
+            bWasShown = true;
+
+            return ra::ui::DialogResult::No;
+        });
+
+        Assert::IsFalse(emulator.WarnDisableHardcoreMode("test"));
+        Assert::IsTrue(bWasShown);
+        Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
+    TEST_METHOD(TestWarnDisableHardcoreAllowed)
+    {
+        EmulatorContextHarness emulator;
+        emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+
+        bool bWasShown = false;
+        emulator.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWasShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Disable Hardcore mode?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"You cannot test while Hardcore mode is active."), vmMessageBox.GetMessage());
+            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
+            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning, vmMessageBox.GetIcon());
+            bWasShown = true;
+
+            return ra::ui::DialogResult::Yes;
+        });
+
+        Assert::IsTrue(emulator.WarnDisableHardcoreMode("test"));
+        Assert::IsTrue(bWasShown);
+        Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
+    TEST_METHOD(TestWarnDisableHardcoreNoActivity)
+    {
+        EmulatorContextHarness emulator;
+        emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+
+        Assert::IsTrue(emulator.WarnDisableHardcoreMode(""));
+        Assert::IsFalse(emulator.mockDesktop.WasDialogShown());
+        Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
     TEST_METHOD(TestDisableHardcoreMode)
     {
         EmulatorContextHarness emulator;
diff --git a/tests/mocks/MockEmulatorContext.hh b/tests/mocks/MockEmulatorContext.hh
index 8503f2d1..3d5d2a60 100644
--- a/tests/mocks/MockEmulatorContext.hh
+++ b/tests/mocks/MockEmulatorContext.hh
@@ -33,6 +33,31 @@ public:
         SetGetGameTitleFunction([sTitle](char* sBuffer) noexcept { strcpy_s(sBuffer, 64, sTitle); });
     }
 
+    void MockDisableHardcoreWarning(ra::ui::DialogResult nPromptResult) noexcept
+    {
+        m_nWarnDisableHardcoreResult = nPromptResult;
+        m_sWarnDisableHardcoreActivity.clear();
+    }
+
+    const std::string& GetDisableHardcoreWarningMessage() const noexcept
+    {
+        return m_sWarnDisableHardcoreActivity;
+    }
+
+    bool WarnDisableHardcoreMode(const std::string& sActivity) override
+    {
+        auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
+        if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore))
+            return true;
+
+        m_sWarnDisableHardcoreActivity = sActivity;
+        if (m_nWarnDisableHardcoreResult != ra::ui::DialogResult::Yes)
+            return false;
+
+        pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
+        return true;
+    }
+
     void DisableHardcoreMode() override
     {
         auto& pConfiguration = ra::services::ServiceLocator::GetMutable<ra::services::IConfiguration>();
@@ -87,6 +112,9 @@ private:
         s_pMemory.at(nAddress) = nValue;
     }
 
+    std::string m_sWarnDisableHardcoreActivity;
+    ra::ui::DialogResult m_nWarnDisableHardcoreResult = ra::ui::DialogResult::No;
+
     ra::services::ServiceLocator::ServiceOverride<ra::data::context::EmulatorContext> m_Override;
     static gsl::span<uint8_t> s_pMemory;
 };
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index 158fe3cd..a6f4a83d 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -1550,6 +1550,67 @@ public:
         Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
     }
 
+    TEST_METHOD(TestActivateSelectedHardcoreDeactivateCore)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.SetGameId(1U);
+        vmAssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Core);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.mockGameContext.Assets().GetItemAt(2)->SetState(AssetState::Active);
+
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        vmAssetList.FilteredAssets().GetItemAt(1)->SetSelected(true);
+        Assert::IsFalse(vmAssetList.FilteredAssets().GetItemAt(0)->IsSelected());
+        Assert::IsTrue(vmAssetList.FilteredAssets().GetItemAt(1)->IsSelected());
+        Assert::AreEqual(AssetState::Active, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
+        vmAssetList.ForceUpdateButtons();
+
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::No);
+        vmAssetList.ActivateSelected();
+        vmAssetList.ForceUpdateButtons();
+        Assert::AreEqual(std::string("deactivate core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        Assert::AreEqual(AssetState::Active, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
+        Assert::AreEqual(std::wstring(L"De&activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
+        Assert::IsTrue(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::Yes);
+        vmAssetList.ActivateSelected();
+        vmAssetList.ForceUpdateButtons();
+        Assert::AreEqual(std::string("deactivate core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(1)->GetState());
+        Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
+        Assert::IsFalse(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
+    TEST_METHOD(TestActivateSelectedHardcoreDeactivateLocal)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.AddAchievement(AssetCategory::Local, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetGameId(1U);
+        vmAssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Local);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.mockGameContext.Assets().GetItemAt(0)->SetState(AssetState::Active);
+
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        Assert::IsTrue(vmAssetList.FilteredAssets().GetItemAt(0)->IsSelected());
+        Assert::AreEqual(AssetState::Active, vmAssetList.FilteredAssets().GetItemAt(0)->GetState());
+        vmAssetList.ForceUpdateButtons();
+
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::No);
+        vmAssetList.ActivateSelected();
+        vmAssetList.ForceUpdateButtons();
+        Assert::AreEqual(std::string(), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        Assert::AreEqual(AssetState::Inactive, vmAssetList.FilteredAssets().GetItemAt(0)->GetState());
+        Assert::AreEqual(std::wstring(L"&Activate"), vmAssetList.GetActivateButtonText());
+        Assert::IsTrue(vmAssetList.CanActivate());
+        Assert::IsTrue(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
     TEST_METHOD(TestSaveSelectedLocalUnmodified)
     {
         AssetListViewModelHarness vmAssetList;
@@ -2636,6 +2697,85 @@ public:
         Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
     }
 
+    TEST_METHOD(TestResetSelectedModifiedCoreHardcore)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetTrigger("0x2345=1");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+        Assert::AreEqual(AssetCategory::Core, pAsset->GetCategory());
+
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(ResetButtonState::Reset);
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::Yes);
+        vmAssetList.ResetSelected();
+        Assert::AreEqual(std::string("reset core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestResetSelectedModifiedCoreHardcoreCancel)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetTrigger("0x2345=1");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+        Assert::AreEqual(AssetCategory::Core, pAsset->GetCategory());
+
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(ResetButtonState::Reset);
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::No);
+        vmAssetList.ResetSelected();
+        Assert::AreEqual(std::string("reset core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestResetSelectedModifiedFromFileHardcore)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Local, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Local);
+        vmAssetList.FilteredAssets().GetItemAt(0)->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(ResetButtonState::Reset);
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetTrigger("0x2345=1");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+
+        vmAssetList.MockUserFileContents("1:\"0xH1234=0\":Test:::::User:0:0:0:::00000\n");
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.ResetSelected();
+        Assert::AreEqual(std::string(), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+
+        Assert::AreEqual(std::string("0xH1234=0"), pAsset->GetTrigger());
+        Assert::AreEqual(AssetCategory::Local, pAsset->GetCategory());
+        Assert::AreEqual(AssetChanges::Unpublished, pAsset->GetChanges());
+    }
+
     TEST_METHOD(TestResetSelectedNew)
     {
         AssetListViewModelHarness vmAssetList;
@@ -2883,6 +3023,77 @@ public:
         Assert::IsNull(pAsset);
     }
 
+    TEST_METHOD(TestRevertSelectedCoreHardcore)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::Yes);
+        vmAssetList.RevertSelected();
+
+        Assert::AreEqual(std::string("revert core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        Assert::AreEqual(AssetChanges::None, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestRevertSelectedCoreHardcoreCancel)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddThreeAchievements();
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::No);
+        vmAssetList.RevertSelected();
+
+        Assert::AreEqual(std::string("revert core achievements"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+    }
+
+    TEST_METHOD(TestRevertSelectedLocalHardcore)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Local, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Local);
+        vmAssetList.ForceUpdateButtons();
+        vmAssetList.AssertButtonState(RevertButtonState::RevertAll);
+
+        auto* pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNotNull(pAsset);
+        Ensures(pAsset != nullptr);
+        pAsset->SetName(L"Apple");
+        Assert::AreEqual(AssetChanges::Modified, pAsset->GetChanges());
+
+        vmAssetList.mockDesktop.ExpectWindow<MessageBoxViewModel>([](MessageBoxViewModel&) { return DialogResult::Yes; });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::Yes);
+        vmAssetList.RevertSelected();
+
+        Assert::AreEqual(std::string(), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
+        pAsset = vmAssetList.mockGameContext.Assets().FindAchievement({ 1U });
+        Assert::IsNull(pAsset);
+    }
+
     TEST_METHOD(TestIsProcessingActive)
     {
         AssetListViewModelHarness vmAssetList;
@@ -2901,7 +3112,7 @@ public:
         Assert::IsTrue(vmAssetList.mockEmulatorContext.WasMemoryModified());
     }
 
-    TEST_METHOD(TestOpenEditor)
+    TEST_METHOD(TestOpenEditorNonHardcore)
     {
         AssetListViewModelHarness vmAssetList;
         vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
@@ -2923,16 +3134,14 @@ public:
         vmAssetList.OpenEditor(pItem);
         Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
         Assert::AreEqual(ra::to_unsigned(pItem->GetId()), vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::AreEqual(std::string(), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
     }
 
     TEST_METHOD(TestOpenEditorHardcore)
     {
         AssetListViewModelHarness vmAssetList;
         vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        vmAssetList.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel&)
-        {
-            return ra::ui::DialogResult::Yes;
-        });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::Yes);
 
         vmAssetList.SetGameId(22U);
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
@@ -2951,6 +3160,7 @@ public:
         vmAssetList.OpenEditor(pItem);
         Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
         Assert::AreEqual(ra::to_unsigned(pItem->GetId()), vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::AreEqual(std::string("edit assets"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
         Assert::IsFalse(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
     }
 
@@ -2958,10 +3168,7 @@ public:
     {
         AssetListViewModelHarness vmAssetList;
         vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
-        vmAssetList.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel&)
-        {
-            return ra::ui::DialogResult::No;
-        });
+        vmAssetList.mockEmulatorContext.MockDisableHardcoreWarning(ra::ui::DialogResult::No);
 
         vmAssetList.SetGameId(22U);
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
@@ -2980,6 +3187,7 @@ public:
         vmAssetList.OpenEditor(pItem);
         Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
         Assert::AreEqual(0U, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::AreEqual(std::string("edit assets"), vmAssetList.mockEmulatorContext.GetDisableHardcoreWarningMessage());
         Assert::IsTrue(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
     }
 };