diff --git a/rcheevos b/rcheevos index 64fcb9ea..48942653 160000 --- a/rcheevos +++ b/rcheevos @@ -1 +1 @@ -Subproject commit 64fcb9ea3cf990e65343057ace9271ff3b77428e +Subproject commit 489426539757de6106a4d0d34d3495f5922f62df diff --git a/src/Exports.cpp b/src/Exports.cpp index 636788b0..e600b511 100644 --- a/src/Exports.cpp +++ b/src/Exports.cpp @@ -15,6 +15,7 @@ #include "data\context\UserContext.hh" #include "services\AchievementRuntime.hh" +#include "services\AchievementRuntimeExports.hh" #include "services\FrameEventQueue.hh" #include "services\GameIdentifier.hh" #include "services\Http.hh" @@ -79,8 +80,11 @@ API void CCONV _RA_UpdateHWnd(HWND hMainHWND) { pDesktop.SetMainHWnd(hMainHWND); - auto& pOverlayWindow = ra::services::ServiceLocator::GetMutable(); - pOverlayWindow.CreateOverlayWindow(hMainHWND); + if (!IsExternalRcheevosClient()) + { + auto& pOverlayWindow = ra::services::ServiceLocator::GetMutable(); + pOverlayWindow.CreateOverlayWindow(hMainHWND); + } } } #endif diff --git a/src/RA_Core.cpp b/src/RA_Core.cpp index 55ca0722..c07304a2 100644 --- a/src/RA_Core.cpp +++ b/src/RA_Core.cpp @@ -95,7 +95,7 @@ void __gsl_contract_handler(const char* const file, unsigned int line, const cha } #endif -API int CCONV _RA_Shutdown() +static int DoShutdown() { // if the services haven't been registered, there's nothing to shut down if (!ra::services::Initialization::IsInitialized()) @@ -155,6 +155,20 @@ API int CCONV _RA_Shutdown() return 0; } +API int CCONV _RA_Shutdown() +{ + try + { + return DoShutdown(); + } + catch (std::runtime_error&) + { + + } + + return 0; +} + API int CCONV _RA_ConfirmLoadNewRom(int bQuittingApp) { // Returns true if we can go ahead and load the new rom. diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj index 88dc51fa..0a466692 100644 --- a/src/RA_Integration.vcxproj +++ b/src/RA_Integration.vcxproj @@ -86,6 +86,7 @@ + @@ -231,6 +232,7 @@ + diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters index da4ec9ba..a9d0b890 100644 --- a/src/RA_Integration.vcxproj.filters +++ b/src/RA_Integration.vcxproj.filters @@ -405,6 +405,9 @@ UI\ViewModels + + Services + @@ -968,6 +971,9 @@ UI\ViewModels + + Services + diff --git a/src/data/context/EmulatorContext.cpp b/src/data/context/EmulatorContext.cpp index 8cff7dc0..5f0f62c5 100644 --- a/src/data/context/EmulatorContext.cpp +++ b/src/data/context/EmulatorContext.cpp @@ -3,6 +3,7 @@ #include "Exports.hh" #include "RA_BuildVer.h" #include "RA_Log.h" +#include "RA_Resource.h" #include "RA_StringUtils.h" #include "api\LatestClient.hh" @@ -11,6 +12,7 @@ #include "data\context\UserContext.hh" #include "services\AchievementRuntime.hh" +#include "services\AchievementRuntimeExports.hh" #include "services\IClock.hh" #include "services\IConfiguration.hh" #include "services\IFileSystem.hh" @@ -456,10 +458,9 @@ void EmulatorContext::DisableHardcoreMode() auto& pWindowManager = ra::services::ServiceLocator::GetMutable(); pWindowManager.AssetList.UpdateButtons(); - RebuildMenu(); + SyncClientExternalHardcoreState(); - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - rc_client_set_hardcore_enabled(pRuntime.GetClient(), false); + UpdateMenuState(IDM_RA_HARDCORE_MODE); // GameContext::DoFrame synchronizes the models to the runtime auto& pGameContext = ra::services::ServiceLocator::GetMutable(); @@ -532,12 +533,11 @@ bool EmulatorContext::EnableHardcoreMode(bool bShowWarning) // updating the enabled-ness of the buttons of the asset list pWindowManager.AssetList.UpdateButtons(); - // update the integration menu - RebuildMenu(); - // update the runtime - auto* pClient = ra::services::ServiceLocator::GetMutable().GetClient(); - rc_client_set_hardcore_enabled(pClient, true); + SyncClientExternalHardcoreState(); + + // update the integration menu + UpdateMenuState(IDM_RA_HARDCORE_MODE); // GameContext::DoFrame synchronizes the models to the runtime pGameContext.DoFrame(); @@ -1027,6 +1027,13 @@ bool EmulatorContext::IsMemoryInsecure() const return m_bMemoryInsecure; } +void EmulatorContext::UpdateMenuState(int nMenuItemId) const +{ + SyncClientExternalRAIntegrationMenuItem(nMenuItemId); + + RebuildMenu(); +} + } // namespace context } // namespace data } // namespace ra diff --git a/src/data/context/EmulatorContext.hh b/src/data/context/EmulatorContext.hh index 4f5dd5d2..d5ddef52 100644 --- a/src/data/context/EmulatorContext.hh +++ b/src/data/context/EmulatorContext.hh @@ -115,6 +115,12 @@ public: m_fRebuildMenu(); } + /// + /// Updates the state of a single menu item. + /// + /// Indirectly calls RebuildMenu after raising an event for the singular menu item + void UpdateMenuState(int nMenuItemId) const; + /// /// Gets whether or not the emulator can be paused. /// diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp index 68f422bf..eda226c2 100644 --- a/src/data/context/GameContext.cpp +++ b/src/data/context/GameContext.cpp @@ -86,7 +86,7 @@ static bool ValidateConsole(int nServerConsoleId) return true; } -void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, Mode nMode) +bool GameContext::BeginLoadGame(unsigned int nGameId, Mode nMode, bool& bWasPaused) { OnBeforeActiveGameChanged(); @@ -119,7 +119,7 @@ void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, M m_nGameId = 0; OnActiveGameChanged(); } - return; + return false; } // start the load process @@ -132,45 +132,39 @@ void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, M pLocalBadges->CreateLocalCheckpoint(); m_vAssets.Append(std::move(pLocalBadges)); - // capture the old rich presence data so we can tell if it changed on the server - std::string sOldRichPresence = "[NONE]"; - { - auto& pLocalStorage = ra::services::ServiceLocator::GetMutable(); - auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId)); - if (pData != nullptr) - { - rapidjson::Document pDocument; - if (LoadDocument(pDocument, *pData) && pDocument.HasMember("RichPresencePatch") && - pDocument["RichPresencePatch"].IsString()) - sOldRichPresence = pDocument["RichPresencePatch"].GetString(); - } - } - - const bool bWasPaused = pRuntime.IsPaused(); + bWasPaused = pRuntime.IsPaused(); pRuntime.SetPaused(true); + return true; +} + +void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, Mode nMode) +{ + bool bWasPaused; + + if (!BeginLoadGame(nGameId, nMode, bWasPaused)) + return; + // download the game data struct LoadGameUserData { bool bWasPaused = false; - std::string sOldRichPresence; }* pLoadGameUserData; pLoadGameUserData = new LoadGameUserData; pLoadGameUserData->bWasPaused = bWasPaused; - pLoadGameUserData->sOldRichPresence = std::move(sOldRichPresence); + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); pRuntime.BeginLoadGame(sGameHash, nGameId, [](int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) { auto& pGameContext = ra::services::ServiceLocator::GetMutable(); auto* pLoadGameUserData = static_cast(pUserdata); - pGameContext.FinishLoadGame(nResult, sErrorMessage, pLoadGameUserData->bWasPaused, - pLoadGameUserData->sOldRichPresence); + pGameContext.FinishLoadGame(nResult, sErrorMessage, pLoadGameUserData->bWasPaused); delete pLoadGameUserData; }, pLoadGameUserData); } -void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused, const std::string& sOldRichPresence) +void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused) { if (nResult != RC_OK) { @@ -181,26 +175,22 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW } else { - BeginLoad(); - auto pCodeNotes = std::make_unique(); - pCodeNotes->Refresh( - m_nGameId, - [this](ra::ByteAddress nAddress, const std::wstring& sNewNote) { - OnCodeNoteChanged(nAddress, sNewNote); - }, - [this]() { EndLoad(); }); - m_vAssets.Append(std::move(pCodeNotes)); - auto& pClient = ra::services::ServiceLocator::GetMutable(); auto* pGame = rc_client_get_game_info(pClient.GetClient()); if (pGame == nullptr || pGame->id == 0) { // invalid hash + m_nGameId = 0; + m_sGameTitle.clear(); + m_sGameHash.clear(); + nResult = RC_NO_GAME_LOADED; } else if (!ValidateConsole(pGame->console_id)) { m_nGameId = 0; m_sGameTitle.clear(); + m_sGameHash.clear(); + nResult = RC_INVALID_STATE; } else { @@ -218,13 +208,42 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW ra::StringPrintf(L"Loaded %s", pGame->title), sDescription, ra::StringPrintf(L"You have earned %u achievements", pSummary.num_unlocked_achievements), ra::ui::ImageType::Icon, pGame->badge_name); - - // merge local assets - std::vector vEmptyAssetsList; - m_vAssets.ReloadAssets(vEmptyAssetsList); } } + EndLoadGame(nResult, bWasPaused, true); +} + +void GameContext::EndLoadGame(int nResult, bool bWasPaused, bool bShowSoftcoreWarning) +{ + ra::data::models::RichPresenceModel* pRichPresence = nullptr; + std::string sOldRichPresence; + + if (nResult == RC_OK && m_nGameId > 0) { + BeginLoad(); + + auto pCodeNotes = std::make_unique(); + pCodeNotes->Refresh(m_nGameId, + [this](ra::ByteAddress nAddress, const std::wstring& sNewNote) { + OnCodeNoteChanged(nAddress, sNewNote); + }, + [this]() { + EndLoad(); + }); + + m_vAssets.Append(std::move(pCodeNotes)); + + // the old server value (if different from current server value) will be stored as Local modification. + // capture it now. ReloadAssets will load the XXX-Rich.txt file and replace it + pRichPresence = m_vAssets.FindRichPresence(); + if (pRichPresence) + sOldRichPresence = pRichPresence->GetScript(); + + // merge local assets + std::vector vEmptyAssetsList; + m_vAssets.ReloadAssets(vEmptyAssetsList); + } + if (!bWasPaused) { ra::services::ServiceLocator::GetMutable().SetPaused(false); @@ -235,7 +254,6 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW } // activate rich presence (or remove if not defined) - auto* pRichPresence = m_vAssets.FindRichPresence(); if (pRichPresence && nResult == RC_OK) { // if the server value differs from the local value, the model will appear as Unpublished @@ -264,15 +282,20 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW } // modified assets should start in the inactive state + size_t nLocalAssets = 0; for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vAssets.Count()); ++nIndex) { auto* pAsset = m_vAssets.GetItemAt(nIndex); if (pAsset != nullptr && pAsset->GetChanges() != ra::data::models::AssetChanges::None) { + if (pAsset->HasUnpublishedChanges()) + ++nLocalAssets; + if (pAsset->IsActive()) { if (pAsset->GetType() == ra::data::models::AssetType::RichPresence && - ra::services::ServiceLocator::Get().RichPresenceMonitor.IsVisible()) + ra::services::ServiceLocator::Get() + .RichPresenceMonitor.IsVisible()) { // if rich presence monitor is open, allow modified rich presence to remain active. otherwise, // it will be activated when the monitor is opened. it cannot be activated from the list. @@ -283,6 +306,10 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW } } } + if (nLocalAssets > 0) + { + RA_LOG_INFO("%d unpublished assets loaded", nLocalAssets); + } // finish up m_vAssets.EndUpdate(); @@ -302,18 +329,17 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW if (bShowHardcorePrompt) { - ra::services::ServiceLocator::GetMutable().QueueFunction([]() { - ra::ui::viewmodels::MessageBoxViewModel vmWarning; - vmWarning.SetHeader(L"Enable Hardcore mode?"); - vmWarning.SetMessage(L"You are loading a game with achievements and do not currently have hardcore mode enabled."); - vmWarning.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning); - vmWarning.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo); - - if (vmWarning.ShowModal() == ra::ui::DialogResult::Yes) - ra::services::ServiceLocator::GetMutable().EnableHardcoreMode(false); - }); + ra::ui::viewmodels::MessageBoxViewModel vmWarning; + vmWarning.SetHeader(L"Enable Hardcore mode?"); + vmWarning.SetMessage( + L"You are loading a game with achievements and do not currently have hardcore mode enabled."); + vmWarning.SetIcon(ra::ui::viewmodels::MessageBoxViewModel::Icon::Warning); + vmWarning.SetButtons(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo); + + if (vmWarning.ShowModal() == ra::ui::DialogResult::Yes) + ra::services::ServiceLocator::GetMutable().EnableHardcoreMode(false); } - else + else if (bShowSoftcoreWarning) { const bool bLeaderboardsEnabled = pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards); @@ -332,7 +358,9 @@ void GameContext::InitializeFromAchievementRuntime(const std::map().GetClient(); const auto* pGame = rc_client_get_game_info(pClient); + m_nGameId = pGame->id; m_sGameTitle = ra::Widen(pGame->title); + m_sGameHash = pGame->hash ? pGame->hash : ""; #ifndef RA_UTEST auto& pImageRepository = ra::services::ServiceLocator::GetMutable(); @@ -392,6 +420,9 @@ void GameContext::InitializeFromAchievementRuntime(const std::mappublic_.badge_name); + + if (!pAchievementData->public_.unlocked) + pImageRepository.FetchImage(ra::ui::ImageType::Badge, std::string(pAchievementData->public_.badge_name) + "_lock"); #endif } } diff --git a/src/data/context/GameContext.hh b/src/data/context/GameContext.hh index ccc1cc17..464ddc41 100644 --- a/src/data/context/GameContext.hh +++ b/src/data/context/GameContext.hh @@ -7,6 +7,8 @@ #include #include +namespace ra { namespace services { class AchievementRuntimeExports; } } + namespace ra { namespace data { namespace context { @@ -117,7 +119,11 @@ public: private: using NotifyTargetSet = std::set; - void FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused, const std::string& sOldRichPresence); + void FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused); + + friend class ra::services::AchievementRuntimeExports; + bool BeginLoadGame(unsigned int nGameId, Mode nMode, bool& bWasPaused); + void EndLoadGame(int nResult, bool bWasPaused, bool bShowSoftcoreWarning); protected: void OnBeforeActiveGameChanged(); @@ -129,7 +135,6 @@ protected: unsigned int m_nGameId = 0; std::wstring m_sGameTitle; std::string m_sGameHash; - std::string m_sGameImage; Mode m_nMode{}; private: diff --git a/src/rcheevos.vcxproj b/src/rcheevos.vcxproj index 36ee58dc..11e76cfa 100644 --- a/src/rcheevos.vcxproj +++ b/src/rcheevos.vcxproj @@ -147,6 +147,8 @@ + + @@ -162,6 +164,7 @@ + diff --git a/src/rcheevos.vcxproj.filters b/src/rcheevos.vcxproj.filters index c312b230..b94b7f00 100644 --- a/src/rcheevos.vcxproj.filters +++ b/src/rcheevos.vcxproj.filters @@ -161,5 +161,8 @@ rcheevos + + + \ No newline at end of file diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp index 11c1ae62..3002ce8d 100644 --- a/src/services/AchievementRuntime.cpp +++ b/src/services/AchievementRuntime.cpp @@ -3,6 +3,7 @@ #include "Exports.hh" #include "RA_Defs.h" #include "RA_Log.h" +#include "RA_Resource.h" #include "RA_StringUtils.h" #include "RA_md5factory.h" @@ -22,12 +23,15 @@ #include "services\ServiceLocator.hh" #include "services\impl\JsonFileConfiguration.hh" +#include "ui\viewmodels\IntegrationMenuViewModel.hh" +#include "ui\viewmodels\LoginViewModel.hh" #include "ui\viewmodels\MessageBoxViewModel.hh" #include "ui\viewmodels\OverlayManager.hh" #include "ui\viewmodels\PopupMessageViewModel.hh" #include "ui\viewmodels\WindowManager.hh" #include +#include #include // for parsing cached patchdata response #include #include @@ -880,6 +884,8 @@ void AchievementRuntime::LoginCallback(int nResult, const char* sErrorMessage, r { pUserContext.Initialize(user->username, user->display_name, user->token); pUserContext.SetScore(user->score); + + ra::ui::viewmodels::LoginViewModel::PostLoginInitialization(); } } } @@ -927,7 +933,7 @@ static void ExtractPatchData(const rc_api_server_response_t* server_response, ui GSL_SUPPRESS_CON3 void AchievementRuntime::PostProcessGameDataResponse(const rc_api_server_response_t* server_response, struct rc_api_fetch_game_data_response_t* game_data_response, - rc_client_t* client, void* pUserdata) + rc_client_t*, void* pUserdata) { auto* wrapper = static_cast(pUserdata); Expects(wrapper != nullptr); @@ -937,18 +943,29 @@ void AchievementRuntime::PostProcessGameDataResponse(const rc_api_server_respons auto pRichPresence = std::make_unique(); pRichPresence->SetScript(game_data_response->rich_presence_script); pRichPresence->CreateServerCheckpoint(); + + // extract the old rich presence script from the last cached server response so we can tell if the + // local value has changed. store it as the local value, and we'll merge in the real local value later. + auto& pLocalStorage = ra::services::ServiceLocator::GetMutable(); + auto pData = pLocalStorage.ReadText(ra::services::StorageItemType::GameData, std::to_wstring(game_data_response->id)); + if (pData != nullptr) + { + rapidjson::Document pDocument; + if (LoadDocument(pDocument, *pData) && + pDocument.HasMember("RichPresencePatch") && + pDocument["RichPresencePatch"].IsString()) + { + pRichPresence->SetScript(pDocument["RichPresencePatch"].GetString()); + } + } + pRichPresence->CreateLocalCheckpoint(); pGameContext.Assets().Append(std::move(pRichPresence)); #ifndef RA_UTEST // prefetch the game icon - if (client->game) - { - auto& pImageRepository = ra::services::ServiceLocator::GetMutable(); - pImageRepository.FetchImage(ra::ui::ImageType::Icon, client->game->public_.badge_name); - } -#else - (void*)client; + auto& pImageRepository = ra::services::ServiceLocator::GetMutable(); + pImageRepository.FetchImage(ra::ui::ImageType::Icon, game_data_response->image_name); #endif const rc_api_achievement_definition_t* pAchievement = game_data_response->achievements; @@ -2323,464 +2340,6 @@ int AchievementRuntime::SaveProgressToBuffer(uint8_t* pBuffer, int nBufferSize) return nSize; } -/* ---- Exports ----- */ - -#ifdef RC_CLIENT_EXPORTS_EXTERNAL - -class AchievementRuntimeExports : private AchievementRuntime -{ -public: - static void destroy() noexcept - { - memset(&s_callbacks, 0, sizeof(s_callbacks)); - } - - static void enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) - { - s_callbacks.log_callback = callback; - s_callbacks.log_client = client; - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_enable_logging(pClient.GetClient(), level, AchievementRuntimeExports::LogMessageExternal); - } - - static void set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) - { - s_callbacks.event_handler = handler; - s_callbacks.event_client = client; - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_event_handler(pClient.GetClient(), AchievementRuntimeExports::EventHandlerExternal); - } - - static void set_read_memory(rc_client_t* client, rc_client_read_memory_func_t handler) - { - s_callbacks.read_memory_handler = handler; - s_callbacks.read_memory_client = client; - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_read_memory_function(pClient.GetClient(), AchievementRuntimeExports::ReadMemoryExternal); - - auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); - pEmulatorContext.ClearMemoryBlocks(); - pEmulatorContext.AddMemoryBlock(0, 0xFFFFFFFF, nullptr, nullptr); - 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; - s_callbacks.get_time_millisecs_client = client; - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_get_time_millisecs_function(pClient.GetClient(), AchievementRuntimeExports::GetTimeMillisecsExternal); - } - - static void set_host(const char* value) - { - auto* pConfiguration = dynamic_cast( - &ra::services::ServiceLocator::GetMutable()); - if (pConfiguration != nullptr) - pConfiguration->SetHost(value); - } - - static void set_hardcore_enabled(int value) - { - auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); - if (value) - pEmulatorContext.EnableHardcoreMode(false); - else - pEmulatorContext.DisableHardcoreMode(); - } - - static int get_hardcore_enabled() - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - return pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore); - } - - static void set_unofficial_enabled(int value) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_unofficial_enabled(pClient.GetClient(), value); - } - - static int get_unofficial_enabled() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_unofficial_enabled(pClient.GetClient()); - } - - static void set_encore_mode_enabled(int value) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_encore_mode_enabled(pClient.GetClient(), value); - } - - static int get_encore_mode_enabled() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_encore_mode_enabled(pClient.GetClient()); - } - - static void set_spectator_mode_enabled(int value) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - rc_client_set_spectator_mode_enabled(pClient.GetClient(), value); - } - - static int get_spectator_mode_enabled() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_spectator_mode_enabled(pClient.GetClient()); - } - - static void abort_async(rc_client_async_handle_t* handle) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - rc_client_abort_async(pClient.GetClient(), handle); - } - - static rc_client_async_handle_t* begin_login_with_password(rc_client_t* client, const char* username, - const char* password, rc_client_callback_t callback, - void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginLoginWithPassword(username, password, pCallbackData); - } - - static rc_client_async_handle_t* begin_login_with_token(rc_client_t* client, const char* username, - const char* token, rc_client_callback_t callback, - void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackWrapper = new LoadGameCallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginLoginWithToken(username, token, pCallbackWrapper); - } - - static const rc_client_user_t* get_user_info() - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_get_user_info(pClient.GetClient()); - } - - static void logout() - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_logout(pClient.GetClient()); - } - - static rc_client_async_handle_t* begin_identify_and_load_game(rc_client_t* client, uint32_t console_id, - const char* file_path, const uint8_t* data, - size_t data_size, rc_client_callback_t callback, - void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackWrapper = new LoadGameCallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginIdentifyAndLoadGame(console_id, file_path, data, data_size, pCallbackWrapper); - } - - static rc_client_async_handle_t* begin_load_game(rc_client_t* client, const char* hash, - rc_client_callback_t callback, void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginLoadGame(hash, 0, pCallbackData); - } - - static void unload_game() - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_unload_game(pClient.GetClient()); - } - - static const rc_client_game_t* get_game_info() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_game_info(pClient.GetClient()); - } - - static const rc_client_subset_t* get_subset_info(uint32_t subset_id) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_subset_info(pClient.GetClient(), subset_id); - } - - static void get_user_game_summary(rc_client_user_game_summary_t* summary) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_user_game_summary(pClient.GetClient(), summary); - } - - static rc_client_async_handle_t* begin_change_media(rc_client_t* client, const char* file_path, - const uint8_t* data, size_t data_size, - rc_client_callback_t callback, void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginChangeMedia(file_path, data, data_size, pCallbackData); - } - - static rc_client_achievement_list_info_t* create_achievement_list(int category, int grouping) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - GSL_SUPPRESS_TYPE1 - auto* list = reinterpret_cast( - rc_client_create_achievement_list(pClient.GetClient(), category, grouping)); - list->destroy_func = destroy_achievement_list; - return list; - } - - static int has_achievements() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_has_achievements(pClient.GetClient()); - } - - static const rc_client_achievement_t* get_achievement_info(uint32_t id) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_achievement_info(pClient.GetClient(), id); - } - - static rc_client_leaderboard_list_info_t* create_leaderboard_list(int grouping) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - GSL_SUPPRESS_TYPE1 - auto* list = reinterpret_cast( - rc_client_create_leaderboard_list(pClient.GetClient(), grouping)); - list->destroy_func = destroy_leaderboard_list; - return list; - } - - static int has_leaderboards() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_has_leaderboards(pClient.GetClient()); - } - - static const rc_client_leaderboard_t* get_leaderboard_info(uint32_t id) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_leaderboard_info(pClient.GetClient(), id); - } - -private: - class LeaderboardEntriesListCallbackWrapper - { - public: - LeaderboardEntriesListCallbackWrapper(rc_client_t* client, - rc_client_fetch_leaderboard_entries_callback_t callback, - void* callback_userdata) noexcept : - m_pClient(client), m_fCallback(callback), m_pCallbackUserdata(callback_userdata) - {} - - void DoCallback(int nResult, rc_client_leaderboard_entry_list_t* pList, const char* sErrorMessage) noexcept - { - m_fCallback(nResult, sErrorMessage, pList, m_pClient, m_pCallbackUserdata); - } - - static void Dispatch(int nResult, const char* sErrorMessage, rc_client_leaderboard_entry_list_t* pList, - rc_client_t*, void* pUserdata) - { - auto* wrapper = static_cast(pUserdata); - Expects(wrapper != nullptr); - - if (pList) - { - GSL_SUPPRESS_TYPE1 - reinterpret_cast(pList)->destroy_func = destroy_leaderboard_entry_list; - } - - wrapper->DoCallback(nResult, pList, sErrorMessage); - - delete wrapper; - } - - private: - rc_client_t* m_pClient; - rc_client_fetch_leaderboard_entries_callback_t m_fCallback; - void* m_pCallbackUserdata; - }; - -public: - static rc_client_async_handle_t* begin_fetch_leaderboard_entries(rc_client_t* client, - uint32_t leaderboard_id, uint32_t first_entry, uint32_t count, - rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackData = new LeaderboardEntriesListCallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_begin_fetch_leaderboard_entries(pClient.GetClient(), leaderboard_id, first_entry, count, - LeaderboardEntriesListCallbackWrapper::Dispatch, pCallbackData); - } - - static rc_client_async_handle_t* begin_fetch_leaderboard_entries_around_user(rc_client_t* client, - uint32_t leaderboard_id, uint32_t count, - rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) - { - GSL_SUPPRESS_R3 - auto* pCallbackData = new LeaderboardEntriesListCallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_begin_fetch_leaderboard_entries_around_user(pClient.GetClient(), leaderboard_id, count, - LeaderboardEntriesListCallbackWrapper::Dispatch, pCallbackData); - } - - static size_t get_rich_presence_message(char buffer[], size_t buffer_size) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_get_rich_presence_message(pClient.GetClient(), buffer, buffer_size); - } - - static int has_rich_presence() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_has_rich_presence(pClient.GetClient()); - } - - static void do_frame() noexcept - { - _RA_DoAchievementsFrame(); - } - - static void idle() - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_idle(pClient.GetClient()); - } - - static int is_processing_required() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_is_processing_required(pClient.GetClient()); - } - - static int can_pause(uint32_t* frames_remaining) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_can_pause(pClient.GetClient(), frames_remaining); - } - - static void reset() noexcept - { -#ifndef RA_UTEST - _RA_OnReset(); -#endif - } - - static size_t progress_size() - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_progress_size(pClient.GetClient()); - } - - static int serialize_progress(uint8_t* buffer) - { - const auto& pClient = ra::services::ServiceLocator::Get(); - return rc_client_serialize_progress(pClient.GetClient(), buffer); - } - - static int deserialize_progress(const uint8_t* buffer) - { - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return rc_client_deserialize_progress(pClient.GetClient(), buffer); - } - -private: - typedef struct ExternalClientCallbacks - { - rc_client_message_callback_t log_callback; - rc_client_t* log_client; - - rc_client_event_handler_t event_handler; - rc_client_t* event_client; - - rc_client_read_memory_func_t read_memory_handler; - rc_client_t* read_memory_client; - - rc_get_time_millisecs_func_t get_time_millisecs_handler; - rc_client_t* get_time_millisecs_client; - - } ExternalClientCallbacks; - - static ExternalClientCallbacks s_callbacks; - - static void LogMessageExternal(const char* sMessage, const rc_client_t*) - { - const auto& pLogger = ra::services::ServiceLocator::Get(); - if (pLogger.IsEnabled(ra::services::LogLevel::Info)) - pLogger.LogMessage(ra::services::LogLevel::Info, sMessage); - - if (s_callbacks.log_callback) - s_callbacks.log_callback(sMessage, s_callbacks.log_client); - } - - static void EventHandlerExternal(const rc_client_event_t* event, rc_client_t*) noexcept(false) - { - if (s_callbacks.event_handler) - s_callbacks.event_handler(event, s_callbacks.event_client); - } - - static uint32_t ReadMemoryBlock(uint32_t address, uint8_t* buffer, uint32_t num_bytes) noexcept(false) - { - if (s_callbacks.read_memory_handler) - return s_callbacks.read_memory_handler(address, buffer, num_bytes, s_callbacks.read_memory_client); - - return 0; - } - - static uint32_t ReadMemoryExternal(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t*) noexcept(false) - { - if (s_callbacks.read_memory_handler) - return s_callbacks.read_memory_handler(address, buffer, num_bytes, s_callbacks.read_memory_client); - - return 0; - } - - static rc_clock_t GetTimeMillisecsExternal(const rc_client_t*) noexcept(false) - { - if (s_callbacks.get_time_millisecs_handler) - return s_callbacks.get_time_millisecs_handler(s_callbacks.get_time_millisecs_client); - - return 0; - } - - static void destroy_achievement_list(rc_client_achievement_list_info_t* list) noexcept - { - if (list) - free(list); - } - - static void destroy_leaderboard_list(rc_client_leaderboard_list_info_t* list) noexcept - { - if (list) - free(list); - } - - static void destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_info_t* list) noexcept - { - if (list) - free(list); - } -}; - -AchievementRuntimeExports::ExternalClientCallbacks AchievementRuntimeExports::s_callbacks{}; - -#endif RC_CLIENT_EXPORTS_EXTERNAL - } // namespace services } // namespace ra @@ -2799,93 +2358,3 @@ extern "C" unsigned int rc_peek_callback(unsigned int nAddress, unsigned int nBy return 0U; } } - -#ifdef RC_CLIENT_EXPORTS_EXTERNAL - -#include "Exports.hh" -#include "rcheevos/src/rc_client_external.h" - -#ifdef __cplusplus -extern "C" { -#endif - -static void GetExternalClientV1(rc_client_external_t* pClientExternal) -{ - pClientExternal->destroy = ra::services::AchievementRuntimeExports::destroy; - - pClientExternal->enable_logging = ra::services::AchievementRuntimeExports::enable_logging; - pClientExternal->set_event_handler = ra::services::AchievementRuntimeExports::set_event_handler; - pClientExternal->set_read_memory = ra::services::AchievementRuntimeExports::set_read_memory; - pClientExternal->set_get_time_millisecs = ra::services::AchievementRuntimeExports::set_get_time_millisecs; - pClientExternal->set_host = ra::services::AchievementRuntimeExports::set_host; - - pClientExternal->set_hardcore_enabled = ra::services::AchievementRuntimeExports::set_hardcore_enabled; - pClientExternal->get_hardcore_enabled = ra::services::AchievementRuntimeExports::get_hardcore_enabled; - pClientExternal->set_unofficial_enabled = ra::services::AchievementRuntimeExports::set_unofficial_enabled; - pClientExternal->get_unofficial_enabled = ra::services::AchievementRuntimeExports::get_unofficial_enabled; - pClientExternal->set_encore_mode_enabled = ra::services::AchievementRuntimeExports::set_encore_mode_enabled; - pClientExternal->get_encore_mode_enabled = ra::services::AchievementRuntimeExports::get_encore_mode_enabled; - pClientExternal->set_spectator_mode_enabled = ra::services::AchievementRuntimeExports::set_spectator_mode_enabled; - pClientExternal->get_spectator_mode_enabled = ra::services::AchievementRuntimeExports::get_spectator_mode_enabled; - - pClientExternal->abort_async = ra::services::AchievementRuntimeExports::abort_async; - - pClientExternal->begin_login_with_password = ra::services::AchievementRuntimeExports::begin_login_with_password; - pClientExternal->begin_login_with_token = ra::services::AchievementRuntimeExports::begin_login_with_token; - pClientExternal->logout = ra::services::AchievementRuntimeExports::logout; - pClientExternal->get_user_info = ra::services::AchievementRuntimeExports::get_user_info; - - pClientExternal->begin_identify_and_load_game = ra::services::AchievementRuntimeExports::begin_identify_and_load_game; - pClientExternal->begin_load_game = ra::services::AchievementRuntimeExports::begin_load_game; - pClientExternal->get_game_info = ra::services::AchievementRuntimeExports::get_game_info; - pClientExternal->get_subset_info = ra::services::AchievementRuntimeExports::get_subset_info; - pClientExternal->unload_game = ra::services::AchievementRuntimeExports::unload_game; - pClientExternal->get_user_game_summary = ra::services::AchievementRuntimeExports::get_user_game_summary; - pClientExternal->begin_change_media = ra::services::AchievementRuntimeExports::begin_change_media; - - pClientExternal->create_achievement_list = ra::services::AchievementRuntimeExports::create_achievement_list; - pClientExternal->has_achievements = ra::services::AchievementRuntimeExports::has_achievements; - pClientExternal->get_achievement_info = ra::services::AchievementRuntimeExports::get_achievement_info; - - pClientExternal->create_leaderboard_list = ra::services::AchievementRuntimeExports::create_leaderboard_list; - pClientExternal->has_leaderboards = ra::services::AchievementRuntimeExports::has_leaderboards; - pClientExternal->get_leaderboard_info = ra::services::AchievementRuntimeExports::get_leaderboard_info; - pClientExternal->begin_fetch_leaderboard_entries = ra::services::AchievementRuntimeExports::begin_fetch_leaderboard_entries; - pClientExternal->begin_fetch_leaderboard_entries_around_user = - ra::services::AchievementRuntimeExports::begin_fetch_leaderboard_entries_around_user; - - pClientExternal->get_rich_presence_message = ra::services::AchievementRuntimeExports::get_rich_presence_message; - pClientExternal->has_rich_presence = ra::services::AchievementRuntimeExports::has_rich_presence; - - pClientExternal->do_frame = ra::services::AchievementRuntimeExports::do_frame; - pClientExternal->idle = ra::services::AchievementRuntimeExports::idle; - pClientExternal->is_processing_required = ra::services::AchievementRuntimeExports::is_processing_required; - pClientExternal->can_pause = ra::services::AchievementRuntimeExports::can_pause; - pClientExternal->reset = ra::services::AchievementRuntimeExports::reset; - - pClientExternal->progress_size = ra::services::AchievementRuntimeExports::progress_size; - pClientExternal->serialize_progress = ra::services::AchievementRuntimeExports::serialize_progress; - pClientExternal->deserialize_progress = ra::services::AchievementRuntimeExports::deserialize_progress; -} - -API int CCONV _Rcheevos_GetExternalClient(rc_client_external_t* pClientExternal, int nVersion) -{ - switch (nVersion) - { - default: - RA_LOG_WARN("Unknown rc_client_external interface version: %s", nVersion); - __fallthrough; - - case 1: - GetExternalClientV1(pClientExternal); - break; - } - - return 1; -} - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* RC_CLIENT_EXPORTS_EXTERNAL */ diff --git a/src/services/AchievementRuntime.hh b/src/services/AchievementRuntime.hh index 5c801c85..fbdcb6bb 100644 --- a/src/services/AchievementRuntime.hh +++ b/src/services/AchievementRuntime.hh @@ -13,8 +13,6 @@ #include -#define RC_CLIENT_EXPORTS_EXTERNAL - #include struct rc_api_fetch_game_data_response_t; diff --git a/src/services/AchievementRuntimeExports.cpp b/src/services/AchievementRuntimeExports.cpp new file mode 100644 index 00000000..25ede0a8 --- /dev/null +++ b/src/services/AchievementRuntimeExports.cpp @@ -0,0 +1,950 @@ +#include "AchievementRuntime.hh" + +#include "Exports.hh" +#include "RA_Log.h" +#include "RA_Resource.h" + +#include "data\context\ConsoleContext.hh" +#include "data\context\GameContext.hh" + +#include "services\AchievementRuntime.hh" +#include "services\IConfiguration.hh" +#include "services\ServiceLocator.hh" +#include "services\impl\JsonFileConfiguration.hh" + +#include "ui\viewmodels\IntegrationMenuViewModel.hh" + +#include +#include +#include + +namespace ra { +namespace services { + +class AchievementRuntimeExports : private AchievementRuntime +{ +public: + static void destroy() noexcept + { + memset(&s_callbacks, 0, sizeof(s_callbacks)); + + if (s_pIntegrationMenu) + { + s_pIntegrationMenu = nullptr; + rc_buffer_destroy(&s_pIntegrationMenuBuffer); + } + } + + static void enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) + { + s_callbacks.log_callback = callback; + s_callbacks.log_client = client; + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_enable_logging(pClient.GetClient(), level, AchievementRuntimeExports::LogMessageExternal); + } + + static void set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) + { + s_callbacks.event_handler = handler; + s_callbacks.event_client = client; + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_event_handler(pClient.GetClient(), AchievementRuntimeExports::EventHandlerExternal); + } + + static void set_read_memory(rc_client_t* client, rc_client_read_memory_func_t handler) + { + s_callbacks.read_memory_handler = handler; + s_callbacks.read_memory_client = client; + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_read_memory_function(pClient.GetClient(), AchievementRuntimeExports::ReadMemoryExternal); + + ResetMemory(); + } + + static void set_get_time_millisecs(rc_client_t* client, rc_get_time_millisecs_func_t handler) + { + s_callbacks.get_time_millisecs_handler = handler; + s_callbacks.get_time_millisecs_client = client; + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_get_time_millisecs_function(pClient.GetClient(), AchievementRuntimeExports::GetTimeMillisecsExternal); + } + + static void set_host(const char* value) + { + auto* pConfiguration = dynamic_cast( + &ra::services::ServiceLocator::GetMutable()); + if (pConfiguration != nullptr) + pConfiguration->SetHost(value); + } + + static bool IsUpdatingHardcore() noexcept { return s_bUpdatingHardcore; } + + static void set_hardcore_enabled(int value) + { + auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); + + s_bUpdatingHardcore = true; // prevent raising RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED event + + if (value) + pEmulatorContext.EnableHardcoreMode(false); + else + pEmulatorContext.DisableHardcoreMode(); + + s_bUpdatingHardcore = false; + } + + static int get_hardcore_enabled() + { + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + return pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore); + } + + static void set_unofficial_enabled(int value) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_unofficial_enabled(pClient.GetClient(), value); + } + + static int get_unofficial_enabled() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_unofficial_enabled(pClient.GetClient()); + } + + static void set_encore_mode_enabled(int value) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_encore_mode_enabled(pClient.GetClient(), value); + } + + static int get_encore_mode_enabled() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_encore_mode_enabled(pClient.GetClient()); + } + + static void set_spectator_mode_enabled(int value) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + rc_client_set_spectator_mode_enabled(pClient.GetClient(), value); + } + + static int get_spectator_mode_enabled() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_spectator_mode_enabled(pClient.GetClient()); + } + + static void abort_async(rc_client_async_handle_t* handle) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + rc_client_abort_async(pClient.GetClient(), handle); + } + + static rc_client_async_handle_t* begin_login_with_password(rc_client_t* client, const char* username, + const char* password, rc_client_callback_t callback, + void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginLoginWithPassword(username, password, pCallbackData); + } + + static rc_client_async_handle_t* begin_login_with_token(rc_client_t* client, const char* username, + const char* token, rc_client_callback_t callback, + void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pCallbackWrapper = new LoadGameCallbackWrapper(client, callback, callback_userdata); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginLoginWithToken(username, token, pCallbackWrapper); + } + + static const rc_client_user_t* get_user_info() + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_get_user_info(pClient.GetClient()); + } + + static void logout() + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_logout(pClient.GetClient()); + } + + class LoadExternalGameCallbackWrapper : public CallbackWrapper + { + public: + LoadExternalGameCallbackWrapper(rc_client_t* client, rc_client_callback_t callback, + void* callback_userdata) noexcept : + CallbackWrapper(client, callback, callback_userdata) + {} + + bool bWasPaused = false; + }; + + static void load_game_callback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) + { + auto* wrapper = static_cast(pUserdata); + Expects(wrapper != nullptr); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + const auto* pGame = pClient.GetClient()->game; + if (pGame) + { + _RA_SetConsoleID(pGame->public_.console_id); + ResetMemory(); + } + + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + pGameContext.EndLoadGame(nResult, wrapper->bWasPaused, false); + + wrapper->DoCallback(nResult, sErrorMessage); + + delete wrapper; + } + + static rc_client_async_handle_t* begin_identify_and_load_game(rc_client_t* client, uint32_t console_id, + const char* file_path, const uint8_t* data, + size_t data_size, rc_client_callback_t callback, + void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pNestedCallbackData = new LoadExternalGameCallbackWrapper(client, callback, callback_userdata); + GSL_SUPPRESS_R3 + auto* pCallbackData = new LoadGameCallbackWrapper(client, load_game_callback, pNestedCallbackData); + + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + pGameContext.BeginLoadGame(0xFFFFFFFF, ra::data::context::GameContext::Mode::Normal, + pNestedCallbackData->bWasPaused); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginIdentifyAndLoadGame(console_id, file_path, data, data_size, pCallbackData); + } + + static rc_client_async_handle_t* begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pNestedCallbackData = new LoadExternalGameCallbackWrapper(client, callback, callback_userdata); + GSL_SUPPRESS_R3 + auto* pCallbackData = new LoadGameCallbackWrapper(client, load_game_callback, pNestedCallbackData); + + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + pGameContext.BeginLoadGame(0xFFFFFFFF, ra::data::context::GameContext::Mode::Normal, + pNestedCallbackData->bWasPaused); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginLoadGame(hash, 0, pCallbackData); + } + + static void unload_game() + { + _RA_ActivateGame(0); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_unload_game(pClient.GetClient()); + } + + static const rc_client_game_t* get_game_info() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_game_info(pClient.GetClient()); + } + + static const rc_client_subset_t* get_subset_info(uint32_t subset_id) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_subset_info(pClient.GetClient(), subset_id); + } + + static void get_user_game_summary(rc_client_user_game_summary_t* summary) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_user_game_summary(pClient.GetClient(), summary); + } + + static rc_client_async_handle_t* begin_change_media(rc_client_t* client, const char* file_path, + const uint8_t* data, size_t data_size, + rc_client_callback_t callback, void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginChangeMedia(file_path, data, data_size, pCallbackData); + } + + static rc_client_achievement_list_info_t* create_achievement_list(int category, int grouping) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + GSL_SUPPRESS_TYPE1 + auto* list = reinterpret_cast( + rc_client_create_achievement_list(pClient.GetClient(), category, grouping)); + list->destroy_func = destroy_achievement_list; + return list; + } + + static int has_achievements() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_has_achievements(pClient.GetClient()); + } + + static const rc_client_achievement_t* get_achievement_info(uint32_t id) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_achievement_info(pClient.GetClient(), id); + } + + static rc_client_leaderboard_list_info_t* create_leaderboard_list(int grouping) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + GSL_SUPPRESS_TYPE1 + auto* list = reinterpret_cast( + rc_client_create_leaderboard_list(pClient.GetClient(), grouping)); + list->destroy_func = destroy_leaderboard_list; + return list; + } + + static int has_leaderboards() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_has_leaderboards(pClient.GetClient()); + } + + static const rc_client_leaderboard_t* get_leaderboard_info(uint32_t id) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_leaderboard_info(pClient.GetClient(), id); + } + +private: + class LeaderboardEntriesListCallbackWrapper + { + public: + LeaderboardEntriesListCallbackWrapper(rc_client_t* client, + rc_client_fetch_leaderboard_entries_callback_t callback, + void* callback_userdata) noexcept : + m_pClient(client), m_fCallback(callback), m_pCallbackUserdata(callback_userdata) + {} + + void DoCallback(int nResult, rc_client_leaderboard_entry_list_t* pList, const char* sErrorMessage) noexcept + { + m_fCallback(nResult, sErrorMessage, pList, m_pClient, m_pCallbackUserdata); + } + + static void Dispatch(int nResult, const char* sErrorMessage, rc_client_leaderboard_entry_list_t* pList, + rc_client_t*, void* pUserdata) + { + auto* wrapper = static_cast(pUserdata); + Expects(wrapper != nullptr); + + if (pList) + { + GSL_SUPPRESS_TYPE1 + reinterpret_cast(pList)->destroy_func = destroy_leaderboard_entry_list; + } + + wrapper->DoCallback(nResult, pList, sErrorMessage); + + delete wrapper; + } + + private: + rc_client_t* m_pClient; + rc_client_fetch_leaderboard_entries_callback_t m_fCallback; + void* m_pCallbackUserdata; + }; + +public: + static rc_client_async_handle_t* begin_fetch_leaderboard_entries(rc_client_t* client, + uint32_t leaderboard_id, uint32_t first_entry, uint32_t count, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pCallbackData = new LeaderboardEntriesListCallbackWrapper(client, callback, callback_userdata); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_begin_fetch_leaderboard_entries(pClient.GetClient(), leaderboard_id, first_entry, count, + LeaderboardEntriesListCallbackWrapper::Dispatch, pCallbackData); + } + + static rc_client_async_handle_t* begin_fetch_leaderboard_entries_around_user(rc_client_t* client, + uint32_t leaderboard_id, uint32_t count, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) + { + GSL_SUPPRESS_R3 + auto* pCallbackData = new LeaderboardEntriesListCallbackWrapper(client, callback, callback_userdata); + + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_begin_fetch_leaderboard_entries_around_user(pClient.GetClient(), leaderboard_id, count, + LeaderboardEntriesListCallbackWrapper::Dispatch, pCallbackData); + } + + static size_t get_rich_presence_message(char buffer[], size_t buffer_size) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_get_rich_presence_message(pClient.GetClient(), buffer, buffer_size); + } + + static int has_rich_presence() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_has_rich_presence(pClient.GetClient()); + } + + static void do_frame() noexcept + { + _RA_DoAchievementsFrame(); + } + + static void idle() + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_idle(pClient.GetClient()); + } + + static int is_processing_required() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_is_processing_required(pClient.GetClient()); + } + + static int can_pause(uint32_t* frames_remaining) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_can_pause(pClient.GetClient(), frames_remaining); + } + + static void reset() noexcept + { +#ifndef RA_UTEST + _RA_OnReset(); +#endif + } + + static size_t progress_size() + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_progress_size(pClient.GetClient()); + } + + static int serialize_progress(uint8_t* buffer) + { + const auto& pClient = ra::services::ServiceLocator::Get(); + return rc_client_serialize_progress(pClient.GetClient(), buffer); + } + + static int deserialize_progress(const uint8_t* buffer) + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_deserialize_progress(pClient.GetClient(), buffer); + } + + static void set_raintegration_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) noexcept + { + s_callbacks.write_memory_client = client; + s_callbacks.write_memory_handler = handler; + } + + static void set_raintegration_event_handler(rc_client_t* client, rc_client_raintegration_event_handler_t handler) noexcept + { + s_callbacks.raintegration_event_client = client; + s_callbacks.raintegration_event_handler = handler; + } + + static const rc_client_raintegration_menu_t* get_raintegration_menu() + { + ra::ui::viewmodels::LookupItemViewModelCollection vmMenuItems; + ra::ui::viewmodels::IntegrationMenuViewModel::BuildMenu(vmMenuItems); + + for (gsl::index nIndex = gsl::narrow_cast(vmMenuItems.Count()) - 1; nIndex >= 0; --nIndex) + { + const auto* pItem = vmMenuItems.GetItemAt(nIndex); + if (!pItem) + continue; + + switch (pItem->GetId()) + { + case IDM_RA_FILES_LOGIN: + case IDM_RA_FILES_LOGOUT: + case IDM_RA_TOGGLELEADERBOARDS: + case IDM_RA_OVERLAYSETTINGS: + vmMenuItems.RemoveAt(nIndex); + break; + + case 0: // separator - prevent two adjacent separators + pItem = vmMenuItems.GetItemAt(nIndex + 1); + if (pItem && pItem->GetId() == 0) + vmMenuItems.RemoveAt(nIndex + 1); + break; + } + } + + { + const auto* pItem = vmMenuItems.GetItemAt(0); + if (pItem && pItem->GetId() == 0) + vmMenuItems.RemoveAt(0); + } + + bool bChanged = true; + rc_client_raintegration_menu_item_t* pMenuItem = nullptr; + + if (s_pIntegrationMenu && s_pIntegrationMenu->num_items == vmMenuItems.Count()) + { + bChanged = false; + + pMenuItem = s_pIntegrationMenu->items; + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(vmMenuItems.Count()); ++nIndex, ++pMenuItem) + { + const auto* pItem = vmMenuItems.GetItemAt(nIndex); + if (!pItem) + continue; + + const auto nId = ra::to_unsigned(pItem->GetId()); + if (pMenuItem->id != nId) + { + if (pMenuItem->id == 0 || nId == 0) + { + bChanged = true; + break; + } + + pMenuItem->id = nId; + } + + pMenuItem->checked = pItem->IsSelected() ? 1 : 0; + } + + if (!bChanged) + { + pMenuItem = s_pIntegrationMenu->items; + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(vmMenuItems.Count()); + ++nIndex, ++pMenuItem) + { + const auto* pItem = vmMenuItems.GetItemAt(nIndex); + if (!pItem) + continue; + + if (pMenuItem->label) + { + if (ra::Narrow(pItem->GetLabel()) != pMenuItem->label) + { + bChanged = true; + break; + } + } + else if (!pItem->GetLabel().empty()) + { + bChanged = true; + break; + } + } + } + } + + if (!bChanged) + return s_pIntegrationMenu; + + if (s_pIntegrationMenu) + rc_buffer_destroy(&s_pIntegrationMenuBuffer); + + rc_buffer_init(&s_pIntegrationMenuBuffer); + s_pIntegrationMenu = static_cast( + rc_buffer_alloc(&s_pIntegrationMenuBuffer, sizeof(*s_pIntegrationMenu))); + s_pIntegrationMenu->num_items = 0; + s_pIntegrationMenu->items = static_cast( + rc_buffer_alloc(&s_pIntegrationMenuBuffer, sizeof(rc_client_raintegration_menu_item_t) * vmMenuItems.Count())); + + pMenuItem = s_pIntegrationMenu->items; + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(vmMenuItems.Count()); ++nIndex, ++pMenuItem) + { + const auto* pItem = vmMenuItems.GetItemAt(nIndex); + const auto nId = pItem ? pItem->GetId() : 0; + if (nId == 0) + { + memset(pMenuItem, 0, sizeof(*pMenuItem)); + } + else + { + pMenuItem->label = rc_buffer_strcpy(&s_pIntegrationMenuBuffer, ra::Narrow(pItem->GetLabel()).c_str()); + pMenuItem->id = nId; + pMenuItem->checked = pItem->IsSelected(); + } + + pMenuItem->enabled = (nId != IDM_RA_FILES_LOGIN); + } + + s_pIntegrationMenu->num_items = gsl::narrow_cast(pMenuItem - s_pIntegrationMenu->items); + return s_pIntegrationMenu; + } + + static void SyncMenuItem(int nMenuItemId) + { + if (!s_pIntegrationMenu) + return; + + auto* pMenuItem = s_pIntegrationMenu->items; + const auto* pStop = pMenuItem + s_pIntegrationMenu->num_items; + while (pMenuItem < pStop && ra::to_signed(pMenuItem->id) != nMenuItemId) + ++pMenuItem; + if (pMenuItem == pStop) + return; + + rc_client_raintegration_event_t pEvent; + memset(&pEvent, 0, sizeof(pEvent)); + pEvent.menu_item = pMenuItem; + + ra::ui::viewmodels::LookupItemViewModelCollection vmMenuItems; + ra::ui::viewmodels::IntegrationMenuViewModel::BuildMenu(vmMenuItems); + + for (gsl::index nIndex = gsl::narrow_cast(vmMenuItems.Count()) - 1; nIndex >= 0; --nIndex) + { + const auto* pItem = vmMenuItems.GetItemAt(nIndex); + if (pItem && pItem->GetId() == nMenuItemId) + { + const uint8_t checked = pItem->IsSelected() ? 1 : 0; + if (pMenuItem->checked != checked) + { + pMenuItem->checked = checked; + + if (s_callbacks.raintegration_event_handler) + { + pEvent.type = RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED; + s_callbacks.raintegration_event_handler(&pEvent, s_callbacks.raintegration_event_client); + } + } + + break; + } + } + } + + static int activate_menu_item(uint32_t nMenuItemId) + { + if (nMenuItemId < IDM_RA_MENUSTART || nMenuItemId > IDM_RA_MENUEND) + return 0; + + if (!s_pIntegrationMenu) + return 0; + + auto* pMenuItem = s_pIntegrationMenu->items; + const auto* pStop = pMenuItem + s_pIntegrationMenu->num_items; + while (pMenuItem < pStop && pMenuItem->id != nMenuItemId) + ++pMenuItem; + if (pMenuItem == pStop || !pMenuItem->enabled) + return 0; + + ra::ui::viewmodels::IntegrationMenuViewModel::ActivateMenuItem(ra::to_signed(nMenuItemId)); + return 1; + } + + static void RaiseIntegrationEvent(uint32_t nType) noexcept + { + if (s_callbacks.raintegration_event_handler) + { + rc_client_raintegration_event_t pEvent; + memset(&pEvent, 0, sizeof(pEvent)); + pEvent.type = nType; + + s_callbacks.raintegration_event_handler(&pEvent, s_callbacks.raintegration_event_client); + } + } + + static bool IsExternalRcheevosClient() noexcept + { + return s_bIsExternalRcheevosClient; + } + + static void HookupCallbackEvents() + { + auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); + pEmulatorContext.SetPauseFunction(RaisePauseEvent); + pEmulatorContext.SetResetFunction(RaiseResetEvent); + + s_bIsExternalRcheevosClient = true; + } + +private: + typedef struct ExternalClientCallbacks + { + rc_client_message_callback_t log_callback; + rc_client_t* log_client; + + rc_client_event_handler_t event_handler; + rc_client_t* event_client; + + rc_client_read_memory_func_t read_memory_handler; + rc_client_t* read_memory_client; + + rc_client_raintegration_write_memory_func_t write_memory_handler; + rc_client_t* write_memory_client; + + rc_get_time_millisecs_func_t get_time_millisecs_handler; + rc_client_t* get_time_millisecs_client; + + rc_client_raintegration_event_handler_t raintegration_event_handler; + rc_client_t* raintegration_event_client; + + } ExternalClientCallbacks; + + static ExternalClientCallbacks s_callbacks; + + static void LogMessageExternal(const char* sMessage, const rc_client_t*) + { + const auto& pLogger = ra::services::ServiceLocator::Get(); + if (pLogger.IsEnabled(ra::services::LogLevel::Info)) + pLogger.LogMessage(ra::services::LogLevel::Info, sMessage); + + if (s_callbacks.log_callback) + s_callbacks.log_callback(sMessage, s_callbacks.log_client); + } + + static void EventHandlerExternal(const rc_client_event_t* event, rc_client_t*) noexcept(false) + { + if (s_callbacks.event_handler) + s_callbacks.event_handler(event, s_callbacks.event_client); + } + + static uint8_t ReadMemoryByte(uint32_t address) noexcept + { + if (s_callbacks.read_memory_handler) + { + uint8_t value = 0; + + if (s_callbacks.read_memory_handler(address, &value, 1, s_callbacks.read_memory_client) == 1) + return value; + } + + return 0; + } + + static void WriteMemoryByte(uint32_t address, uint8_t value) noexcept + { + if (s_callbacks.write_memory_handler) + s_callbacks.write_memory_handler(address, &value, 1, s_callbacks.write_memory_client); + } + + static uint32_t ReadMemoryBlock(uint32_t address, uint8_t* buffer, uint32_t num_bytes) noexcept(false) + { + if (s_callbacks.read_memory_handler) + return s_callbacks.read_memory_handler(address, buffer, num_bytes, s_callbacks.read_memory_client); + + return 0; + } + + static uint32_t ReadMemoryExternal(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t*) noexcept(false) + { + if (s_callbacks.read_memory_handler) + return s_callbacks.read_memory_handler(address, buffer, num_bytes, s_callbacks.read_memory_client); + + return 0; + } + + static void ResetMemory() + { + const auto& pConsoleContext = ra::services::ServiceLocator::Get(); + const auto nNumBytes = pConsoleContext.MaxAddress() + 1; + + auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); + 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); + } + + static void RaiseResetEvent() noexcept + { + if (s_callbacks.event_handler) + { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + + s_callbacks.event_handler(&client_event, s_callbacks.event_client); + } + } + + static rc_clock_t GetTimeMillisecsExternal(const rc_client_t*) noexcept(false) + { + if (s_callbacks.get_time_millisecs_handler) + return s_callbacks.get_time_millisecs_handler(s_callbacks.get_time_millisecs_client); + + return 0; + } + + static void destroy_achievement_list(rc_client_achievement_list_info_t* list) noexcept + { + if (list) + free(list); + } + + static void destroy_leaderboard_list(rc_client_leaderboard_list_info_t* list) noexcept + { + if (list) + free(list); + } + + static void destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_info_t* list) noexcept + { + if (list) + free(list); + } + + static bool s_bIsExternalRcheevosClient; + static bool s_bUpdatingHardcore; + + static rc_buffer_t s_pIntegrationMenuBuffer; + static rc_client_raintegration_menu_t* s_pIntegrationMenu; +}; + +AchievementRuntimeExports::ExternalClientCallbacks AchievementRuntimeExports::s_callbacks{}; +bool AchievementRuntimeExports::s_bIsExternalRcheevosClient = false; +bool AchievementRuntimeExports::s_bUpdatingHardcore = false; +rc_client_raintegration_menu_t* AchievementRuntimeExports::s_pIntegrationMenu = nullptr; +rc_buffer_t AchievementRuntimeExports::s_pIntegrationMenuBuffer{}; + +} // namespace services +} // namespace ra + +bool IsExternalRcheevosClient() noexcept +{ + return ra::services::AchievementRuntimeExports::IsExternalRcheevosClient(); +} + +void SyncClientExternalRAIntegrationMenuItem(int nMenuItemId) +{ + ra::services::AchievementRuntimeExports::SyncMenuItem(nMenuItemId); +} + +void SyncClientExternalHardcoreState() +{ + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + const int bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore) ? 1 : 0; + + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + if (rc_client_get_hardcore_enabled(pRuntime.GetClient()) != bHardcore) + { + rc_client_set_hardcore_enabled(pRuntime.GetClient(), bHardcore); + + if (!ra::services::AchievementRuntimeExports::IsUpdatingHardcore()) + ra::services::AchievementRuntimeExports::RaiseIntegrationEvent(RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED); + } +} + +static void GetExternalClientV1(rc_client_external_t* pClientExternal) noexcept +{ + pClientExternal->destroy = ra::services::AchievementRuntimeExports::destroy; + + pClientExternal->enable_logging = ra::services::AchievementRuntimeExports::enable_logging; + pClientExternal->set_event_handler = ra::services::AchievementRuntimeExports::set_event_handler; + pClientExternal->set_read_memory = ra::services::AchievementRuntimeExports::set_read_memory; + pClientExternal->set_get_time_millisecs = ra::services::AchievementRuntimeExports::set_get_time_millisecs; + pClientExternal->set_host = ra::services::AchievementRuntimeExports::set_host; + + pClientExternal->set_hardcore_enabled = ra::services::AchievementRuntimeExports::set_hardcore_enabled; + pClientExternal->get_hardcore_enabled = ra::services::AchievementRuntimeExports::get_hardcore_enabled; + pClientExternal->set_unofficial_enabled = ra::services::AchievementRuntimeExports::set_unofficial_enabled; + pClientExternal->get_unofficial_enabled = ra::services::AchievementRuntimeExports::get_unofficial_enabled; + pClientExternal->set_encore_mode_enabled = ra::services::AchievementRuntimeExports::set_encore_mode_enabled; + pClientExternal->get_encore_mode_enabled = ra::services::AchievementRuntimeExports::get_encore_mode_enabled; + pClientExternal->set_spectator_mode_enabled = ra::services::AchievementRuntimeExports::set_spectator_mode_enabled; + pClientExternal->get_spectator_mode_enabled = ra::services::AchievementRuntimeExports::get_spectator_mode_enabled; + + pClientExternal->abort_async = ra::services::AchievementRuntimeExports::abort_async; + + pClientExternal->begin_login_with_password = ra::services::AchievementRuntimeExports::begin_login_with_password; + pClientExternal->begin_login_with_token = ra::services::AchievementRuntimeExports::begin_login_with_token; + pClientExternal->logout = ra::services::AchievementRuntimeExports::logout; + pClientExternal->get_user_info = ra::services::AchievementRuntimeExports::get_user_info; + + pClientExternal->begin_identify_and_load_game = ra::services::AchievementRuntimeExports::begin_identify_and_load_game; + pClientExternal->begin_load_game = ra::services::AchievementRuntimeExports::begin_load_game; + pClientExternal->get_game_info = ra::services::AchievementRuntimeExports::get_game_info; + pClientExternal->get_subset_info = ra::services::AchievementRuntimeExports::get_subset_info; + pClientExternal->unload_game = ra::services::AchievementRuntimeExports::unload_game; + pClientExternal->get_user_game_summary = ra::services::AchievementRuntimeExports::get_user_game_summary; + pClientExternal->begin_change_media = ra::services::AchievementRuntimeExports::begin_change_media; + + pClientExternal->create_achievement_list = ra::services::AchievementRuntimeExports::create_achievement_list; + pClientExternal->has_achievements = ra::services::AchievementRuntimeExports::has_achievements; + pClientExternal->get_achievement_info = ra::services::AchievementRuntimeExports::get_achievement_info; + + pClientExternal->create_leaderboard_list = ra::services::AchievementRuntimeExports::create_leaderboard_list; + pClientExternal->has_leaderboards = ra::services::AchievementRuntimeExports::has_leaderboards; + pClientExternal->get_leaderboard_info = ra::services::AchievementRuntimeExports::get_leaderboard_info; + pClientExternal->begin_fetch_leaderboard_entries = ra::services::AchievementRuntimeExports::begin_fetch_leaderboard_entries; + pClientExternal->begin_fetch_leaderboard_entries_around_user = + ra::services::AchievementRuntimeExports::begin_fetch_leaderboard_entries_around_user; + + pClientExternal->get_rich_presence_message = ra::services::AchievementRuntimeExports::get_rich_presence_message; + pClientExternal->has_rich_presence = ra::services::AchievementRuntimeExports::has_rich_presence; + + pClientExternal->do_frame = ra::services::AchievementRuntimeExports::do_frame; + pClientExternal->idle = ra::services::AchievementRuntimeExports::idle; + pClientExternal->is_processing_required = ra::services::AchievementRuntimeExports::is_processing_required; + pClientExternal->can_pause = ra::services::AchievementRuntimeExports::can_pause; + pClientExternal->reset = ra::services::AchievementRuntimeExports::reset; + + pClientExternal->progress_size = ra::services::AchievementRuntimeExports::progress_size; + pClientExternal->serialize_progress = ra::services::AchievementRuntimeExports::serialize_progress; + pClientExternal->deserialize_progress = ra::services::AchievementRuntimeExports::deserialize_progress; +} + +#ifdef __cplusplus +extern "C" { +#endif + +API int CCONV _Rcheevos_GetExternalClient(rc_client_external_t* pClientExternal, int nVersion) +{ + switch (nVersion) + { + default: + RA_LOG_WARN("Unknown rc_client_external interface version: %s", nVersion); + __fallthrough; + + case 1: + GetExternalClientV1(pClientExternal); + break; + } + + ra::services::AchievementRuntimeExports::HookupCallbackEvents(); + + return 1; +} + +API const rc_client_raintegration_menu_t* CCONV _Rcheevos_RAIntegrationGetMenu() +{ + return ra::services::AchievementRuntimeExports::get_raintegration_menu(); +} + +API int CCONV _Rcheevos_ActivateRAIntegrationMenuItem(uint32_t nId) +{ + return ra::services::AchievementRuntimeExports::activate_menu_item(nId); +} + +API void CCONV _Rcheevos_SetRAIntegrationWriteMemoryFunction(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler) +{ + ra::services::AchievementRuntimeExports::set_raintegration_write_memory_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); +} + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/services/AchievementRuntimeExports.hh b/src/services/AchievementRuntimeExports.hh new file mode 100644 index 00000000..1177d459 --- /dev/null +++ b/src/services/AchievementRuntimeExports.hh @@ -0,0 +1,9 @@ +#ifndef RA_SERVICES_ACHIEVEMENT_RUNTIME_EXPORTS_HH +#define RA_SERVICES_ACHIEVEMENT_RUNTIME_EXPORTS_HH +#pragma once + +void SyncClientExternalRAIntegrationMenuItem(int nMenuItemId); +void SyncClientExternalHardcoreState(); +bool IsExternalRcheevosClient(); + +#endif // !RA_SERVICES_ACHIEVEMENT_RUNTIME_EXPORTS_HH diff --git a/src/ui/IDesktop.hh b/src/ui/IDesktop.hh index 512d6401..e4017960 100644 --- a/src/ui/IDesktop.hh +++ b/src/ui/IDesktop.hh @@ -77,12 +77,12 @@ public: /// /// Returns true if the current thread is the UI thread. /// - virtual bool IsOnUIThread(const WindowViewModelBase& vmViewModel) const = 0; + virtual bool IsOnUIThread() const = 0; /// /// Executes a function on the UI thread. /// - virtual void InvokeOnUIThread(const WindowViewModelBase& vmViewModel, std::function fAction) const = 0; + virtual void InvokeOnUIThread(std::function fAction) const = 0; virtual void Shutdown() = 0; diff --git a/src/ui/viewmodels/IntegrationMenuViewModel.cpp b/src/ui/viewmodels/IntegrationMenuViewModel.cpp index 5e215692..f3c463f2 100644 --- a/src/ui/viewmodels/IntegrationMenuViewModel.cpp +++ b/src/ui/viewmodels/IntegrationMenuViewModel.cpp @@ -237,7 +237,7 @@ void IntegrationMenuViewModel::ToggleNonHardcoreWarning() !pConfiguration.IsFeatureEnabled(ra::services::Feature::NonHardcoreWarning)); const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - pEmulatorContext.RebuildMenu(); + pEmulatorContext.UpdateMenuState(IDM_RA_NON_HARDCORE_WARNING); } void IntegrationMenuViewModel::ToggleLeaderboards() @@ -259,7 +259,7 @@ void IntegrationMenuViewModel::ToggleLeaderboards() } const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - pEmulatorContext.RebuildMenu(); + pEmulatorContext.UpdateMenuState(IDM_RA_TOGGLELEADERBOARDS); } diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp index d4f20ec2..d9d23960 100644 --- a/src/ui/viewmodels/MemorySearchViewModel.cpp +++ b/src/ui/viewmodels/MemorySearchViewModel.cpp @@ -738,12 +738,11 @@ void MemorySearchViewModel::UpdateResults() return; } - const auto& vmMemoryInspector = ra::services::ServiceLocator::Get().MemoryInspector; const auto& pDesktop = ra::services::ServiceLocator::Get(); - if (!pDesktop.IsOnUIThread(vmMemoryInspector)) + if (!pDesktop.IsOnUIThread()) { m_bUpdateResultsPending = true; - pDesktop.InvokeOnUIThread(vmMemoryInspector, [this]() + pDesktop.InvokeOnUIThread([this]() { if (m_bUpdateResultsPending) { diff --git a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp index c5672077..c30b65ca 100644 --- a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp +++ b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp @@ -62,7 +62,7 @@ static void SetAchievement(OverlayListPageViewModel::ItemViewModel& vmItem, if (pAchievement.measured_progress[0]) { vmItem.SetProgressString(ra::Widen(pAchievement.measured_progress)); - vmItem.SetProgressPercentage(pAchievement.measured_percent); + vmItem.SetProgressPercentage(pAchievement.measured_percent / 100.0f); } else { diff --git a/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp b/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp index 8d7ad50e..a0105142 100644 --- a/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp +++ b/src/ui/viewmodels/RichPresenceMonitorViewModel.cpp @@ -66,18 +66,19 @@ void RichPresenceMonitorViewModel::UpdateDisplayString() &pGameContext.Assets().Append(std::move(pNewRichPresence))); } - // modified rich presence cannot be active in hardcore - const bool bHardcore = ra::services::ServiceLocator::Get().IsFeatureEnabled( - ra::services::Feature::Hardcore); - if (bHardcore) - pRichPresence->Deactivate(); - // load the updated file pRichPresence->ReloadRichPresenceScript(); // automatically activate if not in hardcore - if (!bHardcore) - pRichPresence->Activate(); + if (!pRichPresence->IsActive()) + { + const bool bHardcore = ra::services::ServiceLocator::Get().IsFeatureEnabled( + ra::services::Feature::Hardcore); + if (!bHardcore) + { + pRichPresence->Activate(); + } + } UpdateWindowTitle(); } diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp index 63333323..7720df20 100644 --- a/src/ui/win32/AssetEditorDialog.cpp +++ b/src/ui/win32/AssetEditorDialog.cpp @@ -499,12 +499,15 @@ void AssetEditorDialog::ActiveCheckBoxBinding::OnViewModelIntValueChanged(const if (args.Property == ra::ui::viewmodels::AssetEditorViewModel::StateProperty) { const bool bIsActive = IsActive(); - const bool bWasActive = Button_GetCheck(m_hWnd); - if (bIsActive != bWasActive) - { - Button_SetCheck(m_hWnd, bIsActive); - OnValueChanged(); - } + + InvokeOnUIThread([this, bIsActive]() { + const bool bWasActive = Button_GetCheck(m_hWnd); + if (bIsActive != bWasActive) + { + Button_SetCheck(m_hWnd, bIsActive); + OnValueChanged(); + } + }); } } diff --git a/src/ui/win32/Desktop.cpp b/src/ui/win32/Desktop.cpp index fb64fc77..217c403c 100644 --- a/src/ui/win32/Desktop.cpp +++ b/src/ui/win32/Desktop.cpp @@ -59,10 +59,16 @@ Desktop::Desktop() noexcept void Desktop::ShowWindow(WindowViewModelBase& vmViewModel) const { + if (m_pWindowBinding) + m_pWindowBinding->EnableInvokeOnUIThread(); + auto* pPresenter = GetDialogPresenter(vmViewModel); if (pPresenter != nullptr) { - pPresenter->ShowWindow(vmViewModel); + ra::ui::win32::bindings::WindowBinding::InvokeOnUIThread( + [this, &vmViewModel, pPresenter]() { + pPresenter->ShowWindow(vmViewModel); + }); } else { @@ -78,13 +84,19 @@ ra::ui::DialogResult Desktop::ShowModal(WindowViewModelBase& vmViewModel) const ra::ui::DialogResult Desktop::ShowModal(WindowViewModelBase& vmViewModel, const WindowViewModelBase& vmParentViewModel) const { + if (m_pWindowBinding) + m_pWindowBinding->EnableInvokeOnUIThread(); + auto* pPresenter = GetDialogPresenter(vmViewModel); if (pPresenter != nullptr) { const auto* pBinding = ui::win32::bindings::WindowBinding::GetBindingFor(vmParentViewModel); const HWND hParentWnd = pBinding ? pBinding->GetHWnd() : nullptr; - pPresenter->ShowModal(vmViewModel, hParentWnd); + ra::ui::win32::bindings::WindowBinding::InvokeOnUIThreadAndWait( + [this, &vmViewModel, pPresenter, hParentWnd]() { + pPresenter->ShowModal(vmViewModel, hParentWnd); + }); } else { @@ -166,6 +178,8 @@ void Desktop::SetMainHWnd(HWND hWnd) } m_pWindowBinding->SetHWND(nullptr, hWnd); + m_pWindowBinding->SetUIThread(::GetWindowThreadProcessId(hWnd, nullptr)); + m_pWindowBinding->EnableInvokeOnUIThread(); g_RAMainWnd = hWnd; } @@ -299,18 +313,14 @@ bool Desktop::IsDebuggerPresent() const return IsSuspiciousProcessRunning(); } -bool Desktop::IsOnUIThread(const WindowViewModelBase& vmViewModel) const +bool Desktop::IsOnUIThread() const noexcept { - const auto* pBinding = ra::ui::win32::bindings::WindowBinding::GetBindingFor(vmViewModel); - Expects(pBinding != nullptr); - return pBinding->IsOnUIThread(); + return ra::ui::win32::bindings::WindowBinding::IsOnUIThread(); } -void Desktop::InvokeOnUIThread(const WindowViewModelBase& vmViewModel, std::function fAction) const +void Desktop::InvokeOnUIThread(std::function fAction) const { - auto* pBinding = ra::ui::win32::bindings::WindowBinding::GetBindingFor(vmViewModel); - Expects(pBinding != nullptr); - pBinding->InvokeOnUIThread(fAction); + ra::ui::win32::bindings::WindowBinding::InvokeOnUIThread(fAction); } void Desktop::Shutdown() noexcept diff --git a/src/ui/win32/Desktop.hh b/src/ui/win32/Desktop.hh index 3d707b00..c75155b0 100644 --- a/src/ui/win32/Desktop.hh +++ b/src/ui/win32/Desktop.hh @@ -36,8 +36,8 @@ public: bool IsDebuggerPresent() const override; - bool IsOnUIThread(const WindowViewModelBase& vmViewModel) const override; - void InvokeOnUIThread(const WindowViewModelBase& vmViewModel, std::function fAction) const override; + bool IsOnUIThread() const noexcept override; + void InvokeOnUIThread(std::function fAction) const override; private: _Success_(return != nullptr) diff --git a/src/ui/win32/DialogBase.cpp b/src/ui/win32/DialogBase.cpp index adbe0857..1949bbf1 100644 --- a/src/ui/win32/DialogBase.cpp +++ b/src/ui/win32/DialogBase.cpp @@ -3,6 +3,7 @@ #include "ui\win32\bindings\ControlBinding.hh" #include "ui\win32\bindings\GridBinding.hh" #include "ui\win32\bindings\NumericUpDownBinding.hh" +#include "ui\win32\bindings\WindowBinding.hh" #include "RA_Core.h" // g_RAMainWnd, g_hThisDLLInst diff --git a/src/ui/win32/DialogBase.hh b/src/ui/win32/DialogBase.hh index d52b62f4..5b9e64eb 100644 --- a/src/ui/win32/DialogBase.hh +++ b/src/ui/win32/DialogBase.hh @@ -64,6 +64,11 @@ public: /// void SetDialogResult(DialogResult nResult); + /// + /// Returns true if the current thread is the UI thread. + /// + bool IsOnUIThread() const noexcept { return m_bindWindow.IsOnUIThread(); } + protected: explicit DialogBase(_Inout_ ra::ui::WindowViewModelBase& vmWindow) noexcept; ~DialogBase() noexcept; diff --git a/src/ui/win32/bindings/CheckBoxBinding.hh b/src/ui/win32/bindings/CheckBoxBinding.hh index 78099a29..6ce14c90 100644 --- a/src/ui/win32/bindings/CheckBoxBinding.hh +++ b/src/ui/win32/bindings/CheckBoxBinding.hh @@ -45,7 +45,9 @@ protected: { if (m_pIsCheckedProperty && *m_pIsCheckedProperty == args.Property) { - Button_SetCheck(m_hWnd, args.tNewValue ? BST_CHECKED : BST_UNCHECKED); + InvokeOnUIThread([this, nValue = args.tNewValue ? BST_CHECKED : BST_UNCHECKED]() noexcept { + Button_SetCheck(m_hWnd, nValue); + }); OnValueChanged(); } } diff --git a/src/ui/win32/bindings/ComboBoxBinding.hh b/src/ui/win32/bindings/ComboBoxBinding.hh index 919568f9..602ba7b8 100644 --- a/src/ui/win32/bindings/ComboBoxBinding.hh +++ b/src/ui/win32/bindings/ComboBoxBinding.hh @@ -104,7 +104,7 @@ protected: void OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs& args) override { if (m_pSelectedIdProperty && *m_pSelectedIdProperty == args.Property) - UpdateSelectedItem(); + InvokeOnUIThread([this]() { UpdateSelectedItem(); }); } void OnViewModelStringValueChanged(gsl::index nIndex, const StringModelProperty::ChangeArgs& args) override @@ -112,26 +112,32 @@ protected: if (args.Property == *m_pItemTextProperty) { const auto& pLabel = m_pViewModelCollection->GetItemValue(nIndex, *m_pItemTextProperty); - ComboBox_DeleteString(m_hWnd, nIndex); - ComboBox_InsertString(m_hWnd, nIndex, NativeStr(pLabel).c_str()); - - if (m_nSelectedIndex == nIndex) - { - ComboBox_SetCurSel(m_hWnd, nIndex); - ComboBox_SetText(m_hWnd, NativeStr(pLabel).c_str()); - } + InvokeOnUIThread([this, pLabel = NativeStr(pLabel), nIndex]() { + ComboBox_DeleteString(m_hWnd, nIndex); + ComboBox_InsertString(m_hWnd, nIndex, NativeStr(pLabel).c_str()); + + if (m_nSelectedIndex == nIndex) + { + ComboBox_SetCurSel(m_hWnd, nIndex); + ComboBox_SetText(m_hWnd, NativeStr(pLabel).c_str()); + } + }); } } void OnViewModelAdded(gsl::index nIndex) override { const auto& pLabel = m_pViewModelCollection->GetItemValue(nIndex, *m_pItemTextProperty); - ComboBox_InsertString(m_hWnd, nIndex, NativeStr(pLabel).c_str()); + InvokeOnUIThread([this, nIndex, pLabel = NativeStr(pLabel)]() { + ComboBox_InsertString(m_hWnd, nIndex, NativeStr(pLabel).c_str()); + }); } - void OnViewModelRemoved(gsl::index nIndex) noexcept override + void OnViewModelRemoved(gsl::index nIndex) override { - ComboBox_DeleteString(m_hWnd, nIndex); + InvokeOnUIThread([this, nIndex]() noexcept { + ComboBox_DeleteString(m_hWnd, nIndex); + }); } virtual void PopulateComboBox() diff --git a/src/ui/win32/bindings/ControlBinding.hh b/src/ui/win32/bindings/ControlBinding.hh index 6d5c10f1..f6887462 100644 --- a/src/ui/win32/bindings/ControlBinding.hh +++ b/src/ui/win32/bindings/ControlBinding.hh @@ -151,8 +151,12 @@ protected: void InvokeOnUIThread(std::function fAction) { - if (m_pDialog) - m_pDialog->QueueFunction(fAction); + WindowBinding::InvokeOnUIThread(fAction); + } + + void InvokeOnUIThreadAndWait(std::function fAction) + { + WindowBinding::InvokeOnUIThreadAndWait(fAction); } HWND GetDialogHwnd() const diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp index c41d6095..fbd6cca3 100644 --- a/src/ui/win32/bindings/GridBinding.cpp +++ b/src/ui/win32/bindings/GridBinding.cpp @@ -81,6 +81,12 @@ void GridBinding::UpdateLayout() if (m_vColumns.empty()) return; + if (!WindowBinding::IsOnUIThread()) + { + InvokeOnUIThread([this]() { UpdateLayout(); }); + return; + } + DWORD exStyle = LVS_EX_DOUBLEBUFFER | LVS_EX_FULLROWSELECT | LVS_EX_LABELTIP; if (m_bShowGridLines) exStyle |= LVS_EX_GRIDLINES; @@ -275,10 +281,12 @@ void GridBinding::OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs& { m_nScrollOffset = args.tNewValue; - const auto nTopIndex = ListView_GetTopIndex(m_hWnd); - const auto nSpacing = HIWORD(ListView_GetItemSpacing(m_hWnd, TRUE)); - const int nDeltaY = (m_nScrollOffset - nTopIndex) * gsl::narrow_cast(nSpacing); - ListView_Scroll(m_hWnd, 0, nDeltaY); + InvokeOnUIThread([this]() noexcept { + const auto nTopIndex = ListView_GetTopIndex(m_hWnd); + const auto nSpacing = HIWORD(ListView_GetItemSpacing(m_hWnd, TRUE)); + const int nDeltaY = (m_nScrollOffset - nTopIndex) * gsl::narrow_cast(nSpacing); + ListView_Scroll(m_hWnd, 0, nDeltaY); + }); return; } @@ -286,10 +294,12 @@ void GridBinding::OnViewModelIntValueChanged(const IntModelProperty::ChangeArgs& { if (m_hWnd) { - if (args.tNewValue < 1) - ListView_SetItemCountEx(m_hWnd, 0, 0); - else - ListView_SetItemCountEx(m_hWnd, gsl::narrow_cast(args.tNewValue), LVSICF_NOSCROLL); + InvokeOnUIThread([this, nValue = args.tNewValue]() { + if (nValue < 1) + ListView_SetItemCountEx(m_hWnd, 0, 0); + else + ListView_SetItemCountEx(m_hWnd, gsl::narrow_cast(nValue), LVSICF_NOSCROLL); + }); CheckForScrollBar(); } @@ -315,7 +325,10 @@ void GridBinding::OnViewModelIntValueChanged(gsl::index nIndex, const IntModelPr { if (m_nAdjustingScrollOffset == 0) { - ListView_RedrawItems(m_hWnd, nIndex, nIndex); + InvokeOnUIThread([this, nIndex]() noexcept { + ListView_RedrawItems(m_hWnd, nIndex, nIndex); + }); + m_bForceRepaintItems = true; } return; @@ -325,18 +338,7 @@ void GridBinding::OnViewModelIntValueChanged(gsl::index nIndex, const IntModelPr { const auto& pColumn = *m_vColumns.at(nColumnIndex); if (pColumn.DependsOn(args.Property)) - { - SuspendRedraw(); - - // if the affected data is in the sort column, it's no longer sorted - if (m_nSortIndex == ra::to_signed(nColumnIndex)) - m_nSortIndex = -1; - UpdateCell(nIndex, nColumnIndex); - - ListView_RedrawItems(m_hWnd, nIndex, nIndex); - m_bForceRepaintItems = true; - } } if (!m_vmItems->IsUpdating()) @@ -349,24 +351,17 @@ void GridBinding::OnViewModelBoolValueChanged(gsl::index nIndex, const BoolModel nIndex = GetRealItemIndex(nIndex); if (m_pIsSelectedProperty && *m_pIsSelectedProperty == args.Property) - ListView_SetItemState(m_hWnd, nIndex, args.tNewValue ? LVIS_SELECTED : 0, LVIS_SELECTED); + { + InvokeOnUIThread([this, nIndex, nValue = args.tNewValue ? LVIS_SELECTED : 0]() noexcept { + ListView_SetItemState(m_hWnd, nIndex, nValue, LVIS_SELECTED); + }); + } for (size_t nColumnIndex = 0; nColumnIndex < m_vColumns.size(); ++nColumnIndex) { const auto& pColumn = *m_vColumns.at(nColumnIndex); if (pColumn.DependsOn(args.Property)) - { - SuspendRedraw(); - - // if the affected data is in the sort column, it's no longer sorted - if (m_nSortIndex == ra::to_signed(nColumnIndex)) - m_nSortIndex = -1; - UpdateCell(nIndex, nColumnIndex); - - ListView_RedrawItems(m_hWnd, nIndex, nIndex); - m_bForceRepaintItems = true; - } } } @@ -379,41 +374,46 @@ void GridBinding::OnViewModelStringValueChanged(gsl::index nIndex, const StringM { const auto& pColumn = *m_vColumns.at(nColumnIndex); if (pColumn.DependsOn(args.Property)) - { - SuspendRedraw(); - - // if the affected data is in the sort column, it's no longer sorted - if (m_nSortIndex == ra::to_signed(nColumnIndex)) - m_nSortIndex = -1; - UpdateCell(nIndex, nColumnIndex); - - ListView_RedrawItems(m_hWnd, nIndex, nIndex); - m_bForceRepaintItems = true; - } } } void GridBinding::UpdateCell(gsl::index nIndex, gsl::index nColumnIndex) { - std::wstring sText; - LV_ITEMW item{}; - item.mask = LVIF_TEXT; - item.iItem = gsl::narrow_cast(nIndex); - item.iSubItem = gsl::narrow_cast(nColumnIndex); + SuspendRedraw(); + + // if the affected data is in the sort column, it's no longer sorted + if (m_nSortIndex == nColumnIndex) + m_nSortIndex = -1; const auto& pColumn = *m_vColumns.at(nColumnIndex); - sText = pColumn.GetText(*m_vmItems, nIndex); - item.pszText = sText.data(); + std::wstring sText = pColumn.GetText(*m_vmItems, nIndex); - GSL_SUPPRESS_TYPE1 - SNDMSG(m_hWnd, LVM_SETITEMW, 0, reinterpret_cast(&item)); + InvokeOnUIThread([this, sText, nIndex, nColumnIndex]() noexcept { + LV_ITEMW item{}; + item.mask = LVIF_TEXT; + item.iItem = gsl::narrow_cast(nIndex); + item.iSubItem = gsl::narrow_cast(nColumnIndex); + GSL_SUPPRESS_TYPE3 + item.pszText = const_cast(sText.data()); + + GSL_SUPPRESS_TYPE1 + SNDMSG(m_hWnd, LVM_SETITEMW, 0, reinterpret_cast(&item)); + + ListView_RedrawItems(m_hWnd, nIndex, nIndex); + }); + + m_bForceRepaintItems = true; } void GridBinding::EnsureVisible(gsl::index nIndex) { if (nIndex >= 0 && nIndex < gsl::narrow_cast(m_vmItems->Count())) - ListView_EnsureVisible(m_hWnd, gsl::narrow_cast(nIndex), FALSE); + { + InvokeOnUIThread([this, nIndex]() noexcept { + ListView_EnsureVisible(m_hWnd, gsl::narrow_cast(nIndex), FALSE); + }); + } } void GridBinding::DeselectAll() noexcept @@ -449,6 +449,12 @@ void GridBinding::UpdateRow(gsl::index nIndex, bool bExisting) if (m_pUpdateSelectedItems) return; + if (!WindowBinding::IsOnUIThread()) + { + InvokeOnUIThread([this, nIndex, bExisting]() { UpdateRow(nIndex, bExisting); }); + return; + } + std::wstring sText; LV_ITEMW item{}; @@ -526,7 +532,9 @@ void GridBinding::OnViewModelRemoved(gsl::index nIndex) else { SuspendRedraw(); - ListView_DeleteItem(m_hWnd, nIndex); + InvokeOnUIThread([this, nIndex]() noexcept { + ListView_DeleteItem(m_hWnd, nIndex); + }); } if (!m_vmItems->IsUpdating()) @@ -573,6 +581,12 @@ void GridBinding::OnEndViewModelCollectionUpdate() { if (m_hWnd) { + if (!WindowBinding::IsOnUIThread()) + { + WindowBinding::InvokeOnUIThread([this]() { OnEndViewModelCollectionUpdate(); }); + return; + } + if (m_bRedrawSuspended) { // enable redraw before calling CheckForScrollBar to ensure metrics are updated @@ -606,13 +620,15 @@ void GridBinding::OnEndViewModelCollectionUpdate() } } -void GridBinding::SuspendRedraw() noexcept +void GridBinding::SuspendRedraw() { if (!m_bRedrawSuspended && m_vmItems->IsUpdating()) { // if we're in a BeginUpdate/EndUpdate block, stop redrawing until the EndUpdate m_bRedrawSuspended = true; - SendMessage(m_hWnd, WM_SETREDRAW, FALSE, 0); + InvokeOnUIThread([this]() noexcept { + SendMessage(m_hWnd, WM_SETREDRAW, FALSE, 0); + }); } } diff --git a/src/ui/win32/bindings/GridBinding.hh b/src/ui/win32/bindings/GridBinding.hh index c0039266..3fdf7192 100644 --- a/src/ui/win32/bindings/GridBinding.hh +++ b/src/ui/win32/bindings/GridBinding.hh @@ -74,7 +74,7 @@ public: virtual void EnsureVisible(gsl::index nIndex) noexcept(false); protected: - void SuspendRedraw() noexcept; + void SuspendRedraw(); void UpdateLayout(); virtual void UpdateAllItems(); virtual void UpdateItems(gsl::index nColumn); diff --git a/src/ui/win32/bindings/TextBoxBinding.hh b/src/ui/win32/bindings/TextBoxBinding.hh index 7f0e4f8d..aead9b61 100644 --- a/src/ui/win32/bindings/TextBoxBinding.hh +++ b/src/ui/win32/bindings/TextBoxBinding.hh @@ -98,7 +98,11 @@ protected: void OnViewModelBoolValueChanged(const BoolModelProperty::ChangeArgs& args) override { if (m_pReadOnlyProperty && *m_pReadOnlyProperty == args.Property) - UpdateReadOnly(); + { + InvokeOnUIThread([this]() { + UpdateReadOnly(); + }); + } } INT_PTR CALLBACK WndProc(HWND hControl, UINT uMsg, WPARAM wParam, LPARAM lParam) override @@ -135,7 +139,9 @@ protected: virtual void UpdateTextFromSource(const std::wstring& sText) noexcept(false) { - SetWindowTextW(m_hWnd, sText.c_str()); + InvokeOnUIThread([this, sTextCopy = sText]() noexcept { + SetWindowTextW(m_hWnd, sTextCopy.c_str()); + }); } virtual void UpdateSourceFromText(const std::wstring& sText) diff --git a/src/ui/win32/bindings/WindowBinding.cpp b/src/ui/win32/bindings/WindowBinding.cpp index e53718a7..13f81c8b 100644 --- a/src/ui/win32/bindings/WindowBinding.cpp +++ b/src/ui/win32/bindings/WindowBinding.cpp @@ -1,6 +1,8 @@ #include "WindowBinding.hh" #include "RA_Core.h" +#include "RA_Log.h" +#include "RA_Resource.h" #include "data\ModelProperty.hh" @@ -16,6 +18,37 @@ namespace ui { namespace win32 { namespace bindings { +class DispatchingWindow : public DialogBase +{ +public: + explicit DispatchingWindow(WindowViewModelBase& vmWindow) : DialogBase(vmWindow) {} + virtual ~DispatchingWindow() noexcept = default; + DispatchingWindow(const DispatchingWindow&) noexcept = delete; + DispatchingWindow& operator=(const DispatchingWindow&) noexcept = delete; + DispatchingWindow(DispatchingWindow&&) noexcept = delete; + DispatchingWindow& operator=(DispatchingWindow&&) noexcept = delete; + + class ViewModel : public WindowViewModelBase + { + }; + + class Presenter : public IDialogPresenter + { + public: + bool IsSupported(const ra::ui::WindowViewModelBase&) noexcept override { return true; } + void ShowWindow(ra::ui::WindowViewModelBase&) noexcept override {} + void ShowModal(ra::ui::WindowViewModelBase&, HWND) noexcept override {} + }; + + Presenter m_pPresenter; + +private: + + ViewModel m_vmViewModel; +}; +static std::unique_ptr s_pDispatchingWindow; +static DispatchingWindow::ViewModel s_vmDispatchingWindow; + std::vector WindowBinding::s_vKnownBindings; DWORD WindowBinding::s_hUIThreadId; @@ -94,14 +127,6 @@ void WindowBinding::SetHWND(DialogBase* pDialog, HWND hWnd) RestoreSizeAndPosition(); } - - if (m_pDialog) - { - InvokeOnUIThread([this]() noexcept - { - s_hUIThreadId = GetCurrentThreadId(); - }); - } } void WindowBinding::UpdateAppTitle() @@ -300,7 +325,11 @@ void WindowBinding::OnViewModelStringValueChanged(const StringModelProperty::Cha const auto pIter = m_mLabelBindings.find(args.Property.GetKey()); if (pIter != m_mLabelBindings.end()) - SetDlgItemTextW(m_hWnd, pIter->second, args.tNewValue.c_str()); + { + InvokeOnUIThread([this, nDlgItemId = pIter->second, sValue = args.tNewValue]() noexcept { + SetDlgItemTextW(m_hWnd, nDlgItemId, sValue.c_str()); + }); + } } void WindowBinding::BindLabel(int nDlgItemId, const StringModelProperty& pSourceProperty) @@ -319,8 +348,9 @@ void WindowBinding::OnViewModelIntValueChanged(const IntModelProperty::ChangeArg const auto pIter = m_mLabelBindings.find(args.Property.GetKey()); if (pIter != m_mLabelBindings.end()) { - const auto sText = std::to_wstring(args.tNewValue); - SetDlgItemTextW(m_hWnd, pIter->second, sText.c_str()); + InvokeOnUIThread([this, nDlgItemId = pIter->second, sText = std::to_wstring(args.tNewValue)]() noexcept { + SetDlgItemTextW(m_hWnd, nDlgItemId, sText.c_str()); + }); } if (m_pDialog && args.Property == WindowViewModelBase::DialogResultProperty) @@ -351,7 +381,9 @@ void WindowBinding::OnViewModelBoolValueChanged(const BoolModelProperty::ChangeA auto hControl = GetDlgItem(m_hWnd, nDlgItemId); if (hControl) { - EnableWindow(hControl, args.tNewValue ? TRUE : FALSE); + InvokeOnUIThread([hControl, bValue = args.tNewValue ? TRUE : FALSE]() noexcept { + EnableWindow(hControl, bValue); + }); bRepaint = true; } } @@ -369,7 +401,9 @@ void WindowBinding::OnViewModelBoolValueChanged(const BoolModelProperty::ChangeA if (bVisible && m_vMultipleVisibilityBoundControls.find(nDlgItemId) != m_vMultipleVisibilityBoundControls.end()) bVisible = CheckMultiBoundVisibility(nDlgItemId); - ShowWindow(hControl, bVisible ? SW_SHOW : SW_HIDE); + InvokeOnUIThread([hControl, bVisible]() noexcept { + ShowWindow(hControl, bVisible ? SW_SHOW : SW_HIDE); + }); bRepaint = true; } } @@ -489,10 +523,73 @@ bool WindowBinding::GetValueFromAny(const BoolModelProperty& pProperty) const return bValue; } +void WindowBinding::SetUIThread(DWORD hThreadId) noexcept +{ + if (s_hUIThreadId == hThreadId) + return; + + s_hUIThreadId = hThreadId; + + s_pDispatchingWindow.reset(); +} + +void WindowBinding::EnableInvokeOnUIThread() +{ + if (s_pDispatchingWindow == nullptr && IsOnUIThread()) + { + // create a hidden dummy window for dispatching UI messages + + s_pDispatchingWindow.reset(new DispatchingWindow(s_vmDispatchingWindow)); + if (!s_pDispatchingWindow->CreateDialogWindow(MAKEINTRESOURCE(IDD_RA_RICHPRESENCE), + &s_pDispatchingWindow->m_pPresenter)) + { + RA_LOG_ERR("Could not create Code Notes dialog!"); + s_pDispatchingWindow.reset(); + } + } +} + void WindowBinding::InvokeOnUIThread(std::function fAction) { - Expects(m_pDialog != nullptr); - m_pDialog->QueueFunction(fAction); + if (s_pDispatchingWindow == nullptr) + { + // no window to post the action to - just run it + fAction(); + } + else if (IsOnUIThread()) + { + // already on the UI thread + fAction(); + } + else + { + // queue the function to be called from the UI thread + s_pDispatchingWindow->QueueFunction(fAction); + } +} + +void WindowBinding::InvokeOnUIThreadAndWait(std::function fAction) +{ + if (s_pDispatchingWindow == nullptr || IsOnUIThread()) + { + fAction(); + } + else + { + std::mutex pMutex; + std::condition_variable pCondition; + bool bExecuted = false; + + s_pDispatchingWindow->QueueFunction([fAction, &pCondition, &bExecuted]() { + fAction(); + + bExecuted = true; + pCondition.notify_all(); + }); + + std::unique_lock lock(pMutex); + pCondition.wait(lock, [&bExecuted]() { return bExecuted; }); + } } } // namespace bindings diff --git a/src/ui/win32/bindings/WindowBinding.hh b/src/ui/win32/bindings/WindowBinding.hh index cc86ac95..4337c559 100644 --- a/src/ui/win32/bindings/WindowBinding.hh +++ b/src/ui/win32/bindings/WindowBinding.hh @@ -126,14 +126,29 @@ public: void OnPositionChanged(ra::ui::Position oPosition); /// - /// Executes a function on the UI thread. + /// Specifies the UI thread. /// - void InvokeOnUIThread(std::function fAction); + static void SetUIThread(DWORD hUIThreadId) noexcept; + + /// + /// Ensures the UI thread dispatcher is enabled. + /// + static void EnableInvokeOnUIThread(); /// /// Returns true if the current thread is the UI thread. /// - bool IsOnUIThread() const noexcept { return GetCurrentThreadId() == s_hUIThreadId; } + static bool IsOnUIThread() noexcept { return GetCurrentThreadId() == s_hUIThreadId; } + + /// + /// Dispatches a function to the UI thread. + /// + static void InvokeOnUIThread(std::function fAction); + + /// + /// Executes a function on the UI thread and waits for it to complete. + /// + static void InvokeOnUIThreadAndWait(std::function fAction); protected: // ViewModelBase::NotifyTarget diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj index 09a7876c..3d788cc6 100644 --- a/tests/RA_Integration.Tests.vcxproj +++ b/tests/RA_Integration.Tests.vcxproj @@ -313,6 +313,7 @@ + diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters index 98a2c0ad..150cf5fa 100644 --- a/tests/RA_Integration.Tests.vcxproj.filters +++ b/tests/RA_Integration.Tests.vcxproj.filters @@ -462,6 +462,9 @@ Mocks + + Code + diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp index c5230976..140b5232 100644 --- a/tests/data/context/GameContext_Tests.cpp +++ b/tests/data/context/GameContext_Tests.cpp @@ -170,7 +170,7 @@ TEST_CLASS(GameContext_Tests) "}}"); mockAchievementRuntime.MockResponse( "r=startsession&u=Username&t=ApiToken&g=" + std::to_string(nGameID) + - "&l=" RCHEEVOS_VERSION_STRING, + "&h=1&m=" + sHash + "&l=" RCHEEVOS_VERSION_STRING, sUnlocks.empty() ? "{\"Success\":true}" : "{\"Success\":true," + sUnlocks + "}"); } diff --git a/tests/mocks/MockDesktop.hh b/tests/mocks/MockDesktop.hh index c363de97..60992158 100644 --- a/tests/mocks/MockDesktop.hh +++ b/tests/mocks/MockDesktop.hh @@ -94,8 +94,8 @@ public: bool IsDebuggerPresent() const noexcept override { return m_bDebuggerPresent; } void SetDebuggerPresent(bool bValue) noexcept { m_bDebuggerPresent = bValue; } - bool IsOnUIThread(const WindowViewModelBase&) const override { return true; } - void InvokeOnUIThread(const WindowViewModelBase&, std::function fAction) const override { fAction(); } + bool IsOnUIThread() const override { return true; } + void InvokeOnUIThread(std::function fAction) const override { fAction(); } private: ra::ui::DialogResult Handle(WindowViewModelBase& vmViewModel) const diff --git a/tests/services/AchievementRuntime_Tests.cpp b/tests/services/AchievementRuntime_Tests.cpp index 1767a6c9..c8fa2ff1 100644 --- a/tests/services/AchievementRuntime_Tests.cpp +++ b/tests/services/AchievementRuntime_Tests.cpp @@ -26,8 +26,11 @@ #ifdef RC_CLIENT_EXPORTS_EXTERNAL #include +#include #include "Exports.hh" +#include "RA_Resource.h" extern "C" API int CCONV _Rcheevos_GetExternalClient(rc_client_external_t* pClientExternal, int nVersion); +extern "C" API const rc_client_raintegration_menu_t* CCONV _Rcheevos_RAIntegrationGetMenu(); #endif using namespace Microsoft::VisualStudio::CppUnitTestFramework; @@ -3181,6 +3184,97 @@ TEST_CLASS(AchievementRuntime_Tests) AssertV1Exports(pClient); } + + TEST_METHOD(TestRAIntegrationGetMenu) + { + AchievementRuntimeHarness runtime; + runtime.mockUserContext.Logout(); + + const rc_client_raintegration_menu_t* pMenu; + + auto AssertMenuItem = [&pMenu](int nIndex, uint32_t nId, const char* label) { + const auto* pItem = &pMenu->items[nIndex]; + Assert::AreEqual(nId, pItem->id); + Assert::AreEqual(std::string(label), std::string(pItem->label)); + Assert::AreEqual({1}, pItem->enabled); + Assert::AreEqual({0}, pItem->checked); + }; + + auto AssertMenuItemChecked = [&pMenu](int nIndex, uint32_t nId, const char* label) { + const auto* pItem = &pMenu->items[nIndex]; + Assert::AreEqual(nId, pItem->id); + Assert::AreEqual(std::string(label), std::string(pItem->label)); + Assert::AreEqual({1}, pItem->enabled); + Assert::AreEqual({1}, pItem->checked); + }; + + auto AssertMenuSeparator = [&pMenu](int nIndex) { + const auto* pItem = &pMenu->items[nIndex]; + Assert::AreEqual(0U, pItem->id); + Assert::IsNull(pItem->label); + Assert::AreEqual({1}, pItem->enabled); + Assert::AreEqual({0}, pItem->checked); + }; + + pMenu = _Rcheevos_RAIntegrationGetMenu(); + Assert::AreEqual(11U, pMenu->num_items); + AssertMenuItem(0, IDM_RA_HARDCORE_MODE, "&Hardcore Mode"); + AssertMenuItem(1, IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning"); + AssertMenuSeparator(2); + AssertMenuItem(3, IDM_RA_FILES_OPENALL, "&Open All"); + AssertMenuItem(4, IDM_RA_FILES_ACHIEVEMENTS, "Assets Li&st"); + AssertMenuItem(5, IDM_RA_FILES_ACHIEVEMENTEDITOR, "Assets &Editor"); + AssertMenuItem(6, IDM_RA_FILES_MEMORYFINDER, "&Memory Inspector"); + AssertMenuItem(7, IDM_RA_FILES_MEMORYBOOKMARKS, "Memory &Bookmarks"); + AssertMenuItem(8, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); + AssertMenuItem(9, IDM_RA_FILES_CODENOTES, "Code &Notes"); + AssertMenuItem(10, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); + + runtime.mockUserContext.Initialize("User", "ApiToken"); + + pMenu = _Rcheevos_RAIntegrationGetMenu(); + Assert::AreEqual(17U, pMenu->num_items); + AssertMenuItem(0, IDM_RA_OPENUSERPAGE, "Open my &User Page"); + AssertMenuItem(1, IDM_RA_OPENGAMEPAGE, "Open this &Game's Page"); + AssertMenuSeparator(2); + AssertMenuItem(3, IDM_RA_HARDCORE_MODE, "&Hardcore Mode"); + AssertMenuItem(4, IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning"); + AssertMenuSeparator(5); + AssertMenuItem(6, IDM_RA_FILES_OPENALL, "&Open All"); + AssertMenuItem(7, IDM_RA_FILES_ACHIEVEMENTS, "Assets Li&st"); + AssertMenuItem(8, IDM_RA_FILES_ACHIEVEMENTEDITOR, "Assets &Editor"); + AssertMenuItem(9, IDM_RA_FILES_MEMORYFINDER, "&Memory Inspector"); + AssertMenuItem(10, IDM_RA_FILES_MEMORYBOOKMARKS, "Memory &Bookmarks"); + AssertMenuItem(11, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); + AssertMenuItem(12, IDM_RA_FILES_CODENOTES, "Code &Notes"); + AssertMenuItem(13, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); + AssertMenuSeparator(14); + AssertMenuItem(15, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); + AssertMenuItem(16, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); + + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::NonHardcoreWarning, true); + + pMenu = _Rcheevos_RAIntegrationGetMenu(); + Assert::AreEqual(17U, pMenu->num_items); + AssertMenuItem(0, IDM_RA_OPENUSERPAGE, "Open my &User Page"); + AssertMenuItem(1, IDM_RA_OPENGAMEPAGE, "Open this &Game's Page"); + AssertMenuSeparator(2); + AssertMenuItemChecked(3, IDM_RA_HARDCORE_MODE, "&Hardcore Mode"); + AssertMenuItemChecked(4, IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning"); + AssertMenuSeparator(5); + AssertMenuItem(6, IDM_RA_FILES_OPENALL, "&Open All"); + AssertMenuItem(7, IDM_RA_FILES_ACHIEVEMENTS, "Assets Li&st"); + AssertMenuItem(8, IDM_RA_FILES_ACHIEVEMENTEDITOR, "Assets &Editor"); + AssertMenuItem(9, IDM_RA_FILES_MEMORYFINDER, "&Memory Inspector"); + AssertMenuItem(10, IDM_RA_FILES_MEMORYBOOKMARKS, "Memory &Bookmarks"); + AssertMenuItem(11, IDM_RA_FILES_POINTERFINDER, "Pointer &Finder"); + AssertMenuItem(12, IDM_RA_FILES_CODENOTES, "Code &Notes"); + AssertMenuItem(13, IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); + AssertMenuSeparator(14); + AssertMenuItem(15, IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); + AssertMenuItem(16, IDM_RA_GETROMCHECKSUM, "View Game H&ash"); + } #endif }; diff --git a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp index d7511368..8937c911 100644 --- a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp +++ b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp @@ -603,7 +603,7 @@ TEST_CLASS(OverlayAchievementsPageViewModel_Tests) achievementsPage.AssertHeader(0, L"Locked"); achievementsPage.AssertProgressAchievement(1, pAch1, 0.0, L""); - achievementsPage.AssertProgressAchievement(2, pAch2, 50.0, L"5/10"); + achievementsPage.AssertProgressAchievement(2, pAch2, 0.5, L"5/10"); achievementsPage.AssertProgressAchievement(3, pAch3, 0.0, L""); achievementsPage.AssertHeader(4, L"Unlocked"); achievementsPage.AssertUnlockedAchievement(5, pAch4); @@ -652,10 +652,10 @@ TEST_CLASS(OverlayAchievementsPageViewModel_Tests) Assert::AreEqual(std::wstring(L"2 of 10 points"), achievementsPage.GetTitleDetail()); achievementsPage.AssertHeader(0, L"Almost There"); - achievementsPage.AssertProgressAchievement(1, pAch3, 90.0, L"9/10"); + achievementsPage.AssertProgressAchievement(1, pAch3, 0.9f, L"9/10"); achievementsPage.AssertHeader(2, L"Locked"); - achievementsPage.AssertProgressAchievement(3, pAch1, 10.0, L"1/10"); - achievementsPage.AssertProgressAchievement(4, pAch4, 0.0, L""); + achievementsPage.AssertProgressAchievement(3, pAch1, 0.1f, L"1/10"); + achievementsPage.AssertProgressAchievement(4, pAch4, 0.0f, L""); achievementsPage.AssertHeader(5, L"Unlocked"); achievementsPage.AssertUnlockedAchievement(6, pAch2); Assert::IsNull(achievementsPage.GetItem(7)); @@ -704,10 +704,10 @@ TEST_CLASS(OverlayAchievementsPageViewModel_Tests) Assert::AreEqual(std::wstring(L"0 of 10 points"), achievementsPage.GetTitleDetail()); achievementsPage.AssertHeader(0, L"Almost There"); - achievementsPage.AssertProgressAchievement(1, pAch3, 95.0, L"95/100"); - achievementsPage.AssertProgressAchievement(2, pAch1, 90.0, L"90/100"); - achievementsPage.AssertProgressAchievement(3, pAch2, 85.0, L"85/100"); - achievementsPage.AssertProgressAchievement(4, pAch4, 85.0, L"85/100"); + achievementsPage.AssertProgressAchievement(1, pAch3, 0.95f, L"95/100"); + achievementsPage.AssertProgressAchievement(2, pAch1, 0.90f, L"90/100"); + achievementsPage.AssertProgressAchievement(3, pAch2, 0.85f, L"85/100"); + achievementsPage.AssertProgressAchievement(4, pAch4, 0.85f, L"85/100"); Assert::IsNull(achievementsPage.GetItem(5)); }