Repository: RetroAchievements/RAIntegration
Pull request #682 (2021-02-20 15:44:00)

update to latest RAInterface


Patch:
diff --git a/RAInterface b/RAInterface
index 4fafbc3f..058969cb 160000
--- a/RAInterface
+++ b/RAInterface
@@ -1,1 +1,1 @@
-Subproject commit 4fafbc3f3e55e2cae90e3f38d441fb47556e729c
+Subproject commit 058969cbbbc87ed32fb15062039d4aa73decb9d3
diff --git a/src/Exports.cpp b/src/Exports.cpp
index 2b31eb70..3da79586 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -55,6 +55,35 @@ API int CCONV _RA_HardcoreModeIsActive()
     return pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
 }
 
+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;
+    }
+
+    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;
+}
+
 #ifndef RA_UTEST
 API void CCONV _RA_UpdateHWnd(HWND hMainHWND)
 {
diff --git a/src/RA_Core.cpp b/src/RA_Core.cpp
index 3318e01e..29f402ae 100644
--- a/src/RA_Core.cpp
+++ b/src/RA_Core.cpp
@@ -215,35 +215,6 @@ API bool CCONV _RA_ConfirmLoadNewRom(bool bQuittingApp)
     return (vmMessageBox.ShowModal() == ra::ui::DialogResult::Yes);
 }
 
-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;
-    }
-
-    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;
-}
-
 API void CCONV _RA_OnReset()
 {
     // if there's no game loaded, there shouldn't be any active achievements or popups to clear - except maybe the
@@ -400,15 +371,18 @@ API void CCONV _RA_InvokeDialog(LPARAM nID)
             break;
 
         case IDM_RA_FILES_ACHIEVEMENTEDITOR:
-            ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetEditor.Show();
+            if (_RA_WarnDisableHardcore("edit assets"))
+                ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().AssetEditor.Show();
             break;
 
         case IDM_RA_FILES_MEMORYFINDER:
-            ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryInspector.Show();
+            if (_RA_WarnDisableHardcore("inspect memory"))
+                ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryInspector.Show();
             break;
 
         case IDM_RA_FILES_MEMORYBOOKMARKS:
-            ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryBookmarks.Show();
+            if (_RA_WarnDisableHardcore("view memory bookmarks"))
+                ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryBookmarks.Show();
             break;
 
         case IDM_RA_FILES_LOGIN:
diff --git a/src/data/context/EmulatorContext.cpp b/src/data/context/EmulatorContext.cpp
index ccb60bdf..446028f0 100644
--- a/src/data/context/EmulatorContext.cpp
+++ b/src/data/context/EmulatorContext.cpp
@@ -370,6 +370,10 @@ void EmulatorContext::DisableHardcoreMode()
     {
         pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
 
+        // updating the enabled-ness of the buttons of the asset list
+        auto& pWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
+        pWindowManager.AssetList.UpdateButtons();
+
         RebuildMenu();
 
         auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
@@ -420,6 +424,26 @@ bool EmulatorContext::EnableHardcoreMode(bool bShowWarning)
     // User has agreed to reset the emulator, or no game is loaded. Enabled hardcore!
     pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
 
+    // close any windows not permitted in hardcore
+    auto& pWindowManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>();
+    auto& pDesktop = ra::services::ServiceLocator::Get<ra::ui::IDesktop>();
+
+    if (pWindowManager.MemoryInspector.IsVisible())
+        pDesktop.CloseWindow(pWindowManager.MemoryInspector);
+
+    if (pWindowManager.MemoryBookmarks.IsVisible())
+        pDesktop.CloseWindow(pWindowManager.MemoryBookmarks);
+
+    if (pWindowManager.AssetEditor.IsVisible())
+        pDesktop.CloseWindow(pWindowManager.AssetEditor);
+
+    if (pWindowManager.CodeNotes.IsVisible())
+        pDesktop.CloseWindow(pWindowManager.CodeNotes);
+
+    // updating the enabled-ness of the buttons of the asset list
+    pWindowManager.AssetList.UpdateButtons();
+
+    // update the integration menu
     RebuildMenu();
 
     // when enabling hardcore mode, force a system reset
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 1c8710b7..27b1d5f2 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -369,6 +369,9 @@ gsl::index AssetListViewModel::GetFilteredAssetIndex(const ra::data::models::Ass
 
 void AssetListViewModel::OpenEditor(const AssetSummaryViewModel* pAsset)
 {
+    if (!_RA_WarnDisableHardcore("edit assets"))
+        return;
+
     ra::data::models::AssetModelBase* vmAsset = nullptr;
 
     if (pAsset)
@@ -451,6 +454,7 @@ void AssetListViewModel::DoUpdateButtons()
     bool bHasModified = false;
     bool bHasUnpublished = false;
     bool bHasUnofficial = false;
+    const bool bHardcore = _RA_HardcoreModeIsActive();
 
     const bool bGameLoaded = (GetGameId() != 0);
     if (!bGameLoaded)
@@ -461,7 +465,7 @@ void AssetListViewModel::DoUpdateButtons()
     }
     else
     {
-        SetValue(CanCreateProperty, true);
+        SetValue(CanCreateProperty, !bHardcore);
         SetValue(CanActivateProperty, m_vFilteredAssets.Count() > 0);
 
         for (size_t i = 0; i < m_vFilteredAssets.Count(); ++i)
@@ -544,12 +548,12 @@ void AssetListViewModel::DoUpdateButtons()
     if (bHasInactiveSelection)
     {
         SetValue(ActivateButtonTextProperty, L"&Activate");
-        SetValue(CanCloneProperty, true);
+        SetValue(CanCloneProperty, !bHardcore);
     }
     else if (bHasActiveSelection)
     {
         SetValue(ActivateButtonTextProperty, L"De&activate");
-        SetValue(CanCloneProperty, true);
+        SetValue(CanCloneProperty, !bHardcore);
     }
     else
     {
@@ -1048,6 +1052,9 @@ void AssetListViewModel::RevertSelected()
 
 void AssetListViewModel::CreateNew()
 {
+    if (!CanCreate())
+        return;
+
     auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
     const auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
 
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index be8d6ae4..99099e3e 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -175,6 +175,7 @@ public:
     const ViewModelCollection<AssetSummaryViewModel>& FilteredAssets() const noexcept { return m_vFilteredAssets; }
 
     void OpenEditor(const AssetSummaryViewModel* pAsset);
+    void UpdateButtons();
 
     /// <summary>
     /// The <see cref="ModelProperty" /> for the index of the asset that should be made visible.
@@ -220,7 +221,6 @@ private:
         std::function<bool(const ra::data::models::AssetModelBase&)> pFilterFunction = nullptr);
 
     void UpdateTotals();
-    void UpdateButtons();
     void DoUpdateButtons();
     std::atomic_bool m_bNeedToUpdateButtons = false;
 


Test patch:
diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp
index a99e3887..18500266 100644
--- a/tests/Exports_Tests.cpp
+++ b/tests/Exports_Tests.cpp
@@ -90,6 +90,69 @@ 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/RA_Interface_Tests.cpp b/tests/RA_Interface_Tests.cpp
index 74c3265a..f0b092cb 100644
--- a/tests/RA_Interface_Tests.cpp
+++ b/tests/RA_Interface_Tests.cpp
@@ -74,6 +74,7 @@ public:
         Assert::IsNotNull((const void*)_RA_InitClient);
         Assert::IsNotNull((const void*)_RA_InitClientOffline);
         Assert::IsNotNull((const void*)_RA_InstallSharedFunctions);
+        Assert::IsNotNull((const void*)_RA_SetForceRepaint);
         Assert::IsNotNull((const void*)_RA_CreatePopupMenu);
         Assert::IsNotNull((const void*)_RA_InvokeDialog);
         Assert::IsNotNull((const void*)_RA_SetUserAgentDetail);
@@ -92,6 +93,8 @@ public:
         Assert::IsNotNull((const void*)_RA_OnLoadNewRom);
         Assert::IsNotNull((const void*)_RA_ConfirmLoadNewRom);
         Assert::IsNotNull((const void*)_RA_DoAchievementsFrame);
+        Assert::IsNotNull((const void*)_RA_SuspendRepaint);
+        Assert::IsNotNull((const void*)_RA_ResumeRepaint);
         Assert::IsNotNull((const void*)_RA_UpdateAppTitle);
         Assert::IsNotNull((const void*)_RA_UserName);
         Assert::IsNotNull((const void*)_RA_HardcoreModeIsActive);
@@ -113,6 +116,7 @@ public:
         Assert::IsNull((const void*)_RA_InitClient);
         Assert::IsNull((const void*)_RA_InitClientOffline);
         Assert::IsNull((const void*)_RA_InstallSharedFunctions);
+        Assert::IsNull((const void*)_RA_SetForceRepaint);
         Assert::IsNull((const void*)_RA_CreatePopupMenu);
         Assert::IsNull((const void*)_RA_InvokeDialog);
         Assert::IsNull((const void*)_RA_SetUserAgentDetail);
@@ -131,6 +135,8 @@ public:
         Assert::IsNull((const void*)_RA_OnLoadNewRom);
         Assert::IsNull((const void*)_RA_ConfirmLoadNewRom);
         Assert::IsNull((const void*)_RA_DoAchievementsFrame);
+        Assert::IsNull((const void*)_RA_SuspendRepaint);
+        Assert::IsNull((const void*)_RA_ResumeRepaint);
         Assert::IsNull((const void*)_RA_UpdateAppTitle);
         Assert::IsNull((const void*)_RA_UserName);
         Assert::IsNull((const void*)_RA_HardcoreModeIsActive);
diff --git a/tests/data/context/EmulatorContext_Tests.cpp b/tests/data/context/EmulatorContext_Tests.cpp
index 89d4fe68..d3439ebf 100644
--- a/tests/data/context/EmulatorContext_Tests.cpp
+++ b/tests/data/context/EmulatorContext_Tests.cpp
@@ -11,6 +11,7 @@
 #include "tests\mocks\MockFileSystem.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockHttpRequester.hh"
+#include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockOverlayManager.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
@@ -823,6 +824,43 @@ public:
         Assert::IsFalse(bWasReset);
     }
 
+    TEST_METHOD(TestEnableHardcoreModeClosesEditors)
+    {
+        EmulatorContextHarness emulator;
+        emulator.MockVersions("0.58", "0.57");
+        emulator.mockGameContext.SetGameId(1U);
+        emulator.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel&)
+        {
+            return ra::ui::DialogResult::Yes;
+        });
+
+        ra::services::mocks::MockLocalStorage mockLocalStorage; // required by RichPresenceMonitor.Show()
+
+        emulator.mockWindowManager.RichPresenceMonitor.Show();
+        emulator.mockWindowManager.AssetList.Show();
+        emulator.mockWindowManager.AssetEditor.Show();
+        emulator.mockWindowManager.MemoryBookmarks.Show();
+        emulator.mockWindowManager.MemoryInspector.Show();
+        emulator.mockWindowManager.CodeNotes.Show();
+
+        Assert::IsTrue(emulator.mockWindowManager.RichPresenceMonitor.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.AssetList.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.AssetEditor.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.MemoryBookmarks.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.MemoryInspector.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.CodeNotes.IsVisible());
+
+        Assert::IsTrue(emulator.EnableHardcoreMode());
+
+        Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+        Assert::IsTrue(emulator.mockWindowManager.RichPresenceMonitor.IsVisible());
+        Assert::IsTrue(emulator.mockWindowManager.AssetList.IsVisible());
+        Assert::IsFalse(emulator.mockWindowManager.AssetEditor.IsVisible());
+        Assert::IsFalse(emulator.mockWindowManager.MemoryBookmarks.IsVisible());
+        Assert::IsFalse(emulator.mockWindowManager.MemoryInspector.IsVisible());
+        Assert::IsFalse(emulator.mockWindowManager.CodeNotes.IsVisible());
+    }
+
     TEST_METHOD(TestGetAppTitleDefault)
     {
         EmulatorContextHarness emulator;
diff --git a/tests/mocks/MockDesktop.hh b/tests/mocks/MockDesktop.hh
index ceba2371..457fb182 100644
--- a/tests/mocks/MockDesktop.hh
+++ b/tests/mocks/MockDesktop.hh
@@ -104,6 +104,7 @@ private:
                 return nResult;
         }
 
+        vmViewModel.SetIsVisible(true);
         return ra::ui::DialogResult::None;
     }
 
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index a75f5eb7..b531d9c3 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -9,6 +9,7 @@
 #include "tests\ui\UIAsserts.hh"
 
 #include "tests\mocks\MockAchievementRuntime.hh"
+#include "tests\mocks\MockConfiguration.hh"
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockGameContext.hh"
@@ -85,6 +86,7 @@ private:
     {
     public:
         ra::services::mocks::MockAchievementRuntime mockRuntime;
+        ra::services::mocks::MockConfiguration mockConfiguration;
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
@@ -1973,6 +1975,7 @@ public:
         vmAssetList.MockGameId(22U);
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
         vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1");
+        vmAssetList.ForceUpdateButtons();
 
         Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
         Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
@@ -2569,6 +2572,88 @@ public:
         Assert::IsFalse(vmAssetList.mockRuntime.IsPaused());
         Assert::IsTrue(vmAssetList.mockEmulatorContext.WasMemoryModified());
     }
+
+    TEST_METHOD(TestOpenEditor)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
+
+        vmAssetList.SetGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1");
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Core, vmAssetList.GetFilterCategory());
+
+        auto* pItem = vmAssetList.FilteredAssets().GetItemAt(1);
+        Expects(pItem != nullptr);
+        pItem->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        vmAssetList.OpenEditor(pItem);
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        Assert::AreEqual(ra::to_unsigned(pItem->GetId()), vmAssetList.mockWindowManager.AssetEditor.GetID());
+    }
+
+    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.SetGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1");
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Core, vmAssetList.GetFilterCategory());
+
+        auto* pItem = vmAssetList.FilteredAssets().GetItemAt(1);
+        Expects(pItem != nullptr);
+        pItem->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        vmAssetList.OpenEditor(pItem);
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        Assert::AreEqual(ra::to_unsigned(pItem->GetId()), vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsFalse(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
+
+    TEST_METHOD(TestOpenEditorHardcoreCancel)
+    {
+        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.SetGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1");
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetCategory::Core, vmAssetList.GetFilterCategory());
+
+        auto* pItem = vmAssetList.FilteredAssets().GetItemAt(1);
+        Expects(pItem != nullptr);
+        pItem->SetSelected(true);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        vmAssetList.OpenEditor(pItem);
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsVisible());
+        Assert::AreEqual(0U, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore));
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp b/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
index 6f3ae08d..c1ea18ed 100644
--- a/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
@@ -7,6 +7,7 @@
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockGameContext.hh"
+#include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockWindowManager.hh"
 
 using namespace Microsoft::VisualStudio::CppUnitTestFramework;
@@ -25,6 +26,7 @@ private:
         ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
         ra::data::context::mocks::MockGameContext mockGameContext;
         ra::services::mocks::MockConfiguration mockConfiguration;
+        ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::ui::mocks::MockDesktop mockDesktop;
         ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;