Repository: RetroAchievements/RAIntegration
Pull request #878 (2022-03-25 01:09:36)

use user's display name for UI elements
support for v2 feature, should have no immediate impact


Patch:
diff --git a/src/Exports.cpp b/src/Exports.cpp
index 349b3d38..c3d3c2bf 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -216,7 +216,7 @@ static void HandleLoginResponse(const ra::api::Login::Response& response)
     if (response.Succeeded())
     {
         // initialize the user context
-        pUserContext.Initialize(response.Username, response.ApiToken);
+        pUserContext.Initialize(response.Username, response.DisplayName, response.ApiToken);
         pUserContext.SetScore(response.Score);
 
         // load the session information
@@ -228,7 +228,7 @@ static void HandleLoginResponse(const ra::api::Login::Response& response)
 
         std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmMessage(new ra::ui::viewmodels::PopupMessageViewModel);
         vmMessage->SetTitle(ra::StringPrintf(L"Welcome %s%s", pSessionTracker.HasSessionData() ? L"back " : L"",
-            response.Username.c_str()));
+            response.DisplayName.c_str()));
         vmMessage->SetDescription((response.NumUnreadMessages == 1)
             ? L"You have 1 new message"
             : ra::StringPrintf(L"You have %u new messages", response.NumUnreadMessages));
@@ -291,7 +291,7 @@ API void CCONV _RA_AttemptLogin(int bBlocking)
 API const char* CCONV _RA_UserName()
 {
     auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-    return pUserContext.GetUsername().c_str();
+    return pUserContext.GetDisplayName().c_str();
 }
 
 API void CCONV _RA_SetConsoleID(unsigned int nConsoleId)
diff --git a/src/api/Login.hh b/src/api/Login.hh
index ed2e2b66..5c82a686 100644
--- a/src/api/Login.hh
+++ b/src/api/Login.hh
@@ -16,6 +16,7 @@ public:
     {
         std::string Username;
         std::string ApiToken;
+        std::string DisplayName;
         unsigned int Score{ 0U };
         unsigned int NumUnreadMessages{ 0U };
     };
diff --git a/src/api/impl/ConnectedServer.cpp b/src/api/impl/ConnectedServer.cpp
index d996d577..40c0cf09 100644
--- a/src/api/impl/ConnectedServer.cpp
+++ b/src/api/impl/ConnectedServer.cpp
@@ -481,6 +481,7 @@ Login::Response ConnectedServer::Login(const Login::Request& request)
                 response.Result = ApiResult::Success;
                 response.Username = api_response.username;
                 response.ApiToken = api_response.api_token;
+                response.DisplayName = api_response.display_name;
                 response.Score = api_response.score;
                 response.NumUnreadMessages = api_response.num_unread_messages;
             }
diff --git a/src/data/context/EmulatorContext.cpp b/src/data/context/EmulatorContext.cpp
index 1de5ea7a..e525f1f9 100644
--- a/src/data/context/EmulatorContext.cpp
+++ b/src/data/context/EmulatorContext.cpp
@@ -542,7 +542,7 @@ std::wstring EmulatorContext::GetAppTitle(const std::string& sMessage) const
     if (pUserContext.IsLoggedIn())
     {
         builder.Append(" - ");
-        builder.Append(pUserContext.GetUsername());
+        builder.Append(pUserContext.GetDisplayName());
     }
 
     const auto& sHostName = ra::services::ServiceLocator::Get<ra::services::IConfiguration>().GetHostName();
diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp
index b0ffef9b..4ca41f25 100644
--- a/src/data/context/GameContext.cpp
+++ b/src/data/context/GameContext.cpp
@@ -655,7 +655,7 @@ void GameContext::ShowSimplifiedScoreboard(ra::LeaderboardID nLeaderboardId, int
     ra::ui::viewmodels::ScoreboardViewModel vmScoreboard;
     vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard->GetName()));
 
-    const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetUsername();
+    const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
     auto& pEntryViewModel = vmScoreboard.Entries().Add();
     pEntryViewModel.SetRank(0);
     pEntryViewModel.SetScore(ra::Widen(pLeaderboard->FormatScore(nScore)));
@@ -762,7 +762,7 @@ void GameContext::SubmitLeaderboardEntry(ra::LeaderboardID nLeaderboardId, int n
                 ra::ui::viewmodels::ScoreboardViewModel vmScoreboard;
                 vmScoreboard.SetHeaderText(pLeaderboard->GetName());
 
-                const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetUsername();
+                const auto& pUserName = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
                 constexpr int nEntriesDisplayed = 7; // display is currently hard-coded to show 7 entries
                 bool bSeenPlayer = false;
 
@@ -1224,7 +1224,7 @@ bool GameContext::SetCodeNote(ra::ByteAddress nAddress, const std::wstring& sNot
         if (response.Succeeded())
         {
             const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-            AddCodeNote(nAddress, pUserContext.GetUsername(), sNote);
+            AddCodeNote(nAddress, pUserContext.GetDisplayName(), sNote);
             return true;
         }
 
diff --git a/src/data/context/UserContext.hh b/src/data/context/UserContext.hh
index 39b4fcbc..3133eeb9 100644
--- a/src/data/context/UserContext.hh
+++ b/src/data/context/UserContext.hh
@@ -21,9 +21,10 @@ public:
     /// <summary>
     /// Sets the username and API token for the current user.
     /// </summary>
-    void Initialize(const std::string& sUsername, const std::string& sApiToken)
+    void Initialize(const std::string& sUsername, const std::string& sDisplayName, const std::string& sApiToken)
     {
         m_sUsername = sUsername;
+        m_sDisplayName = sDisplayName;
         m_sApiToken = sApiToken;
         m_nScore = 0U;
     }
@@ -38,6 +39,11 @@ public:
     /// </summary>
     const std::string& GetApiToken() const noexcept { return m_sApiToken; }
 
+    /// <summary>
+    /// Gets the display name of the current user.
+    /// </summary>
+    const std::string& GetDisplayName() const noexcept { return m_sDisplayName; }
+
     /// <summary>
     /// Determines whether the current user is logged in.
     /// </summary>
@@ -70,6 +76,7 @@ public:
 
 protected:
     std::string m_sUsername;
+    std::string m_sDisplayName;
     std::string m_sApiToken;
     unsigned int m_nScore{ 0U };
     bool m_bDisableLogin{ false };
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/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 3a12d214..81bd06dd 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -451,7 +451,7 @@ bool AssetListViewModel::MatchesFilter(const ra::data::models::AssetModelBase& p
             break;
 
         case SpecialFilter::Authored:
-            if (pAsset.GetAuthor() != ra::Widen(ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetUsername()))
+            if (pAsset.GetAuthor() != ra::Widen(ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName()))
                 return false;
             break;
     }
@@ -1589,7 +1589,7 @@ void AssetListViewModel::CreateNew()
     Expects(pNewAsset != nullptr);
 
     const auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
-    pNewAsset->SetAuthor(ra::Widen(pUserContext.GetUsername()));
+    pNewAsset->SetAuthor(ra::Widen(pUserContext.GetDisplayName()));
     pNewAsset->SetCategory(ra::data::models::AssetCategory::Local);
     pNewAsset->UpdateServerCheckpoint();
     pNewAsset->SetNew();
diff --git a/src/ui/viewmodels/LoginViewModel.cpp b/src/ui/viewmodels/LoginViewModel.cpp
index 550489e7..470e84ba 100644
--- a/src/ui/viewmodels/LoginViewModel.cpp
+++ b/src/ui/viewmodels/LoginViewModel.cpp
@@ -68,7 +68,7 @@ bool LoginViewModel::Login() const
 
     // initialize the user context
     auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
-    pUserContext.Initialize(response.Username, response.ApiToken);
+    pUserContext.Initialize(response.Username, response.DisplayName, response.ApiToken);
     pUserContext.SetScore(response.Score);
 
     // load the session information
diff --git a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp
index 576fd604..76b78fa8 100644
--- a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp
+++ b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp
@@ -487,7 +487,7 @@ void OverlayAchievementsPageViewModel::FetchItemDetail(ItemViewModel& vmItem)
         vmAchievement.SetWonBy(ra::StringPrintf(L"Won by %u of %u (%1.0f%%)", response.EarnedBy, response.NumPlayers,
             (static_cast<double>(response.EarnedBy) * 100) / response.NumPlayers));
 
-        const auto& sUsername = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetUsername();
+        const auto& sUsername = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
         for (const auto& pWinner : response.Entries)
         {
             auto& vmWinner = vmAchievement.RecentWinners.Add();
diff --git a/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp b/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp
index 8ead492a..ca5b37b1 100644
--- a/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp
+++ b/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp
@@ -222,7 +222,7 @@ void OverlayLeaderboardsPageViewModel::FetchItemDetail(ItemViewModel& vmItem)
         if (!pLeaderboard)
             return;
 
-        const auto& sUsername = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetUsername();
+        const auto& sUsername = ra::services::ServiceLocator::Get<ra::data::context::UserContext>().GetDisplayName();
         auto& vmLeaderboard = pIter->second;
         for (const auto& pEntry : response.Entries)
         {
diff --git a/src/ui/viewmodels/OverlayViewModel.cpp b/src/ui/viewmodels/OverlayViewModel.cpp
index 1a3df0d5..0154630c 100644
--- a/src/ui/viewmodels/OverlayViewModel.cpp
+++ b/src/ui/viewmodels/OverlayViewModel.cpp
@@ -152,7 +152,7 @@ void OverlayViewModel::CreateRenderImage()
         m_pSurface->LoadFont(pTheme.FontOverlay(), pTheme.FontSizeOverlayHeader(), ra::ui::FontStyles::Normal);
 
     const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-    const auto sUserName = ra::Widen(pUserContext.GetUsername());
+    const auto sUserName = ra::Widen(pUserContext.GetDisplayName());
     const auto szUsername = m_pSurface->MeasureText(nFont, sUserName);
 
     const auto sPoints = ra::StringPrintf(L"%u Points", pUserContext.GetScore());
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index 91e1b8da..342e6d0e 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -1,6 +1,7 @@
 #include "TriggerViewModel.hh"
 
 #include <rcheevos.h>
+#include <rcheevos/src/rcheevos/rc_internal.h>
 
 #include "RA_StringUtils.h"
 
@@ -228,28 +229,45 @@ void TriggerViewModel::PasteFromClipboard()
         return;
     }
 
+    // have to use internal parsing functions to decode conditions without full trigger/value
+    // restriction validation (full validation will occur after new conditions are added)
+    rc_parse_state_t parse;
+    rc_init_parse_state(&parse, nullptr, nullptr, 0);
+    rc_memref_t* first_memref;
+    rc_init_parse_state_memrefs(&parse, &first_memref);
     std::string sTrigger = ra::Narrow(sClipboardText);
-    const auto nSize = rc_trigger_size(sTrigger.c_str());
-    if (nSize < 0)
+    const char* memaddr = sTrigger.c_str();
+    Expects(memaddr != nullptr);
+    rc_parse_condset(&memaddr, &parse, IsValue());
+
+    const auto nSize = parse.offset;
+    if (nSize > 0 && *memaddr == 'S')
     {
-        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Paste failed.", L"Clipboard did not contain valid trigger conditions.");
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
+            L"Paste failed.", L"Clipboard contained multiple groups.");
         return;
     }
 
-    std::string sTriggerBuffer;
-    sTriggerBuffer.resize(nSize);
-    const rc_trigger_t* pTrigger = rc_parse_trigger(sTriggerBuffer.data(), sTrigger.c_str(), nullptr, 0);
-    if (pTrigger->alternative)
+    if (nSize <= 0 || *memaddr != '\0')
     {
-        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Paste failed.", L"Clipboard contained multiple groups.");
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Paste failed.",
+            ra::StringPrintf(L"Clipboard did not contain valid %s conditions.", IsValue() ? "value" : "trigger"));
         return;
     }
 
+    std::string sTriggerBuffer;
+    sTriggerBuffer.resize(nSize);
+    rc_init_parse_state(&parse, sTriggerBuffer.data(), nullptr, 0);
+    rc_init_parse_state_memrefs(&parse, &first_memref);
+    memaddr = sTrigger.c_str();
+    const rc_condset_t* pCondSet = rc_parse_condset(&memaddr, &parse, IsValue());
+    Expects(pCondSet != nullptr);
+
     m_vConditions.BeginUpdate();
 
     DeselectAllConditions();
 
-    for (const rc_condition_t* pCondition = pTrigger->requirement->conditions;
+    for (const rc_condition_t* pCondition = pCondSet->conditions;
         pCondition != nullptr; pCondition = pCondition->next)
     {
         auto& vmCondition = m_vConditions.Add();
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/Exports_Tests.cpp b/tests/Exports_Tests.cpp
index e4469e8a..aa8ac312 100644
--- a/tests/Exports_Tests.cpp
+++ b/tests/Exports_Tests.cpp
@@ -151,6 +151,7 @@ public:
             bLoggedIn = true;
 
             response.Username = "User";
+            response.DisplayName = "UserDisplay";
             response.ApiToken = "ApiToken";
             response.Score = 12345U;
             response.Result = ra::api::ApiResult::Success;
@@ -167,6 +168,7 @@ public:
         // user context
         Assert::IsTrue(harness.mockUserContext.IsLoggedIn());
         Assert::AreEqual(std::string("User"), harness.mockUserContext.GetUsername());
+        Assert::AreEqual(std::string("UserDisplay"), harness.mockUserContext.GetDisplayName());
         Assert::AreEqual(std::string("ApiToken"), harness.mockUserContext.GetApiToken());
         Assert::AreEqual(12345U, harness.mockUserContext.GetScore());
 
@@ -178,7 +180,7 @@ public:
         const auto* pPopup = harness.mockOverlayManager.GetMessage(1);
         Assert::IsNotNull(pPopup);
         Ensures(pPopup != nullptr);
-        Assert::AreEqual(std::wstring(L"Welcome User"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Welcome UserDisplay"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"You have 0 new messages"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"12345 points"), pPopup->GetDetail());
         Assert::AreEqual(ra::ui::ImageType::UserPic, pPopup->GetImage().Type());
@@ -188,7 +190,7 @@ public:
         Assert::IsTrue(bWasMenuRebuilt);
 
         // titlebar
-        Assert::AreEqual(std::wstring(L"RATests - 0.1 - User []"), harness.mockWindowManager.Emulator.GetWindowTitle());
+        Assert::AreEqual(std::wstring(L"RATests - 0.1 - UserDisplay []"), harness.mockWindowManager.Emulator.GetWindowTitle());
     }
 
     TEST_METHOD(TestAttemptLoginSuccessWithMessages)
@@ -198,6 +200,7 @@ public:
         harness.mockServer.HandleRequest<api::Login>([](const ra::api::Login::Request&, ra::api::Login::Response& response)
         {
             response.Username = "User";
+            response.DisplayName = "UserDisplay";
             response.ApiToken = "ApiToken";
             response.NumUnreadMessages = 3;
             response.Score = 0U;
@@ -213,7 +216,7 @@ public:
         const auto* pPopup = harness.mockOverlayManager.GetMessage(1);
         Assert::IsNotNull(pPopup);
         Ensures(pPopup != nullptr);
-        Assert::AreEqual(std::wstring(L"Welcome User"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Welcome UserDisplay"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"You have 3 new messages"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"0 points"), pPopup->GetDetail());
         Assert::AreEqual(ra::ui::ImageType::UserPic, pPopup->GetImage().Type());
@@ -230,6 +233,7 @@ public:
         harness.mockServer.HandleRequest<api::Login>([](const ra::api::Login::Request&, ra::api::Login::Response& response)
         {
             response.Username = "User";
+            response.DisplayName = "UserDisplay";
             response.ApiToken = "ApiToken";
             response.Score = 12345U;
             response.Result = ra::api::ApiResult::Success;
@@ -246,7 +250,7 @@ public:
         const auto* pPopup = harness.mockOverlayManager.GetMessage(1);
         Assert::IsNotNull(pPopup);
         Ensures(pPopup != nullptr);
-        Assert::AreEqual(std::wstring(L"Welcome back User"), pPopup->GetTitle());
+        Assert::AreEqual(std::wstring(L"Welcome back UserDisplay"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"You have 0 new messages"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"12345 points"), pPopup->GetDetail());
         Assert::AreEqual(ra::ui::ImageType::UserPic, pPopup->GetImage().Type());
@@ -349,8 +353,8 @@ public:
     TEST_METHOD(TestUserNameLoggedIn)
     {
         MockUserContext mockUserContext;
-        mockUserContext.Initialize("Test", "ABCDEFG");
-        Assert::AreEqual(_RA_UserName(), "Test");
+        mockUserContext.Initialize("Test", "TestDisplay", "ABCDEFG");
+        Assert::AreEqual(_RA_UserName(), "TestDisplay");
     }
 
 private:
diff --git a/tests/data/context/EmulatorContext_Tests.cpp b/tests/data/context/EmulatorContext_Tests.cpp
index 264ef890..6a903f7e 100644
--- a/tests/data/context/EmulatorContext_Tests.cpp
+++ b/tests/data/context/EmulatorContext_Tests.cpp
@@ -992,8 +992,8 @@ public:
 
         Assert::AreEqual(std::wstring(L"RASnes9X - 0.57"), emulator.GetAppTitle(""));
 
-        emulator.mockUserContext.Initialize("User", "Token");
-        Assert::AreEqual(std::wstring(L"RASnes9X - 0.57 - User"), emulator.GetAppTitle(""));
+        emulator.mockUserContext.Initialize("User", "UserDisplay", "Token");
+        Assert::AreEqual(std::wstring(L"RASnes9X - 0.57 - UserDisplay"), emulator.GetAppTitle(""));
 
         emulator.mockUserContext.Logout();
         Assert::AreEqual(std::wstring(L"RASnes9X - 0.57"), emulator.GetAppTitle(""));
@@ -1022,9 +1022,9 @@ public:
         EmulatorContextHarness emulator;
         emulator.Initialize(EmulatorID::RA_Snes9x, nullptr);
         emulator.mockConfiguration.SetHostName("localhost");
-        emulator.mockUserContext.Initialize("User", "Token");
+        emulator.mockUserContext.Initialize("User", "UserDisplay", "Token");
         emulator.SetClientVersion("0.57");
-        Assert::AreEqual(std::wstring(L"RASnes9X - 0.57 - Test - User [localhost]"), emulator.GetAppTitle("Test"));
+        Assert::AreEqual(std::wstring(L"RASnes9X - 0.57 - Test - UserDisplay [localhost]"), emulator.GetAppTitle("Test"));
     }
 
     TEST_METHOD(TestInitializeUnknownEmulator)
diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp
index dc30ae9d..9a1c1764 100644
--- a/tests/data/context/GameContext_Tests.cpp
+++ b/tests/data/context/GameContext_Tests.cpp
@@ -1872,7 +1872,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         unsigned int nNewScore = 0U;
@@ -1946,7 +1946,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::None);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         unsigned int nNewScore = 0U;
@@ -1978,7 +1978,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         game.mockServer.ExpectUncalled<ra::api::SubmitLeaderboardEntry>();
@@ -2021,7 +2021,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest);
         game.SetGameHash("hash");
 
@@ -2066,7 +2066,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest);
         game.SetGameHash("hash");
 
@@ -2110,7 +2110,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         game.mockServer.HandleRequest<ra::api::SubmitLeaderboardEntry>([]
@@ -2173,7 +2173,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         unsigned int nNewScore = 0U;
@@ -2223,7 +2223,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         unsigned int nNewScore = 0U;
@@ -2281,7 +2281,7 @@ public:
         GameContextHarness game;
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         unsigned int nNewScore = 0U;
@@ -2344,7 +2344,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
 
         game.mockServer.ExpectUncalled<ra::api::SubmitLeaderboardEntry>();
@@ -2387,7 +2387,7 @@ public:
     {
         GameContextHarness game;
         game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight);
-        game.mockUser.Initialize("Player", "ApiToken");
+        game.mockUser.Initialize("player", "Player", "ApiToken");
         game.SetGameHash("hash");
         game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true);
 
diff --git a/tests/mocks/MockUserContext.hh b/tests/mocks/MockUserContext.hh
index 6d9c874d..7746b425 100644
--- a/tests/mocks/MockUserContext.hh
+++ b/tests/mocks/MockUserContext.hh
@@ -19,6 +19,17 @@ public:
     {
     }
 
+    using UserContext::Initialize;
+
+#pragma warning(push)           // the using (above) ensures the base implementation is not hidden, but
+#pragma warning(disable:26434)  // the analyzer stills sees this non-overriding function as having the
+                                // same name as a non-virtual function in the base class
+    void Initialize(const std::string& sUsername, const std::string& sApiToken)
+    {
+        UserContext::Initialize(sUsername, sUsername + "_", sApiToken);
+    }
+#pragma warning(pop)
+
     void Logout() noexcept override
     {
         m_sUsername.clear();
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/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index 50d679cd..e380a6a7 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -1030,7 +1030,7 @@ public:
     {
         AssetListViewModelHarness vmAssetList;
         vmAssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Core);
-        vmAssetList.mockUserContext.Initialize("Me", "ABCDEFGH");
+        vmAssetList.mockUserContext.Initialize("me", "Me", "ABCDEFGH");
 
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Ach1");
         vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Ach2");
diff --git a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
index 2dc1a11f..693a82e3 100644
--- a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp
@@ -761,7 +761,7 @@ public:
         pAch1.SetBadge(L"BADGE_URI");
         pAch1.SetState(AssetState::Active);
 
-        achievementsPage.mockUserContext.Initialize("User2", "ApiToken");
+        achievementsPage.mockUserContext.Initialize("user2", "User2", "ApiToken");
         achievementsPage.mockServer.HandleRequest<ra::api::FetchAchievementInfo>([](const ra::api::FetchAchievementInfo::Request& request, ra::api::FetchAchievementInfo::Response& response)
         {
             Assert::AreEqual(1U, request.AchievementId);
diff --git a/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp
index 25ea369a..75abc2f9 100644
--- a/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp
@@ -395,7 +395,7 @@ public:
         pLB1.SetName(L"LeaderboardTitle");
         pLB1.SetDescription(L"Trigger this");
 
-        leaderboardsPage.mockUserContext.Initialize("User2", "ApiToken");
+        leaderboardsPage.mockUserContext.Initialize("user2", "User2", "ApiToken");
         leaderboardsPage.mockServer.HandleRequest<ra::api::FetchLeaderboardInfo>([](const ra::api::FetchLeaderboardInfo::Request& request, ra::api::FetchLeaderboardInfo::Response& response)
         {
             Assert::AreEqual(1U, request.LeaderboardId);
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index f99b6ce0..e96fda36 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -429,7 +429,75 @@ public:
         Assert::IsTrue(bDialogShown);
     }
 
-    TEST_METHOD(TestPasteFromClipboardNonTrigger)
+    TEST_METHOD(TestPasteFromClipboardTrigger)
+    {
+        TriggerViewModelHarness vmTrigger;
+        Parse(vmTrigger, "A:0xH1235*100");
+        Assert::AreEqual({1U}, vmTrigger.Conditions().Count());
+
+        bool bDialogShown = false;
+        vmTrigger.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>(
+            [&bDialogShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+                bDialogShown = true;
+
+                Assert::AreEqual(std::wstring(L"Paste failed."), vmMessageBox.GetHeader());
+                Assert::AreEqual(std::wstring(L"Clipboard did not contain valid value conditions."),
+                                 vmMessageBox.GetMessage());
+
+                return DialogResult::OK;
+            });
+
+        // Triggers allow conditions without flags
+        vmTrigger.mockClipboard.SetText(L"0xH1234=7");
+        vmTrigger.PasteFromClipboard();
+
+        Assert::IsFalse(bDialogShown);
+
+        // Values do not allow conditions without flags
+        vmTrigger.SetIsValue(true);
+        bDialogShown = false;
+
+        vmTrigger.mockClipboard.SetText(L"0xH1234=7");
+        vmTrigger.PasteFromClipboard();
+
+        Assert::IsTrue(bDialogShown);
+    }
+
+    TEST_METHOD(TestPasteFromClipboardValue)
+    {
+        TriggerViewModelHarness vmTrigger;
+        Parse(vmTrigger, "A:0xH1235*100");
+        Assert::AreEqual({1U}, vmTrigger.Conditions().Count());
+
+        // Measured requires a comparison for triggers
+        bool bDialogShown = false;
+        vmTrigger.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>(
+            [&bDialogShown](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+                bDialogShown = true;
+
+                Assert::AreEqual(std::wstring(L"Paste failed."), vmMessageBox.GetHeader());
+                Assert::AreEqual(std::wstring(L"Clipboard did not contain valid trigger conditions."),
+                                 vmMessageBox.GetMessage());
+
+                return DialogResult::OK;
+            });
+
+        vmTrigger.mockClipboard.SetText(L"M:0xH1234");
+        vmTrigger.PasteFromClipboard();
+
+        Assert::IsTrue(bDialogShown);
+
+        // Measured does not require a comparison for values
+        vmTrigger.SetIsValue(true);
+        bDialogShown = false;
+
+        vmTrigger.mockClipboard.SetText(L"M:0xH1234");
+        vmTrigger.PasteFromClipboard();
+
+        Assert::IsFalse(bDialogShown);
+    }
+
+    TEST_METHOD(TestPasteFromClipboardInvalidSyntax)
     {
         TriggerViewModelHarness vmTrigger;
         Parse(vmTrigger, "0xH1234=16");
@@ -477,7 +545,7 @@ public:
         Assert::IsTrue(bDialogShown);
     }
 
-    TEST_METHOD(TestPasteFromClipboard)
+    TEST_METHOD(TestPasteFromClipboardMultipleConditions)
     {
         TriggerViewModelHarness vmTrigger;
         Parse(vmTrigger, "0xH1234=16");
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)