Repository: RetroAchievements/RAIntegration
Pull request #978 (2023-02-14 04:50:25)

hide all popups and overlay when activating game 0
prevents remnants from remaining after the emulator indicates a game has been unloaded


Patch:
diff --git a/src/Exports.cpp b/src/Exports.cpp
index a6fb841b..1fddb60b 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -398,6 +398,14 @@ API unsigned int CCONV _RA_IdentifyHash(const char* sHash)
 API void CCONV _RA_ActivateGame(unsigned int nGameId)
 {
     _RA_SuspendRepaint();
+
+    if (nGameId == 0)
+    {
+        auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
+        pOverlayManager.ClearPopups();
+        pOverlayManager.HideOverlayImmediately();
+    }
+
     ra::services::ServiceLocator::GetMutable<ra::services::GameIdentifier>().ActivateGame(nGameId);
     _RA_ResumeRepaint();
 }
diff --git a/src/data/models/LeaderboardModel.hh b/src/data/models/LeaderboardModel.hh
index bf9cc1a6..561e8538 100644
--- a/src/data/models/LeaderboardModel.hh
+++ b/src/data/models/LeaderboardModel.hh
@@ -116,6 +116,11 @@ public:
     /// </summary>
     void SetValueDefinition(const std::string& sTrigger) { SetAssetDefinition(m_pValueDefinition, sTrigger); }
 
+    /// <summary>
+    /// Gets the calculated hash for the value definition.
+    /// </summary>
+    const int GetValueDefinitionHash() const { return GetValue(ValueDefinitionProperty); }
+
     /// <summary>
     /// The <see cref="ModelProperty" /> for the value format.
     /// </summary>
diff --git a/src/services/GameIdentifier.cpp b/src/services/GameIdentifier.cpp
index 8d5ecda3..6d4c9191 100644
--- a/src/services/GameIdentifier.cpp
+++ b/src/services/GameIdentifier.cpp
@@ -47,7 +47,13 @@ unsigned int GameIdentifier::IdentifyGame(const BYTE* pROM, size_t nROMSize)
     }
 
     char hash[33];
-    rc_hash_generate_from_buffer(hash, nConsoleId, pROM, nROMSize);
+    if (!rc_hash_generate_from_buffer(hash, nConsoleId, pROM, nROMSize))
+    {
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+            L"Could not identify game.",
+            L"No hash was generated for the provided content.");
+        return 0U;
+    }
 
     return IdentifyHash(hash);
 }
@@ -214,6 +220,9 @@ void GameIdentifier::ActivateGame(unsigned int nGameId)
 
         ra::services::ServiceLocator::GetMutable<ra::data::context::SessionTracker>().EndSession();
 
+        auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
+        pOverlayManager.ClearPopups();
+
         auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
         pGameContext.LoadGame(0U, m_nPendingMode);
         pGameContext.SetGameHash((m_nPendingGameId == 0) ? m_sPendingHash : "");
diff --git a/src/ui/viewmodels/OverlayManager.cpp b/src/ui/viewmodels/OverlayManager.cpp
index a4d89c39..3fc7ba30 100644
--- a/src/ui/viewmodels/OverlayManager.cpp
+++ b/src/ui/viewmodels/OverlayManager.cpp
@@ -580,23 +580,25 @@ void OverlayManager::UpdateActiveScoreboard(ra::ui::drawing::ISurface& pSurface,
     {
         m_vScoreboards.pop_front();
         if (m_vScoreboards.empty())
-            break;
+            return;
 
         pActiveScoreboard = &m_vScoreboards.front();
         pActiveScoreboard->BeginAnimation();
         pActiveScoreboard->UpdateRenderImage(0.0);
+        UpdatePopup(pSurface, pPopupLocations, fElapsed, *pActiveScoreboard);
     }
 
-    if (!m_vScoreboards.empty())
-        AdjustLocationForPopup(pPopupLocations, m_vScoreboards.front());
+    AdjustLocationForPopup(pPopupLocations, *pActiveScoreboard);
 }
 
 void OverlayManager::UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, PopupLocations& pPopupLocations, double fElapsed)
 {
     assert(!m_vScoreTrackers.empty());
 
+    const auto& pAssets = ra::services::ServiceLocator::Get<ra::data::context::GameContext>().Assets();
     const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
     const auto bEnabled = (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) != ra::ui::viewmodels::PopupLocation::None);
+    std::set<unsigned> vDisplayedValues;
 
     auto pIter = m_vScoreTrackers.begin();
     while (pIter != m_vScoreTrackers.end())
@@ -613,7 +615,27 @@ void OverlayManager::UpdateScoreTrackers(ra::ui::drawing::ISurface& pSurface, Po
         else
         {
             if (bEnabled)
+            {
+                if (m_vScoreTrackers.size() > 1)
+                {
+                    const auto* pLeaderboard = pAssets.FindLeaderboard(vmTracker.GetPopupId());
+                    if (pLeaderboard != nullptr)
+                    {
+                        const auto nDisplayedValue = pLeaderboard->GetValueDefinitionHash();
+                        if (vDisplayedValues.find(nDisplayedValue) != vDisplayedValues.end())
+                        {
+                            // position offscreen
+                            vmTracker.SetRenderLocationY(-100);
+                            ++pIter;
+                            continue;
+                        }
+
+                        vDisplayedValues.insert(nDisplayedValue);
+                    }
+                }
+
                 AdjustLocationForPopup(pPopupLocations, vmTracker);
+            }
 
             ++pIter;
         }
diff --git a/src/ui/viewmodels/OverlayManager.hh b/src/ui/viewmodels/OverlayManager.hh
index 4c8fdd6b..4f8ce986 100644
--- a/src/ui/viewmodels/OverlayManager.hh
+++ b/src/ui/viewmodels/OverlayManager.hh
@@ -67,6 +67,14 @@ public:
         m_vmOverlay.Deactivate();
     }
 
+    /// <summary>
+    /// Hides the overlay without animating it.
+    /// </summary>
+    void HideOverlayImmediately()
+    {
+        m_vmOverlay.DeactivateImmediately();
+    }
+
     /// <summary>
     /// Determines if the overlay is completely covering the emulator screen.
     /// </summary>
diff --git a/src/ui/viewmodels/OverlayViewModel.cpp b/src/ui/viewmodels/OverlayViewModel.cpp
index 0154630c..8ea474a2 100644
--- a/src/ui/viewmodels/OverlayViewModel.cpp
+++ b/src/ui/viewmodels/OverlayViewModel.cpp
@@ -357,6 +357,18 @@ void OverlayViewModel::Deactivate()
     }
 }
 
+void OverlayViewModel::DeactivateImmediately()
+{
+    if (m_nState != State::Hidden)
+    {
+        // set the state to completely faded out, but not hidden, as it won't repaint once
+        // it's completely hidden.
+        m_nState = State::FadeOut;
+        m_fAnimationProgress = INOUT_TIME;
+        RefreshOverlay();
+    }
+}
+
 void OverlayViewModel::PopulatePages()
 {
     m_vPages.resize(4);
diff --git a/src/ui/viewmodels/OverlayViewModel.hh b/src/ui/viewmodels/OverlayViewModel.hh
index 939e2d5a..b82db1f2 100644
--- a/src/ui/viewmodels/OverlayViewModel.hh
+++ b/src/ui/viewmodels/OverlayViewModel.hh
@@ -63,6 +63,7 @@ public:
 
     void Activate();
     void Deactivate();
+    void DeactivateImmediately();
 
     class PageViewModel : public ViewModelBase
     {


Test patch:
diff --git a/tests/services/GameIdentifier_Tests.cpp b/tests/services/GameIdentifier_Tests.cpp
index 5e5f7566..cf2d4b85 100644
--- a/tests/services/GameIdentifier_Tests.cpp
+++ b/tests/services/GameIdentifier_Tests.cpp
@@ -164,6 +164,24 @@ public:
         Assert::IsTrue(bDialogShown);
     }
 
+    TEST_METHOD(TestIdentifyGameUnhashable)
+    {
+        GameIdentifierHarness identifier;
+        ra::data::context::mocks::MockConsoleContext n64Context(N64, L"N64");
+
+        bool bDialogShown = false;
+        identifier.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bDialogShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Could not identify game."), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"No hash was generated for the provided content."), vmMessageBox.GetMessage());
+            bDialogShown = true;
+            return ra::ui::DialogResult::OK;
+        });
+
+        Assert::AreEqual(0U, identifier.IdentifyGame(&ROM.at(0), ROM.size()));
+        Assert::IsTrue(bDialogShown);
+    }
+
     TEST_METHOD(TestIdentifyGameNotLoggedIn)
     {
         GameIdentifierHarness identifier;
@@ -179,6 +197,7 @@ public:
         });
 
         Assert::AreEqual(0U, identifier.IdentifyGame(&ROM.at(0), ROM.size()));
+        Assert::IsTrue(bDialogShown);
     }
 
     TEST_METHOD(TestIdentifyGameNoConsole)
@@ -196,6 +215,7 @@ public:
         });
 
         Assert::AreEqual(0U, identifier.IdentifyGame(&ROM.at(0), ROM.size()));
+        Assert::IsTrue(bDialogShown);
     }
 
     TEST_METHOD(TestIdentifyGameHashChange)
diff --git a/tests/ui/viewmodels/OverlayManager_Tests.cpp b/tests/ui/viewmodels/OverlayManager_Tests.cpp
index 75d6f12e..1345b74b 100644
--- a/tests/ui/viewmodels/OverlayManager_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayManager_Tests.cpp
@@ -82,6 +82,17 @@ private:
             return m_vmProgressTracker.get();
         }
 
+        void AddLeaderboard(unsigned id, const std::string& sDefinition)
+        {
+            auto vmLeaderboard = std::make_unique<ra::data::models::LeaderboardModel>();
+            vmLeaderboard->SetID(id);
+            vmLeaderboard->SetCategory(ra::data::models::AssetCategory::Core);
+            vmLeaderboard->SetValueDefinition(sDefinition);
+            vmLeaderboard->CreateServerCheckpoint();
+            vmLeaderboard->CreateLocalCheckpoint();
+            mockGameContext.Assets().Append(std::move(vmLeaderboard));
+        }
+
     private:
         bool m_bRenderRequested = false;
         bool m_bShowRequested = false;
@@ -298,6 +309,72 @@ public:
         Assert::IsNull(overlay.GetScoreTracker(3)); // render should erase and destroy
     }
 
+    TEST_METHOD(TestAddMultipleScoreTrackers)
+    {
+        OverlayManagerHarness overlay;
+        overlay.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight);
+        overlay.AddLeaderboard(3, "0xH1234");
+        overlay.AddLeaderboard(4, "0xH2345");
+
+        const auto& vmScoreTracker = overlay.AddScoreTracker(3);
+        Assert::AreEqual(3, vmScoreTracker.GetPopupId());
+        Assert::IsFalse(vmScoreTracker.IsDestroyPending());
+
+        const auto& vmScoreTracker2 = overlay.AddScoreTracker(4);
+        Assert::AreEqual(4, vmScoreTracker2.GetPopupId());
+        Assert::IsFalse(vmScoreTracker2.IsDestroyPending());
+
+        // no defined font, widget will only be 3 pixels tall
+        constexpr int nWidgetHeight = 3;
+        ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
+        overlay.Render(mockSurface, false);
+        Assert::AreEqual(600 - 10 - nWidgetHeight, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(600 - 10 - nWidgetHeight - 10 - nWidgetHeight, vmScoreTracker2.GetRenderLocationY());
+
+        overlay.ResetRenderRequested();
+        overlay.RemoveScoreTracker(3);
+        Assert::IsTrue(vmScoreTracker.IsDestroyPending()); // has to erase itself in render
+        Assert::IsFalse(overlay.WasRenderRequested());
+        Assert::IsNotNull(overlay.GetScoreTracker(3));
+
+        overlay.Render(mockSurface, false);
+        Assert::IsNull(overlay.GetScoreTracker(3)); // render should erase and destroy
+        Assert::AreEqual(600 - 10 - nWidgetHeight, vmScoreTracker2.GetRenderLocationY());
+    }
+
+    TEST_METHOD(TestAddMultipleScoreTrackersSharedValue)
+    {
+        OverlayManagerHarness overlay;
+        overlay.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight);
+        overlay.AddLeaderboard(3, "0xH1234");
+        overlay.AddLeaderboard(4, "0xH1234"); // same value definition
+
+        const auto& vmScoreTracker = overlay.AddScoreTracker(3);
+        Assert::AreEqual(3, vmScoreTracker.GetPopupId());
+        Assert::IsFalse(vmScoreTracker.IsDestroyPending());
+
+        const auto& vmScoreTracker2 = overlay.AddScoreTracker(4);
+        Assert::AreEqual(4, vmScoreTracker2.GetPopupId());
+        Assert::IsFalse(vmScoreTracker2.IsDestroyPending());
+
+        // no defined font, widget will only be 3 pixels tall
+        constexpr int nWidgetHeight = 3;
+        ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
+        overlay.Render(mockSurface, false);
+        Assert::AreEqual(600 - 10 - nWidgetHeight, vmScoreTracker.GetRenderLocationY());
+        Assert::AreEqual(-100, vmScoreTracker2.GetRenderLocationY()); // second widget positioned offscreen
+
+        overlay.ResetRenderRequested();
+        overlay.RemoveScoreTracker(3);
+        Assert::IsTrue(vmScoreTracker.IsDestroyPending()); // has to erase itself in render
+        Assert::IsFalse(overlay.WasRenderRequested());
+        Assert::IsNotNull(overlay.GetScoreTracker(3));
+
+        overlay.Render(mockSurface, false);
+        Assert::IsNull(overlay.GetScoreTracker(3)); // render should erase and destroy
+        Assert::AreEqual(600 - 10 - nWidgetHeight, vmScoreTracker2.GetRenderLocationY());
+    }
+
     TEST_METHOD(TestQueueScoreboard)
     {
         OverlayManagerHarness overlay;
@@ -605,6 +682,33 @@ public:
 
         Assert::IsFalse(overlay.IsOverlayFullyVisible());
     }
+
+    TEST_METHOD(TestHideOverlayImmediately)
+    {
+        OverlayManagerHarness overlay;
+        ra::ui::drawing::mocks::MockSurface mockSurface(800, 600);
+
+        overlay.ShowOverlay();
+
+        // after 0.5 seconds, it should be fully on screen
+        overlay.mockClock.AdvanceTime(std::chrono::milliseconds(500));
+        overlay.ResetRenderRequested();
+        overlay.Render(mockSurface, false);
+        Assert::AreEqual(0, overlay.GetOverlayRenderX());
+        Assert::IsFalse(overlay.WasRenderRequested());
+        Assert::IsTrue(overlay.IsOverlayFullyVisible());
+
+        overlay.HideOverlayImmediately();
+        Assert::IsTrue(overlay.WasRenderRequested());
+        Assert::IsFalse(overlay.IsOverlayFullyVisible());
+
+        overlay.Render(mockSurface, false); // force state change from FadeOut to Hidden
+
+        // it should be fully off screen immediately
+        Assert::AreEqual(-800, overlay.GetOverlayRenderX());
+        Assert::IsTrue(overlay.WasRenderRequested());
+        Assert::IsTrue(overlay.WasHideRequested());
+    }
 };
 
 } // namespace tests