Repository: RetroAchievements/RAIntegration
Pull request #876 (2022-03-09 01:48:04)

remember previously selected game for Test Compatibility feature
When selecting a game for "Test Compatibility" mode, the selected game will be associated to the unknown hash in a file in the RACache\Data directory. In the future, when the same hash is unidentified, the mapping will be located and the previously selected game will be automatically selected. The unknown game dialog will still be shown in case the user wants to select a different game.


Patch:
diff --git a/src/services/ILocalStorage.hh b/src/services/ILocalStorage.hh
index 47cf90e2..9ec06c79 100644
--- a/src/services/ILocalStorage.hh
+++ b/src/services/ILocalStorage.hh
@@ -18,7 +18,8 @@ enum class StorageItemType
     Badge,
     UserPic,
     SessionStats,
-    Bookmarks
+    Bookmarks,
+    HashMapping,
 };
 
 class ILocalStorage
diff --git a/src/services/impl/FileLocalStorage.cpp b/src/services/impl/FileLocalStorage.cpp
index eda1d370..1b1c7feb 100644
--- a/src/services/impl/FileLocalStorage.cpp
+++ b/src/services/impl/FileLocalStorage.cpp
@@ -117,6 +117,12 @@ std::wstring FileLocalStorage::GetPath(StorageItemType nType, const std::wstring
             sPath.append(L"-Bookmarks.json");
             break;
 
+        case StorageItemType::HashMapping:
+            sPath.append(RA_DIR_DATA);
+            sPath.append(sKey);
+            sPath.append(L".txt");
+            break;
+
         default:
             assert(!"unhandled StorageItemType");
             sPath.append(RA_DIR_DATA);
diff --git a/src/ui/viewmodels/UnknownGameViewModel.cpp b/src/ui/viewmodels/UnknownGameViewModel.cpp
index 432bb308..1e9e8144 100644
--- a/src/ui/viewmodels/UnknownGameViewModel.cpp
+++ b/src/ui/viewmodels/UnknownGameViewModel.cpp
@@ -9,6 +9,7 @@
 #include "data\context\GameContext.hh"
 
 #include "services\IClipboard.hh"
+#include "services\ILocalStorage.hh"
 
 #include "ui\viewmodels\MessageBoxViewModel.hh"
 
@@ -65,6 +66,8 @@ void UnknownGameViewModel::InitializeGameTitles()
 
         SetValue(IsAssociateEnabledProperty, true);
         SetValue(IsSelectedGameEnabledProperty, true);
+
+        CheckForPreviousAssociation();
     });
 }
 
@@ -79,6 +82,59 @@ void UnknownGameViewModel::InitializeTestCompatibilityMode()
     SetValue(IsSelectedGameEnabledProperty, false);
 }
 
+void UnknownGameViewModel::CheckForPreviousAssociation()
+{
+    if (m_vGameTitles.Count() == 0)
+        return;
+
+    const auto& sHash = GetChecksum();
+    if (sHash.empty())
+        return;
+
+    auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
+    auto sMapping = pLocalStorage.ReadText(ra::services::StorageItemType::HashMapping, sHash);
+    std::string sLine;
+    if (sMapping && sMapping->GetLine(sLine))
+    {
+        const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+        const unsigned nId = DecodeID(sLine, sHash, pConsoleContext.Id());
+        if (nId > 0)
+        {
+            const auto& sGameName = m_vGameTitles.GetLabelForId(nId);
+            if (!sGameName.empty())
+                SetSelectedGameId(nId);
+        }
+    }
+}
+
+static constexpr unsigned GenerateMask(const std::wstring& sHash, ConsoleID nConsoleId)
+{
+    // use longs to multiply to something more than 32-bits, then truncated and invert several bits
+    return gsl::narrow_cast<unsigned>(
+        ra::to_unsigned(gsl::narrow_cast<long>(sHash.back())) *  // 7 bits
+        ra::to_unsigned(gsl::narrow_cast<long>(sHash.front())) * // 7 bits
+        (ra::etoi(nConsoleId) + 1) *                             // 7 bits
+        49999                                                    // 16 bits
+        ) ^ ~0x52a761cf;
+}
+
+unsigned UnknownGameViewModel::DecodeID(const std::string& sLine, const std::wstring& sHash, ConsoleID nConsoleId)
+{
+    std::wstring sError;
+    unsigned nEncodedId;
+
+    if (ra::ParseHex(ra::Widen(sLine), 0xFFFFFFFF, nEncodedId, sError))
+        return nEncodedId ^ GenerateMask(sHash, nConsoleId);
+
+    return 0;
+}
+
+std::string UnknownGameViewModel::EncodeID(unsigned nId, const std::wstring& sHash, ConsoleID nConsoleId)
+{
+    const auto nEncodedId = nId ^ GenerateMask(sHash, nConsoleId);
+    return ra::StringPrintf("%08x", nEncodedId);
+}
+
 bool UnknownGameViewModel::Associate()
 {
     ra::api::SubmitNewTitle::Request request;
@@ -148,6 +204,12 @@ bool UnknownGameViewModel::BeginTest()
     if (vmMessageBox.ShowModal(*this) == DialogResult::No)
         return false;
 
+    const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+    auto sValue = EncodeID(nGameId, GetChecksum(), pConsoleContext.Id());
+    auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
+    auto sMapping = pLocalStorage.WriteText(ra::services::StorageItemType::HashMapping, GetChecksum());
+    sMapping->WriteLine(sValue);
+
     SetTestMode(true);
     return true;
 }
@@ -159,6 +221,10 @@ void UnknownGameViewModel::OnValueChanged(const StringModelProperty::ChangeArgs&
         // user is entering a custom name, make sure <New Game> is selected
         SetSelectedGameId(0);
     }
+    else if (args.Property == ChecksumProperty)
+    {
+        CheckForPreviousAssociation();
+    }
 
     WindowViewModelBase::OnValueChanged(args);
 }
diff --git a/src/ui/viewmodels/UnknownGameViewModel.hh b/src/ui/viewmodels/UnknownGameViewModel.hh
index 94e8f54f..3a682c63 100644
--- a/src/ui/viewmodels/UnknownGameViewModel.hh
+++ b/src/ui/viewmodels/UnknownGameViewModel.hh
@@ -2,6 +2,8 @@
 #define RA_UI_UNKNOWNGAMEVIEWMODEL_H
 #pragma once
 
+#include "RAInterface\RA_Consoles.h"
+
 #include "data\AsyncObject.hh"
 
 #include "ui\WindowViewModelBase.hh"
@@ -178,6 +180,11 @@ protected:
     void OnValueChanged(const StringModelProperty::ChangeArgs& args) override;
     void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
 
+    void CheckForPreviousAssociation();
+
+    static unsigned DecodeID(const std::string& sEncoded, const std::wstring& sHash, ConsoleID nConsoleId);
+    static std::string EncodeID(unsigned nId, const std::wstring& sHash, ConsoleID nConsoleId);
+
     LookupItemViewModelCollection m_vGameTitles;
     bool m_bSelectingGame = false;
 };


Test patch:
diff --git a/tests/services/FileLocalStorage_Tests.cpp b/tests/services/FileLocalStorage_Tests.cpp
index 0369a21c..df976e3c 100644
--- a/tests/services/FileLocalStorage_Tests.cpp
+++ b/tests/services/FileLocalStorage_Tests.cpp
@@ -117,6 +117,7 @@ public:
         Assert::AreEqual(storage.GetPath(ra::services::StorageItemType::Badge, L"12345"), std::wstring(L".\\RACache\\Badge\\12345.png"));
         Assert::AreEqual(storage.GetPath(ra::services::StorageItemType::UserPic, L"12345"), std::wstring(L".\\RACache\\UserPic\\12345.png"));
         Assert::AreEqual(storage.GetPath(ra::services::StorageItemType::Bookmarks, L"12345"), std::wstring(L".\\RACache\\Bookmarks\\12345-Bookmarks.json"));
+        Assert::AreEqual(storage.GetPath(ra::services::StorageItemType::HashMapping, L"0123456789abcdef0123456789abcdef"), std::wstring(L".\\RACache\\Data\\0123456789abcdef0123456789abcdef.txt"));
     }
 
     TEST_METHOD(TestReadTextNonExistant)
diff --git a/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp b/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
index 03bc6dec..d73f1432 100644
--- a/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
@@ -8,6 +8,7 @@
 #include "tests\mocks\MockConsoleContext.hh"
 #include "tests\mocks\MockDesktop.hh"
 #include "tests\mocks\MockGameContext.hh"
+#include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
 
@@ -30,6 +31,7 @@ private:
         ra::data::context::mocks::MockConsoleContext mockConsoleContext;
         ra::data::context::mocks::MockGameContext mockGameContext;
         ra::services::mocks::MockThreadPool mockThreadPool;
+        ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::ui::mocks::MockDesktop mockDesktop;
 
         void MockGameTitles()
@@ -365,6 +367,9 @@ public:
         vmUnknownGame.SetSelectedGameId(40U);
         vmUnknownGame.SetChecksum(L"CHECKSUM");
 
+        Assert::AreEqual(std::string(),
+            vmUnknownGame.mockLocalStorage.GetStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM"));
+
         vmUnknownGame.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
         {
             Assert::AreEqual(std::wstring(L"Play 'Game 40' in compatability test mode?"), vmMessageBox.GetHeader());
@@ -375,6 +380,9 @@ public:
         Assert::IsTrue(vmUnknownGame.BeginTest());
         Assert::AreEqual(40, vmUnknownGame.GetSelectedGameId());
         Assert::IsTrue(vmUnknownGame.GetTestMode());
+
+        Assert::AreEqual(std::string("b745f623\n"),
+            vmUnknownGame.mockLocalStorage.GetStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM"));
     }
 
     TEST_METHOD(TestBeginTestExistingGameCancel)
@@ -386,6 +394,9 @@ public:
         vmUnknownGame.SetSelectedGameId(40U);
         vmUnknownGame.SetChecksum(L"CHECKSUM");
 
+        Assert::AreEqual(std::string(),
+            vmUnknownGame.mockLocalStorage.GetStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM"));
+
         vmUnknownGame.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
         {
             Assert::AreEqual(std::wstring(L"Play 'Game 40' in compatability test mode?"), vmMessageBox.GetHeader());
@@ -394,6 +405,37 @@ public:
         });
 
         Assert::IsFalse(vmUnknownGame.BeginTest());
+
+        Assert::AreEqual(std::string(),
+            vmUnknownGame.mockLocalStorage.GetStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM"));
+    }
+
+    TEST_METHOD(TestBeginTestExistingGameRemembered)
+    {
+        UnknownGameViewModelHarness vmUnknownGame;
+        vmUnknownGame.mockConsoleContext.SetId(ConsoleID::VIC20);
+        vmUnknownGame.mockLocalStorage.MockStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM", "b745f623\n");
+        vmUnknownGame.MockGameTitles();
+        vmUnknownGame.SetNewGameName(L"TestGame");
+
+        Assert::AreEqual(0, vmUnknownGame.GetSelectedGameId());
+        vmUnknownGame.SetChecksum(L"CHECKSUM");
+
+        Assert::AreEqual(40, vmUnknownGame.GetSelectedGameId());
+
+        vmUnknownGame.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        {
+            Assert::AreEqual(std::wstring(L"Play 'Game 40' in compatability test mode?"), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"Achievements and leaderboards for the game will be loaded, but you will not be able to earn them."), vmMessageBox.GetMessage());
+            return ra::ui::DialogResult::Yes;
+        });
+
+        Assert::IsTrue(vmUnknownGame.BeginTest());
+        Assert::AreEqual(40, vmUnknownGame.GetSelectedGameId());
+        Assert::IsTrue(vmUnknownGame.GetTestMode());
+
+        Assert::AreEqual(std::string("b745f623\n"),
+            vmUnknownGame.mockLocalStorage.GetStoredData(ra::services::StorageItemType::HashMapping, L"CHECKSUM"));
     }
 
     TEST_METHOD(TestInitializeTestCompatibilityMode)