Repository: RetroAchievements/RAIntegration
Pull request #1070 (2024-02-17 19:54:57)

export unknown game functionality for rc_client


Patch:
diff --git a/rcheevos b/rcheevos
index c5f9a641..3d01191a 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit c5f9a6412e55ef489c1769a52ee1d590e4abd64b
+Subproject commit 3d01191ae877214454638ee6c65c2ae042b7dae3
diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp
index c165ec58..4f2b5a6c 100644
--- a/src/services/AchievementRuntime.cpp
+++ b/src/services/AchievementRuntime.cpp
@@ -1,5 +1,6 @@
 #include "AchievementRuntime.hh"
 
+#include "AchievementRuntimeExports.hh"
 #include "Exports.hh"
 #include "RA_Defs.h"
 #include "RA_Log.h"
@@ -29,6 +30,7 @@
 #include "ui\viewmodels\MessageBoxViewModel.hh"
 #include "ui\viewmodels\OverlayManager.hh"
 #include "ui\viewmodels\PopupMessageViewModel.hh"
+#include "ui\viewmodels\UnknownGameViewModel.hh"
 #include "ui\viewmodels\WindowManager.hh"
 
 #include <rcheevos\include\rc_api_runtime.h>
@@ -147,6 +149,35 @@ static int RichPresenceOverride(rc_client_t*, char buffer[], size_t buffer_size)
     return 0;
 }
 
+
+static uint32_t IdentifyUnknownHash(uint32_t console_id, const char* hash, rc_client_t*, void*)
+{
+    RA_LOG_INFO("Could not identify game with hash %s", hash);
+
+    const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
+    auto sEstimatedGameTitle = ra::Widen(pEmulatorContext.GetGameTitle());
+
+    ra::ui::viewmodels::UnknownGameViewModel vmUnknownGame;
+    vmUnknownGame.InitializeGameTitles(ra::itoe<ConsoleID>(console_id));
+    vmUnknownGame.SetSystemName(ra::Widen(rc_console_name(console_id)));
+    vmUnknownGame.SetChecksum(ra::Widen(hash));
+    vmUnknownGame.SetEstimatedGameName(sEstimatedGameTitle);
+    vmUnknownGame.SetNewGameName(sEstimatedGameTitle);
+
+    if (vmUnknownGame.ShowModal() == ra::ui::DialogResult::OK)
+    {
+        if (vmUnknownGame.GetTestMode())
+        {
+            ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>().SetMode(
+                ra::data::context::GameContext::Mode::CompatibilityTest);
+        }
+
+        return vmUnknownGame.GetSelectedGameId();
+    }
+
+    return 0;
+}
+
 AchievementRuntime::AchievementRuntime()
 {
     m_pClient.reset(rc_client_create(AchievementRuntime::ReadMemory, AchievementRuntime::ServerCallAsync));
@@ -154,6 +185,7 @@ AchievementRuntime::AchievementRuntime()
     m_pClient->callbacks.can_submit_achievement_unlock = CanSubmitAchievementUnlock;
     m_pClient->callbacks.can_submit_leaderboard_entry = CanSubmitLeaderboardEntry;
     m_pClient->callbacks.rich_presence_override = RichPresenceOverride;
+    m_pClient->callbacks.identify_unknown_hash = IdentifyUnknownHash;
 
 #ifndef RA_UTEST
     rc_client_enable_logging(m_pClient.get(), RC_CLIENT_LOG_LEVEL_VERBOSE, AchievementRuntime::LogMessage);
@@ -1138,13 +1170,21 @@ rc_client_async_handle_t* AchievementRuntime::BeginIdentifyAndLoadGame(uint32_t
 }
 
 GSL_SUPPRESS_CON3
-void AchievementRuntime::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata)
+void AchievementRuntime::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata)
 {
     auto* wrapper = static_cast<LoadGameCallbackWrapper*>(pUserdata);
     Expects(wrapper != nullptr);
 
     if (nResult == RC_OK || nResult == RC_NO_GAME_LOADED)
     {
+        // this has to be done before calling InitializeFromAchievementRuntime so the address validation
+        // doesn't flag every achievement as invalid.
+        if (IsExternalRcheevosClient() && pClient->game)
+        {
+            _RA_SetConsoleID(pClient->game->public_.console_id);
+            ResetEmulatorMemoryRegionsForRcheevosClient();
+        }
+
         // initialize the game context
         auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
         pGameContext.InitializeFromAchievementRuntime(wrapper->m_mAchievementDefinitions,
diff --git a/src/services/AchievementRuntimeExports.cpp b/src/services/AchievementRuntimeExports.cpp
index ca186d06..06501464 100644
--- a/src/services/AchievementRuntimeExports.cpp
+++ b/src/services/AchievementRuntimeExports.cpp
@@ -66,6 +66,23 @@ public:
         ResetMemory();
     }
 
+    static void ResetMemory()
+    {
+        const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+        const auto nNumBytes = pConsoleContext.MaxAddress() + 1;
+
+        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+        pEmulatorContext.ClearMemoryBlocks();
+
+        if (nNumBytes > 1)
+        {
+            pEmulatorContext.AddMemoryBlock(0, nNumBytes,
+                                            AchievementRuntimeExports::ReadMemoryByte,
+                                            AchievementRuntimeExports::WriteMemoryByte);
+            pEmulatorContext.AddMemoryBlockReader(0, AchievementRuntimeExports::ReadMemoryBlock);
+        }
+    }
+
     static void set_get_time_millisecs(rc_client_t* client, rc_get_time_millisecs_func_t handler)
     {
         s_callbacks.get_time_millisecs_handler = handler;
@@ -457,6 +474,15 @@ public:
         s_callbacks.write_memory_handler = handler;
     }
 
+    static void set_raintegration_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler)
+    {
+        s_callbacks.get_game_name_client = client;
+        s_callbacks.get_game_name_handler = handler;
+
+        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+        pEmulatorContext.SetGetGameTitleFunction(GetGameTitle);
+    }
+
     static void set_raintegration_event_handler(rc_client_t* client, rc_client_raintegration_event_handler_t handler) noexcept
     {
         s_callbacks.raintegration_event_client = client;
@@ -690,6 +716,9 @@ private:
         rc_client_raintegration_write_memory_func_t write_memory_handler;
         rc_client_t* write_memory_client;
 
+        rc_client_raintegration_get_game_name_func_t get_game_name_handler;
+        rc_client_t* get_game_name_client;
+
         rc_get_time_millisecs_func_t get_time_millisecs_handler;
         rc_client_t* get_time_millisecs_client;
 
@@ -751,17 +780,6 @@ private:
         return 0;
     }
     
-    static void ResetMemory()
-    {
-        const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
-        const auto nNumBytes = pConsoleContext.MaxAddress() + 1;
-
-        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
-        pEmulatorContext.ClearMemoryBlocks();
-        pEmulatorContext.AddMemoryBlock(0, nNumBytes, AchievementRuntimeExports::ReadMemoryByte, AchievementRuntimeExports::WriteMemoryByte);
-        pEmulatorContext.AddMemoryBlockReader(0, AchievementRuntimeExports::ReadMemoryBlock);
-    }
-
     static void RaisePauseEvent() noexcept
     {
         RaiseIntegrationEvent(RC_CLIENT_RAINTEGRATION_EVENT_PAUSE);
@@ -779,6 +797,15 @@ private:
         }
     }
 
+    static void GetGameTitle(char buffer[])
+    {
+        Expects(buffer != nullptr);
+        buffer[0] = '\0';
+
+        if (s_callbacks.get_game_name_handler)
+            s_callbacks.get_game_name_handler(buffer, 256, s_callbacks.raintegration_event_client);
+    }
+
     static rc_clock_t GetTimeMillisecsExternal(const rc_client_t*) noexcept(false)
     {
         if (s_callbacks.get_time_millisecs_handler)
@@ -831,6 +858,11 @@ bool IsExternalRcheevosClient() noexcept
     return ra::services::AchievementRuntimeExports::IsExternalRcheevosClient();
 }
 
+void ResetEmulatorMemoryRegionsForRcheevosClient()
+{
+    ra::services::AchievementRuntimeExports::ResetMemory();
+}
+
 void SyncClientExternalRAIntegrationMenuItem(int nMenuItemId)
 {
     ra::services::AchievementRuntimeExports::SyncMenuItem(nMenuItemId);
@@ -947,11 +979,36 @@ API void CCONV _Rcheevos_SetRAIntegrationWriteMemoryFunction(rc_client_t* client
     ra::services::AchievementRuntimeExports::set_raintegration_write_memory_function(client, handler);
 }
 
+API void CCONV _Rcheevos_SetRAIntegrationGetGameNameFunction(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler)
+{
+    ra::services::AchievementRuntimeExports::set_raintegration_get_game_name_function(client, handler);
+}
+
 API void CCONV _Rcheevos_SetRAIntegrationEventHandler(rc_client_t* client, rc_client_raintegration_event_handler_t handler)
 {
     ra::services::AchievementRuntimeExports::set_raintegration_event_handler(client, handler);
 }
 
+API int CCONV _Rcheevos_HasModifications(void)
+{
+    const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
+    for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(pGameContext.Assets().Count()); ++nIndex)
+    {
+        const auto* pAsset = pGameContext.Assets().GetItemAt(nIndex);
+        if (pAsset && pAsset->IsModified())
+        {
+            switch (pAsset->GetType())
+            {
+                case ra::data::models::AssetType::Achievement:
+                case ra::data::models::AssetType::Leaderboard:
+                    return true;
+            }
+        }
+    }
+
+    return false;
+}
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
diff --git a/src/services/AchievementRuntimeExports.hh b/src/services/AchievementRuntimeExports.hh
index c27e492a..77233f0e 100644
--- a/src/services/AchievementRuntimeExports.hh
+++ b/src/services/AchievementRuntimeExports.hh
@@ -6,5 +6,6 @@ void SyncClientExternalRAIntegrationMenuItem(int nMenuItemId);
 void SyncClientExternalHardcoreState();
 bool IsExternalRcheevosClient() noexcept;
 void ResetExternalRcheevosClient() noexcept;
+void ResetEmulatorMemoryRegionsForRcheevosClient();
 
 #endif // !RA_SERVICES_ACHIEVEMENT_RUNTIME_EXPORTS_HH
diff --git a/src/ui/viewmodels/UnknownGameViewModel.cpp b/src/ui/viewmodels/UnknownGameViewModel.cpp
index bb53a075..7b4b053c 100644
--- a/src/ui/viewmodels/UnknownGameViewModel.cpp
+++ b/src/ui/viewmodels/UnknownGameViewModel.cpp
@@ -7,6 +7,7 @@
 
 #include "data\context\ConsoleContext.hh"
 #include "data\context\GameContext.hh"
+#include "data\context\UserContext.hh"
 
 #include "services\IClipboard.hh"
 #include "services\ILocalStorage.hh"
@@ -34,14 +35,25 @@ UnknownGameViewModel::UnknownGameViewModel() noexcept
 void UnknownGameViewModel::InitializeGameTitles()
 {
     const auto& pConsoleContext = ra::services::ServiceLocator::Get<ra::data::context::ConsoleContext>();
+    InitializeGameTitles(pConsoleContext.Id());
+}
 
+void UnknownGameViewModel::InitializeGameTitles(ConsoleID consoleId)
+{
     m_vGameTitles.Add(0U, L"<New Title>");
 
+    if (!ra::services::ServiceLocator::Get<ra::data::context::UserContext>().IsLoggedIn())
+    {
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(*this, L"Could not retrieve list of existing games",
+                                                                  L"User is not logged in");
+        return;
+    }
+
     SetValue(IsSelectedGameEnabledProperty, false);
     SetValue(IsAssociateEnabledProperty, false);
 
     ra::api::FetchGamesList::Request request;
-    request.ConsoleId = pConsoleContext.Id();
+    request.ConsoleId = consoleId;
 
     request.CallAsync([this, pAsyncHandle = CreateAsyncHandle()](const ra::api::FetchGamesList::Response& response)
     {
diff --git a/src/ui/viewmodels/UnknownGameViewModel.hh b/src/ui/viewmodels/UnknownGameViewModel.hh
index fee43579..8fdcfe1f 100644
--- a/src/ui/viewmodels/UnknownGameViewModel.hh
+++ b/src/ui/viewmodels/UnknownGameViewModel.hh
@@ -59,6 +59,11 @@ public:
     /// <summary>
     /// Initializes the <see cref="GameTitles"/> collection (asynchronously).
     /// </summary>
+    void InitializeGameTitles(ConsoleID nConsoleId);
+
+    /// <summary>
+    /// Freezes the selected game.
+    /// </summary>
     void InitializeTestCompatibilityMode();
 
     /// <summary>
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index b2df8794..ed7d31c2 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -427,31 +427,33 @@ void AssetEditorDialog::BadgeNameBinding::UpdateSourceFromText(const std::wstrin
 
 void AssetEditorDialog::BadgeNameBinding::UpdateTextFromSource(const std::wstring& sText)
 {
-    if (ra::StringStartsWith(sText, L"local\\"))
+    InvokeOnUIThread([this, sTextCopy = sText]()
     {
-        SetWindowTextW(m_hWnd, L"[local]");
-        ::EnableWindow(m_hWndSpinner, false);
-    }
-    else
-    {
-        SetWindowTextW(m_hWnd, sText.c_str());
-
-        if (m_nMinimum == 0)
+        if (ra::StringStartsWith(sTextCopy, L"local\\"))
         {
+            SetWindowTextW(m_hWnd, L"[local]");
             ::EnableWindow(m_hWndSpinner, false);
-
-            ra::api::FetchBadgeIds::Request request;
-            request.CallAsync([this](const ra::api::FetchBadgeIds::Response& response) noexcept
-            {
-                SetRange(ra::to_signed(response.FirstID), ra::to_signed(response.NextID) - 1);
-                ::EnableWindow(m_hWndSpinner, true);
-            });
         }
         else
         {
-            ::EnableWindow(m_hWndSpinner, true);
+            SetWindowTextW(m_hWnd, sTextCopy.c_str());
+
+            if (m_nMinimum == 0)
+            {
+                ::EnableWindow(m_hWndSpinner, false);
+
+                ra::api::FetchBadgeIds::Request request;
+                request.CallAsync([this](const ra::api::FetchBadgeIds::Response& response) noexcept {
+                    SetRange(ra::to_signed(response.FirstID), ra::to_signed(response.NextID) - 1);
+                    ::EnableWindow(m_hWndSpinner, true);
+                });
+            }
+            else
+            {
+                ::EnableWindow(m_hWndSpinner, true);
+            }
         }
-    }
+    });
 }
 
 bool AssetEditorDialog::ActiveCheckBoxBinding::IsActive() const
diff --git a/src/ui/win32/bindings/ImageBinding.hh b/src/ui/win32/bindings/ImageBinding.hh
index 929dd459..d718c561 100644
--- a/src/ui/win32/bindings/ImageBinding.hh
+++ b/src/ui/win32/bindings/ImageBinding.hh
@@ -81,15 +81,17 @@ private:
 
     void UpdateImage()
     {
-        GSL_SUPPRESS_CON4 // inline suppression for this currently not working
-        {
-            const auto hBitmap = ra::ui::drawing::gdi::ImageRepository::GetHBitmap(m_pImageReference);
+        InvokeOnUIThread([this]() {
+            GSL_SUPPRESS_CON4 // inline suppression for this currently not working
+            {
+                const auto hBitmap = ra::ui::drawing::gdi::ImageRepository::GetHBitmap(m_pImageReference);
 
-            GSL_SUPPRESS_TYPE1
-            SendMessage(m_hWnd, STM_SETIMAGE, ra::to_unsigned(IMAGE_BITMAP), reinterpret_cast<LPARAM>(hBitmap));
-        }
+                GSL_SUPPRESS_TYPE1
+                SendMessage(m_hWnd, STM_SETIMAGE, ra::to_unsigned(IMAGE_BITMAP), reinterpret_cast<LPARAM>(hBitmap));
+            }
 
-        ControlBinding::ForceRepaint(m_hWnd);
+            ControlBinding::ForceRepaint(m_hWnd);
+        });
     }
 
     const StringModelProperty* m_pImageBoundProperty = nullptr;


Test patch:
diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp
index 33e986ed..68f52d7b 100644
--- a/tests/data/context/GameContext_Tests.cpp
+++ b/tests/data/context/GameContext_Tests.cpp
@@ -287,6 +287,7 @@ public:
 
         game.LoadGame(1U, "0123456789abcdeffedcba987654321");
 
+        Assert::IsTrue(bDialogShown);
         Assert::AreEqual(0U, game.GameId());
         Assert::AreEqual(ra::data::context::GameContext::Mode::Normal, game.GetMode());
         Assert::AreEqual(std::wstring(L""), game.GameTitle());
diff --git a/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp b/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
index d73f1432..6710b9af 100644
--- a/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/UnknownGameViewModel_Tests.cpp
@@ -11,6 +11,7 @@
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockServer.hh"
 #include "tests\mocks\MockThreadPool.hh"
+#include "tests\mocks\MockUserContext.hh"
 
 #include "tests\ui\UIAsserts.hh"
 
@@ -30,10 +31,16 @@ private:
         ra::api::mocks::MockServer mockServer;
         ra::data::context::mocks::MockConsoleContext mockConsoleContext;
         ra::data::context::mocks::MockGameContext mockGameContext;
+        ra::data::context::mocks::MockUserContext mockUserContext;
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
         ra::ui::mocks::MockDesktop mockDesktop;
 
+        UnknownGameViewModelHarness()
+        {
+            mockUserContext.Initialize("User", "ApiToken");
+        }
+
         void MockGameTitles()
         {
             m_vGameTitles.Add(20, L"Game 20");