diff --git a/.clang-format b/.clang-format index 30a5c717..22bcdbd9 100644 --- a/.clang-format +++ b/.clang-format @@ -2,16 +2,32 @@ #Some settings are unavailable but should be already taken care of +Language: Cpp +Standard: Cpp11 +MaxEmptyLinesToKeep: 1 +ColumnLimit: 120 #should between 80-120 so it'll be easier to see diffs +UseTab: Never +IndentWidth: 4 + +SortIncludes: true +IncludeBlocks: Preserve #Sorts includes separately +SortUsingDeclarations: true +CompactNamespaces: false #Puts em all on the same line +FixNamespaceComments: true +NamespaceIndentation: None + AlignAfterOpenBracket: true AlignEscapedNewlinesLeft: true AlignTrailingComments: true AlignConsecutiveAssignments: false +IndentCaseLabels: true +AllowShortCaseLabelsOnASingleLine: false + AllowAllParametersOfDeclarationOnNextLine: true -AllowShortFunctionsOnASingleLine: All +AllowShortFunctionsOnASingleLine: InlineOnly AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false -AllowShortCaseLabelsOnASingleLine: false AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: true @@ -30,25 +46,14 @@ BraceWrapping: BreakBeforeTernaryOperators: true BreakConstructorInitializers: AfterColon -ColumnLimit: 120 #should between 80-120 so it'll be easier to see diffs ConstructorInitializerAllOnOneLineOrOnePerLine: true -Cpp11BracedListStyle: true DerivePointerAlignment: true #Tries to stay consistent with existing code ExperimentalAutoDetectBinPacking: true -IndentCaseLabels: true -IndentWidth: 4 IndentWrappedFunctionNames: true KeepEmptyLinesAtTheStartOfBlocks: false -Language: Cpp -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: None PointerAlignment: Left ReflowComments: true -Standard: Cpp11 -UseTab: Never - -AccessModifierOffset: -4 SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements @@ -59,8 +64,7 @@ SpacesInParentheses: false SpacesInCStyleCastParentheses: false SpacesInSquareBrackets: false -SortUsingDeclarations: true -SortIncludes: true -IncludeBlocks: Preserve #Sorts includes separately -FixNamespaceComments: true -CompactNamespaces: false #Puts em all on the same line +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: Always +IndentAccessModifiers: false +AccessModifierOffset: -4 diff --git a/rcheevos b/rcheevos index df90bf67..6f02e791 160000 --- a/rcheevos +++ b/rcheevos @@ -1 +1 @@ -Subproject commit df90bf678fe0ac0a8a1775584f6f41a89cec9081 +Subproject commit 6f02e791aa202de5eaf3e86ed269dda6d456b779 diff --git a/src/Exports.cpp b/src/Exports.cpp index 4a97fefe..2db260b3 100644 --- a/src/Exports.cpp +++ b/src/Exports.cpp @@ -23,7 +23,6 @@ #include "services\IFileSystem.hh" #include "services\Initialization.hh" #include "services\PerformanceCounter.hh" -#include "services\RcheevosClient.hh" #include "services\ServiceLocator.hh" #include "ui\drawing\gdi\GDISurface.hh" @@ -108,7 +107,7 @@ static void InitializeOfflineMode() pSessionTracker.Initialize(sUsername); } - auto* pClient = ra::services::ServiceLocator::Get().GetClient(); + auto* pClient = ra::services::ServiceLocator::Get().GetClient(); pClient->user.username = rc_buf_strcpy(&pClient->state.buffer, pUserContext.GetUsername().c_str()); pClient->user.display_name = rc_buf_strcpy(&pClient->state.buffer, pUserContext.GetDisplayName().c_str()); pClient->user.token = rc_buf_strcpy(&pClient->state.buffer, pConfiguration.GetApiToken().c_str()); @@ -325,15 +324,15 @@ API void CCONV _RA_AttemptLogin(int bBlocking) } else { - auto& pClient = ra::services::ServiceLocator::GetMutable(); + auto& pClient = ra::services::ServiceLocator::GetMutable(); if (bBlocking) { - ra::services::RcheevosClient::Synchronizer pSynchronizer; + ra::services::AchievementRuntime::Synchronizer pSynchronizer; pClient.BeginLoginWithToken(pConfiguration.GetUsername(), pConfiguration.GetApiToken(), [](int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) { - auto* pSynchronizer = static_cast(pUserdata); + auto* pSynchronizer = static_cast(pUserdata); Expects(pSynchronizer != nullptr); pSynchronizer->CaptureResult(nResult, sErrorMessage); @@ -443,300 +442,6 @@ API void _RA_NavigateOverlay(const ControllerInput* pInput) ra::services::ServiceLocator::GetMutable().Update(*pInput); } -static void ProcessAchievements() -{ - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - if (pRuntime.IsPaused()) - return; - - auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - - TALLY_PERFORMANCE(PerformanceCheckpoint::RuntimeProcess); - std::vector vChanges; - pRuntime.Process(vChanges); - - const ra::data::models::AchievementModel* vmMeasuredAchievement = nullptr; - const rc_trigger_t* pMeasuredAchievementTrigger = nullptr; - float fMeasuredAchievementPercent = 0.0; - - TALLY_PERFORMANCE(PerformanceCheckpoint::RuntimeEvents); - for (const auto& pChange : vChanges) - { - switch (pChange.nType) - { - case ra::services::AchievementRuntime::ChangeType::AchievementReset: - { - const auto* pAchievement = pGameContext.Assets().FindAchievement(pChange.nId); - if (pAchievement && pAchievement->IsPauseOnReset()) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnReset(pAchievement->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::AchievementTriggered: - { - // AwardAchievement may detach the trigger, which would result in the state changing to Inactive. - // explicitly set the state to Triggered before the trigger gets detached. - auto* vmAchievement = pGameContext.Assets().FindAchievement(pChange.nId); - if (vmAchievement) - vmAchievement->SetState(ra::data::models::AssetState::Triggered); - - pGameContext.AwardAchievement(pChange.nId); - - if (vmAchievement && vmAchievement->IsPauseOnTrigger()) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnTrigger(vmAchievement->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::AchievementDisabled: - { - auto* vmAchievement = pGameContext.Assets().FindAchievement(pChange.nId); - if (vmAchievement) - { - RA_LOG_WARN("Achievement #%u disabled: address %s not supported by current core", pChange.nId, ra::ByteAddressToString(pChange.nValue)); - vmAchievement->SetState(ra::data::models::AssetState::Disabled); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::AchievementPrimed: - { - const auto* vmAchievement = pGameContext.Assets().FindAchievement(pChange.nId); - if (vmAchievement) - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Challenge) != ra::ui::viewmodels::PopupLocation::None) - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.AddChallengeIndicator(vmAchievement->GetID(), ra::ui::ImageType::Badge, ra::Narrow(vmAchievement->GetBadge())); - } - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::AchievementUnprimed: - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.RemoveChallengeIndicator(pChange.nId); - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardStarted: - { - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted) != ra::ui::viewmodels::PopupLocation::None) - { - ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\lb.wav"); - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardStarted, - L"Leaderboard attempt started", pLeaderboard->GetName(), pLeaderboard->GetDescription()); - } - - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) != ra::ui::viewmodels::PopupLocation::None) - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - auto& pScoreTracker = pOverlayManager.AddScoreTracker(pLeaderboard->GetID()); - const auto sDisplayText = pLeaderboard->FormatScore(pChange.nValue); - pScoreTracker.SetDisplayText(ra::Widen(sDisplayText)); - } - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardUpdated: - { - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - auto* pScoreTracker = pOverlayManager.GetScoreTracker(pChange.nId); - if (pScoreTracker != nullptr) - { - const auto sDisplayText = pLeaderboard->FormatScore(pChange.nValue); - pScoreTracker->SetDisplayText(ra::Widen(sDisplayText)); - } - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardCanceled: - { - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled) != ra::ui::viewmodels::PopupLocation::None) - { - ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\lbcancel.wav"); - - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardCanceled, - L"Leaderboard attempt failed", pLeaderboard->GetName(), pLeaderboard->GetDescription()); - } - - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.RemoveScoreTracker(pLeaderboard->GetID()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardTriggered: - { - pGameContext.SubmitLeaderboardEntry(pChange.nId, pChange.nValue); // will show the scoreboard when submission completes - - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - pOverlayManager.RemoveScoreTracker(pChange.nId); - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardStartReset: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnReset(L"Start: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardSubmitReset: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnReset(L"Submit: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardCancelReset: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnReset(L"Cancel: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardValueReset: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnReset(L"Value: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardStartTriggered: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnTrigger(L"Start: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardSubmitTriggered: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnTrigger(L"Submit: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::LeaderboardCancelTriggered: - { - /* ASSERT: this event is only raised if the leaderboard is watching for it */ - const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(pChange.nId); - if (pLeaderboard) - { - auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - pFrameEventQueue.QueuePauseOnTrigger(L"Cancel: " + pLeaderboard->GetName()); - } - - break; - } - - case ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged: - { - const auto* pTrigger = pRuntime.GetAchievementTrigger(pChange.nId); - if (pTrigger != nullptr && pTrigger->measured_target > 0) - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Progress) != ra::ui::viewmodels::PopupLocation::None) - { - const auto* vmAchievement = pGameContext.Assets().FindAchievement(pChange.nId); - if (vmAchievement != nullptr) - { - const float fProgress = gsl::narrow_cast(pTrigger->measured_value) / gsl::narrow_cast(pTrigger->measured_target); - if (fProgress > fMeasuredAchievementPercent) - { - fMeasuredAchievementPercent = fProgress; - vmMeasuredAchievement = vmAchievement; - pMeasuredAchievementTrigger = pTrigger; - } - } - } - } - - break; - } - } - } - - if (vmMeasuredAchievement && pMeasuredAchievementTrigger) - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - - // use locked badge if available - auto sBadgeName = ra::Narrow(vmMeasuredAchievement->GetBadge()); - if (!ra::StringStartsWith(sBadgeName, "local\\")) - sBadgeName += "_lock"; - - pOverlayManager.UpdateProgressTracker(ra::ui::ImageType::Badge, sBadgeName, - pMeasuredAchievementTrigger->measured_value, pMeasuredAchievementTrigger->measured_target, - pMeasuredAchievementTrigger->measured_as_percent); - } -} - API void CCONV _RA_SetPaused(int bIsPaused) { auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); @@ -782,8 +487,11 @@ API void CCONV _RA_DoAchievementsFrame() ra::ui::win32::bindings::ControlBinding::RepaintGuard guard; #endif - // make sure we process the achievements _before_ the frozen bookmarks modify the memory - ProcessAchievements(); + // make sure we process the achievements _before_ updating the UI. + // the frozen bookmarks may modify the memory + TALLY_PERFORMANCE(PerformanceCheckpoint::RuntimeProcess); + auto& pRcheevosClient = ra::services::ServiceLocator::GetMutable(); + pRcheevosClient.DoFrame(); #ifndef RA_UTEST UpdateUIForFrameChange(); @@ -820,7 +528,9 @@ API void CCONV _RA_OnSaveState(const char* sFilename) API int CCONV _RA_CaptureState(char* pBuffer, int nBufferSize) { - return ra::services::ServiceLocator::Get().SaveProgressToBuffer(pBuffer, nBufferSize); + GSL_SUPPRESS_TYPE1 + return ra::services::ServiceLocator::Get(). + SaveProgressToBuffer(reinterpret_cast(pBuffer), nBufferSize); } static bool CanRestoreState() @@ -847,6 +557,7 @@ static bool CanRestoreState() static void OnStateRestored() { + // TODO: rc_client should handle the UI - only need to sync assets const auto& pRuntime = ra::services::ServiceLocator::Get(); auto& pConfiguration = ra::services::ServiceLocator::GetMutable(); @@ -891,7 +602,7 @@ static void OnStateRestored() { auto& pScoreTracker = pOverlayManager.AddScoreTracker(pLeaderboard->GetID()); - const auto pDefinition = pRuntime.GetLeaderboardDefinition(pLeaderboard->GetID()); + const auto* pDefinition = pRuntime.GetLeaderboardDefinition(pLeaderboard->GetID()); if (pDefinition != nullptr) { const auto sDisplayText = pLeaderboard->FormatScore(pDefinition->value.value.value); @@ -923,7 +634,9 @@ API void CCONV _RA_RestoreState(const char* pBuffer) { if (CanRestoreState()) { - ra::services::ServiceLocator::GetMutable().LoadProgressFromBuffer(pBuffer); + GSL_SUPPRESS_TYPE1 + ra::services::ServiceLocator::GetMutable(). + LoadProgressFromBuffer(reinterpret_cast(pBuffer)); OnStateRestored(); } } diff --git a/src/RA_Core.cpp b/src/RA_Core.cpp index ef40fdfe..55ca0722 100644 --- a/src/RA_Core.cpp +++ b/src/RA_Core.cpp @@ -11,7 +11,6 @@ #include "services\IConfiguration.hh" #include "services\IThreadPool.hh" #include "services\Initialization.hh" -#include "services\RcheevosClient.hh" #include "services\ServiceLocator.hh" #include "ui\IDesktop.hh" @@ -147,7 +146,7 @@ API int CCONV _RA_Shutdown() pDesktop.CloseWindow(pWindowManager.PointerFinder); } - ra::services::ServiceLocator::GetMutable().Shutdown(); + ra::services::ServiceLocator::GetMutable().Shutdown(); ra::services::Initialization::Shutdown(); @@ -214,7 +213,7 @@ API void CCONV _RA_OnReset() // Temporarily disable achievements while the system is resetting. They will automatically re-enable when // DoAchievementsFrame is called if the trigger is not active. Prevents most unexpected triggering caused // by resetting the emulator. - ra::services::ServiceLocator::GetMutable().ResetActiveAchievements(); + ra::services::ServiceLocator::GetMutable().ResetRuntime(); ra::services::ServiceLocator::GetMutable().ClearPopups(); } diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj index 10cc5b5a..57bcbeeb 100644 --- a/src/RA_Integration.vcxproj +++ b/src/RA_Integration.vcxproj @@ -96,7 +96,6 @@ - @@ -175,7 +174,6 @@ - @@ -189,10 +187,7 @@ - - - @@ -265,7 +260,6 @@ - diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters index 3986d639..1cdf25b3 100644 --- a/src/RA_Integration.vcxproj.filters +++ b/src/RA_Integration.vcxproj.filters @@ -405,9 +405,6 @@ UI\ViewModels - - Services - @@ -626,9 +623,6 @@ API - - API - UI\ViewModels @@ -647,9 +641,6 @@ Services - - API - UI\ViewModels @@ -668,9 +659,6 @@ API - - API - UI @@ -683,9 +671,6 @@ API - - API - UI\ViewModels @@ -986,9 +971,6 @@ UI\ViewModels - - Services - diff --git a/src/api/ApiCall.cpp b/src/api/ApiCall.cpp index 4cc40680..a3103152 100644 --- a/src/api/ApiCall.cpp +++ b/src/api/ApiCall.cpp @@ -15,9 +15,6 @@ static ra::api::IServer& Server() } StartSession::Response StartSession::Request::Call() const { return Server().StartSession(*this); } -Ping::Response Ping::Request::Call() const { return Server().Ping(*this); } -AwardAchievement::Response AwardAchievement::Request::Call() const { return Server().AwardAchievement(*this); } -SubmitLeaderboardEntry::Response SubmitLeaderboardEntry::Request::Call() const { return Server().SubmitLeaderboardEntry(*this); } FetchUserFriends::Response FetchUserFriends::Request::Call() const { return Server().FetchUserFriends(*this); } ResolveHash::Response ResolveHash::Request::Call() const { return Server().ResolveHash(*this); } FetchCodeNotes::Response FetchCodeNotes::Request::Call() const { return Server().FetchCodeNotes(*this); } diff --git a/src/api/ApiCall.hh b/src/api/ApiCall.hh index ffddb4a8..dafdc292 100644 --- a/src/api/ApiCall.hh +++ b/src/api/ApiCall.hh @@ -29,49 +29,6 @@ protected: ra::services::ServiceLocator::GetMutable().RunAsync( [request, callback = std::move(callback)]{ callback(request.Call()); }); } - - template - static void CallAsyncWithRetry(const TRequest& request, TCallback&& callback) - { - ra::services::ServiceLocator::GetMutable().RunAsync( - [requestCopy = request, callback] - { - DoAsyncWithRetry(requestCopy, callback, std::chrono::milliseconds(0)); - }); - } - -private: - template - static void DoAsyncWithRetry(const TRequest& request, TCallback&& callback, std::chrono::milliseconds delay) - { - auto response = request.Call(); - if (response.Result != ApiResult::Incomplete) - { - callback(response); - return; - } - - if (delay < std::chrono::milliseconds(500)) - { - delay = std::chrono::milliseconds(500); - } - else - { - delay += delay; - if (delay > std::chrono::minutes(2)) - delay = std::chrono::minutes(2); - } - - // TODO: this used to be requestCopy = std::move(request), but the newest analysis tool - // says not to move const items. This is the only solution I could find the satiate it. - // I'm not convinced this is creating a copy of request, but this code will go away after - // AwardAchievement and SubmitLeaderboardEntry are managed by rc_client_. - ra::services::ServiceLocator::GetMutable().ScheduleAsync(delay, - [requestCopy = request, callback, delay] - { - DoAsyncWithRetry(requestCopy, callback, delay); - }); - } }; struct ApiResponseBase diff --git a/src/api/AwardAchievement.hh b/src/api/AwardAchievement.hh deleted file mode 100644 index 37986429..00000000 --- a/src/api/AwardAchievement.hh +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef RA_API_AWARD_ACHIEVEMENT_HH -#define RA_API_AWARD_ACHIEVEMENT_HH -#pragma once - -#include "ApiCall.hh" - -namespace ra { -namespace api { - -class AwardAchievement -{ -public: - static constexpr const char* const Name() noexcept { return "AwardAchievement"; } - - struct Response : ApiResponseBase - { - unsigned int NewPlayerScore{ 0U }; - unsigned int AchievementsRemaining{ 0U }; - }; - - struct Request : ApiRequestBase - { - unsigned int AchievementId{ 0U }; - bool Hardcore{ false }; - std::string GameHash; - - using Callback = std::function; - - Response Call() const; - - void CallAsync(Callback&& callback) const - { - ApiRequestBase::CallAsync(*this, std::move(callback)); - } - - void CallAsyncWithRetry(Callback&& callback) const - { - ApiRequestBase::CallAsyncWithRetry(*this, std::move(callback)); - } - }; -}; - -} // namespace api -} // namespace ra - -#endif // !RA_API_AWARD_ACHIEVEMENT_HH diff --git a/src/api/IServer.hh b/src/api/IServer.hh index ef5abceb..a2d8ffe2 100644 --- a/src/api/IServer.hh +++ b/src/api/IServer.hh @@ -2,7 +2,6 @@ #define RA_API_ISERVER_HH #pragma once -#include "api/AwardAchievement.hh" #include "api/DeleteCodeNote.hh" #include "api/FetchAchievementInfo.hh" #include "api/FetchBadgeIds.hh" @@ -11,10 +10,8 @@ #include "api/FetchLeaderboardInfo.hh" #include "api/FetchUserFriends.hh" #include "api/LatestClient.hh" -#include "api/Ping.hh" #include "api/ResolveHash.hh" #include "api/StartSession.hh" -#include "api/SubmitLeaderboardEntry.hh" #include "api/SubmitNewTitle.hh" #include "api/SubmitTicket.hh" #include "api/UpdateAchievement.hh" @@ -32,9 +29,6 @@ public: // === user functions === virtual StartSession::Response StartSession(const StartSession::Request& request) = 0; - virtual Ping::Response Ping(const Ping::Request& request) = 0; - virtual AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) = 0; - virtual SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) = 0; virtual FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request& request) = 0; // === game functions === diff --git a/src/api/Ping.hh b/src/api/Ping.hh deleted file mode 100644 index 8b95abcd..00000000 --- a/src/api/Ping.hh +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef RA_API_PING_HH -#define RA_API_PING_HH -#pragma once - -#include "ApiCall.hh" - -namespace ra { -namespace api { - -class Ping -{ -public: - static constexpr const char* const Name() noexcept { return "Ping"; } - - struct Response : ApiResponseBase - { - }; - - struct Request : ApiRequestBase - { - unsigned int GameId{}; - std::wstring CurrentActivity; - - using Callback = std::function; - - Response Call() const; - - void CallAsync(Callback&& callback) const - { - ApiRequestBase::CallAsync(*this, std::move(callback)); - } - }; -}; - -} // namespace api -} // namespace ra - -#endif // !RA_API_PING_HH diff --git a/src/api/SubmitLeaderboardEntry.hh b/src/api/SubmitLeaderboardEntry.hh deleted file mode 100644 index bfd7945f..00000000 --- a/src/api/SubmitLeaderboardEntry.hh +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef RA_API_SUBMIT_LEADERBOARD_ENTRY_HH -#define RA_API_SUBMIT_LEADERBOARD_ENTRY_HH -#pragma once - -#include "ApiCall.hh" - -namespace ra { -namespace api { - -class SubmitLeaderboardEntry -{ -public: - static constexpr const char* const Name() noexcept { return "SubmitLeaderboardEntry"; } - - struct Response : ApiResponseBase - { - int Score{ 0U }; - int BestScore{ 0U }; - unsigned int NewRank{ 0U }; - unsigned int NumEntries{ 0U }; - - struct Entry - { - unsigned int Rank{ 0U }; - std::string User; - int Score{ 0U }; - }; - - std::vector TopEntries; - }; - - struct Request : ApiRequestBase - { - unsigned int LeaderboardId{ 0U }; - int Score{ 0U }; - std::string GameHash; - - using Callback = std::function; - - Response Call() const; - - void CallAsync(Callback&& callback) const - { - ApiRequestBase::CallAsync(*this, std::move(callback)); - } - - void CallAsyncWithRetry(Callback&& callback) const - { - ApiRequestBase::CallAsyncWithRetry(*this, std::move(callback)); - } - }; -}; - -} // namespace api -} // namespace ra - -#endif // !RA_API_SUBMIT_LEADERBOARD_ENTRY_HH diff --git a/src/api/impl/ConnectedServer.cpp b/src/api/impl/ConnectedServer.cpp index ca97878c..a49ea90f 100644 --- a/src/api/impl/ConnectedServer.cpp +++ b/src/api/impl/ConnectedServer.cpp @@ -506,138 +506,6 @@ StartSession::Response ConnectedServer::StartSession(const StartSession::Request return response; } -Ping::Response ConnectedServer::Ping(const Ping::Request& request) -{ - Ping::Response response; - - rc_api_ping_request_t api_params; - memset(&api_params, 0, sizeof(api_params)); - - const auto& pUserContext = ra::services::ServiceLocator::Get(); - api_params.username = pUserContext.GetUsername().c_str(); - api_params.api_token = pUserContext.GetApiToken().c_str(); - api_params.game_id = request.GameId; - - std::string sRichPresence; - if (!request.CurrentActivity.empty()) - { - sRichPresence = ra::Narrow(request.CurrentActivity); - api_params.rich_presence = sRichPresence.c_str(); - } - - rc_api_request_t api_request; - if (rc_api_init_ping_request(&api_request, &api_params) == RC_OK) - { - ra::services::Http::Response httpResponse; - if (DoRequest(api_request, Ping::Name(), httpResponse, response)) - { - rc_api_ping_response_t api_response; - const auto nResult = rc_api_process_ping_response(&api_response, httpResponse.Content().c_str()); - - if (ValidateResponse(nResult, api_response.response, Ping::Name(), httpResponse.StatusCode(), response)) - response.Result = ApiResult::Success; - - rc_api_destroy_ping_response(&api_response); - } - } - - rc_api_destroy_request(&api_request); - return response; -} - -AwardAchievement::Response ConnectedServer::AwardAchievement(const AwardAchievement::Request& request) -{ - AwardAchievement::Response response; - - rc_api_award_achievement_request_t api_params; - memset(&api_params, 0, sizeof(api_params)); - - const auto& pUserContext = ra::services::ServiceLocator::Get(); - api_params.username = pUserContext.GetUsername().c_str(); - api_params.api_token = pUserContext.GetApiToken().c_str(); - - if (!request.GameHash.empty()) - api_params.game_hash = request.GameHash.c_str(); - - api_params.achievement_id = request.AchievementId; - api_params.hardcore = request.Hardcore ? 1 : 0; - - rc_api_request_t api_request; - if (rc_api_init_award_achievement_request(&api_request, &api_params) == RC_OK) - { - ra::services::Http::Response httpResponse; - if (DoRequest(api_request, AwardAchievement::Name(), httpResponse, response)) - { - rc_api_award_achievement_response_t api_response; - const auto nResult = rc_api_process_award_achievement_response(&api_response, httpResponse.Content().c_str()); - - if (ValidateResponse(nResult, api_response.response, AwardAchievement::Name(), httpResponse.StatusCode(), response)) - { - response.Result = ApiResult::Success; - response.NewPlayerScore = api_response.new_player_score; - response.AchievementsRemaining = api_response.achievements_remaining; - } - - rc_api_destroy_award_achievement_response(&api_response); - } - } - - rc_api_destroy_request(&api_request); - return response; -} - -SubmitLeaderboardEntry::Response ConnectedServer::SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) -{ - SubmitLeaderboardEntry::Response response; - - rc_api_submit_lboard_entry_request_t api_params; - memset(&api_params, 0, sizeof(api_params)); - - const auto& pUserContext = ra::services::ServiceLocator::Get(); - api_params.username = pUserContext.GetUsername().c_str(); - api_params.api_token = pUserContext.GetApiToken().c_str(); - - api_params.leaderboard_id = request.LeaderboardId; - api_params.score = request.Score; - - if (!request.GameHash.empty()) - api_params.game_hash = request.GameHash.c_str(); - - rc_api_request_t api_request; - if (rc_api_init_submit_lboard_entry_request(&api_request, &api_params) == RC_OK) - { - ra::services::Http::Response httpResponse; - if (DoRequest(api_request, SubmitLeaderboardEntry::Name(), httpResponse, response)) - { - rc_api_submit_lboard_entry_response_t api_response; - const auto nResult = rc_api_process_submit_lboard_entry_response(&api_response, httpResponse.Content().c_str()); - - if (ValidateResponse(nResult, api_response.response, SubmitLeaderboardEntry::Name(), httpResponse.StatusCode(), response)) - { - response.Result = ApiResult::Success; - response.Score = api_response.submitted_score; - response.BestScore = api_response.best_score; - response.NewRank = api_response.new_rank; - response.NumEntries = api_response.num_entries; - - for (unsigned i = 0; i < api_response.num_top_entries; ++i) - { - const auto* pSrcEntry = &api_response.top_entries[i]; - auto& entry = response.TopEntries.emplace_back(); - entry.Rank = pSrcEntry->rank; - entry.Score = pSrcEntry->score; - entry.User = pSrcEntry->username; - } - } - - rc_api_destroy_submit_lboard_entry_response(&api_response); - } - } - - rc_api_destroy_request(&api_request); - return response; -} - FetchUserFriends::Response ConnectedServer::FetchUserFriends(const FetchUserFriends::Request&) { FetchUserFriends::Response response; diff --git a/src/api/impl/ConnectedServer.hh b/src/api/impl/ConnectedServer.hh index 2c5cc009..10900266 100644 --- a/src/api/impl/ConnectedServer.hh +++ b/src/api/impl/ConnectedServer.hh @@ -16,9 +16,6 @@ public: const char* Name() const noexcept override { return m_sHost.c_str(); } StartSession::Response StartSession(const StartSession::Request& request) override; - Ping::Response Ping(const Ping::Request& request) override; - AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) override; - SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) override; FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request& request) override; ResolveHash::Response ResolveHash(const ResolveHash::Request& request) override; FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) override; diff --git a/src/api/impl/OfflineServer.cpp b/src/api/impl/OfflineServer.cpp index fb5b92ee..ec53c54d 100644 --- a/src/api/impl/OfflineServer.cpp +++ b/src/api/impl/OfflineServer.cpp @@ -13,22 +13,6 @@ namespace ra { namespace api { namespace impl { -AwardAchievement::Response OfflineServer::AwardAchievement(const AwardAchievement::Request&) -{ - AwardAchievement::Response response; - response.Result = ApiResult::Failed; - response.ErrorMessage = "Achievements are not actually unlocked in offline mode"; - return response; -} - -SubmitLeaderboardEntry::Response OfflineServer::SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request&) -{ - SubmitLeaderboardEntry::Response response; - response.Result = ApiResult::Failed; - response.ErrorMessage = "Leaderboard entries are not actually submitted in offline mode"; - return response; -} - FetchCodeNotes::Response OfflineServer::FetchCodeNotes(const FetchCodeNotes::Request& request) { FetchCodeNotes::Response response; diff --git a/src/api/impl/OfflineServer.hh b/src/api/impl/OfflineServer.hh index 093f0116..8c4b696f 100644 --- a/src/api/impl/OfflineServer.hh +++ b/src/api/impl/OfflineServer.hh @@ -11,9 +11,6 @@ class OfflineServer : public ServerBase public: const char* Name() const noexcept override { return "offline client"; } - AwardAchievement::Response AwardAchievement(const AwardAchievement::Request&) override; - SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request&) override; - FetchCodeNotes::Response FetchCodeNotes(const FetchCodeNotes::Request& request) override; LatestClient::Response LatestClient(const LatestClient::Request& request) override; diff --git a/src/api/impl/ServerBase.hh b/src/api/impl/ServerBase.hh index 2f6ed646..ae88e48a 100644 --- a/src/api/impl/ServerBase.hh +++ b/src/api/impl/ServerBase.hh @@ -22,21 +22,6 @@ public: return UnsupportedApi(StartSession::Name()); } - Ping::Response Ping(const Ping::Request&) override - { - return UnsupportedApi(Ping::Name()); - } - - AwardAchievement::Response AwardAchievement(const AwardAchievement::Request&) override - { - return UnsupportedApi(AwardAchievement::Name()); - } - - SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request&) override - { - return UnsupportedApi(SubmitLeaderboardEntry::Name()); - } - FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request&) override { return UnsupportedApi(FetchUserFriends::Name()); diff --git a/src/data/context/EmulatorContext.cpp b/src/data/context/EmulatorContext.cpp index 01093165..8cff7dc0 100644 --- a/src/data/context/EmulatorContext.cpp +++ b/src/data/context/EmulatorContext.cpp @@ -458,8 +458,12 @@ void EmulatorContext::DisableHardcoreMode() RebuildMenu(); + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + rc_client_set_hardcore_enabled(pRuntime.GetClient(), false); + + // GameContext::DoFrame synchronizes the models to the runtime auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - pGameContext.RefreshUnlocks(); + pGameContext.DoFrame(); } } @@ -503,17 +507,6 @@ bool EmulatorContext::EnableHardcoreMode(bool bShowWarning) RA_LOG_INFO("Hardcore enabled"); - // Disable any modified assets - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pGameContext.Assets().Count()); ++nIndex) - { - auto* pAsset = pGameContext.Assets().GetItemAt(nIndex); - if (pAsset != nullptr && pAsset->GetChanges() != ra::data::models::AssetChanges::None) - { - if (pAsset->IsActive()) - pAsset->SetState(ra::data::models::AssetState::Inactive); - } - } - // User has agreed to reset the emulator, or no game is loaded. Enabled hardcore! pConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); @@ -542,15 +535,25 @@ bool EmulatorContext::EnableHardcoreMode(bool bShowWarning) // update the integration menu RebuildMenu(); - // when enabling hardcore mode, force a system reset - if (m_fResetEmulator) - m_fResetEmulator(); + // update the runtime + auto* pClient = ra::services::ServiceLocator::GetMutable().GetClient(); + rc_client_set_hardcore_enabled(pClient, true); -#ifndef RA_UTEST - // emulator reset code will normally call back into _RA_OnReset, but call it explicitly just in case. - _RA_OnReset(); -#endif + // GameContext::DoFrame synchronizes the models to the runtime + pGameContext.DoFrame(); + // Disable any modified assets + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pGameContext.Assets().Count()); ++nIndex) + { + auto* pAsset = pGameContext.Assets().GetItemAt(nIndex); + if (pAsset != nullptr && pAsset->GetChanges() != ra::data::models::AssetChanges::None) + { + if (pAsset->IsActive()) + pAsset->SetState(ra::data::models::AssetState::Inactive); + } + } + + // unfreeze bookmarks so the user doesn't get hit with a modified memory error auto& vmBookmarks = ra::services::ServiceLocator::GetMutable().MemoryBookmarks; for (gsl::index nIndex = 0; nIndex < ra::to_signed(vmBookmarks.Bookmarks().Count()); ++nIndex) { @@ -559,9 +562,6 @@ bool EmulatorContext::EnableHardcoreMode(bool bShowWarning) pBookmark->SetBehavior(ra::ui::viewmodels::MemoryBookmarksViewModel::BookmarkBehavior::None); } - pGameContext.ActivateLeaderboards(); - pGameContext.RefreshUnlocks(); - return true; } @@ -704,9 +704,8 @@ bool EmulatorContext::IsValidAddress(ra::ByteAddress nAddress) const noexcept return false; } -uint8_t EmulatorContext::ReadMemoryByte(ra::ByteAddress nAddress) const +uint8_t EmulatorContext::ReadMemoryByte(ra::ByteAddress nAddress) const noexcept { - const ra::ByteAddress nOriginalAddress = nAddress; for (const auto& pBlock : m_vMemoryBlocks) { if (nAddress < pBlock.size) @@ -720,16 +719,12 @@ uint8_t EmulatorContext::ReadMemoryByte(ra::ByteAddress nAddress) const nAddress -= gsl::narrow_cast(pBlock.size); } - if (nOriginalAddress < m_nTotalMemorySize) - ra::services::ServiceLocator::GetMutable().InvalidateAddress(nOriginalAddress); - return 0; } _Use_decl_annotations_ uint32_t EmulatorContext::ReadMemory(ra::ByteAddress nAddress, uint8_t pBuffer[], size_t nCount) const { - const ra::ByteAddress nOriginalAddress = nAddress; uint32_t nBytesRead = 0; Expects(pBuffer != nullptr); @@ -756,10 +751,8 @@ uint32_t EmulatorContext::ReadMemory(ra::ByteAddress nAddress, uint8_t pBuffer[] } else if (!pBlock.read) { - ra::services::ServiceLocator::GetMutable().InvalidateAddress(nOriginalAddress); memset(pBuffer, 0, nToRead); pBuffer += nToRead; - nBytesRead += gsl::narrow_cast(nToRead); } else { diff --git a/src/data/context/EmulatorContext.hh b/src/data/context/EmulatorContext.hh index 47618934..4f5dd5d2 100644 --- a/src/data/context/EmulatorContext.hh +++ b/src/data/context/EmulatorContext.hh @@ -138,6 +138,15 @@ public: m_fUnpauseEmulator(); } + /// + /// Resets the emulator. + /// + void Reset() const + { + if (m_fResetEmulator) + m_fResetEmulator(); + } + /// /// Sets a function to call to reset the emulator. /// @@ -215,7 +224,7 @@ public: /// /// Reads memory from the emulator. /// - uint8_t ReadMemoryByte(ra::ByteAddress nAddress) const; + uint8_t ReadMemoryByte(ra::ByteAddress nAddress) const noexcept; /// /// Reads memory from the emulator. diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp index 8eca6270..c6a967b2 100644 --- a/src/data/context/GameContext.cpp +++ b/src/data/context/GameContext.cpp @@ -7,9 +7,6 @@ #include "RA_md5factory.h" #include "RA_StringUtils.h" -#include "api\AwardAchievement.hh" -#include "api\SubmitLeaderboardEntry.hh" - #include "data\context\ConsoleContext.hh" #include "data\context\EmulatorContext.hh" #include "data\context\SessionTracker.hh" @@ -24,7 +21,6 @@ #include "services\IAudioSystem.hh" #include "services\IConfiguration.hh" #include "services\ILocalStorage.hh" -#include "services\RcheevosClient.hh" #include "services\impl\FileTextReader.hh" #include "services\impl\FileTextWriter.hh" #include "services\impl\StringTextReader.hh" @@ -149,17 +145,13 @@ void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, M } } - // download the game data - ra::services::RcheevosClient::Synchronizer pSynchronizer; - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - // enable spectator mode to prevent unlocks when testing compatibility - rc_client_set_spectator_mode_enabled(pClient.GetClient(), nMode == Mode::CompatibilityTest); + rc_client_set_spectator_mode_enabled(pRuntime.GetClient(), nMode == Mode::CompatibilityTest); const bool bWasPaused = pRuntime.IsPaused(); pRuntime.SetPaused(true); + // download the game data struct LoadGameUserData { bool bWasPaused = false; @@ -169,7 +161,7 @@ void GameContext::LoadGame(unsigned int nGameId, const std::string& sGameHash, M pLoadGameUserData->bWasPaused = bWasPaused; pLoadGameUserData->sOldRichPresence = std::move(sOldRichPresence); - pClient.BeginLoadGame(sGameHash, nGameId, + 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); @@ -201,7 +193,7 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW [this]() { EndLoad(); }); m_vAssets.Append(std::move(pCodeNotes)); - auto& pClient = ra::services::ServiceLocator::GetMutable(); + auto& pClient = ra::services::ServiceLocator::GetMutable(); auto* pGame = rc_client_get_game_info(pClient.GetClient()); if (pGame == nullptr || pGame->id == 0) { @@ -287,17 +279,17 @@ void GameContext::FinishLoadGame(int nResult, const char* sErrorMessage, bool bW // finish up m_vAssets.EndUpdate(); - auto& pRcheevosClient = ra::services::ServiceLocator::GetMutable(); - pRcheevosClient.SyncAssets(); + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + pRuntime.SyncAssets(); EndLoad(); OnActiveGameChanged(); } -void GameContext::InitializeFromRcheevosClient(const std::map mAchievementDefinitions, - const std::map mLeaderboardDefinitions) +void GameContext::InitializeFromAchievementRuntime(const std::map mAchievementDefinitions, + const std::map mLeaderboardDefinitions) { - const auto* pClient = ra::services::ServiceLocator::Get().GetClient(); + const auto* pClient = ra::services::ServiceLocator::Get().GetClient(); const auto* pGame = rc_client_get_game_info(pClient); m_sGameTitle = ra::Widen(pGame->title); @@ -309,107 +301,70 @@ void GameContext::InitializeFromRcheevosClient(const std::mapachievements; - const auto* pAchievementStop = pAchievementData + pSubset->public_.num_achievements; - for (; pAchievementData < pAchievementStop; ++pAchievementData) + if (pAchievementData != nullptr) { - // if the server has provided an unexpected category (usually 0), ignore it. - ra::data::models::AssetCategory nCategory = ra::data::models::AssetCategory::None; - switch (pAchievementData->public_.category) + const auto* pAchievementStop = pAchievementData + pSubset->public_.num_achievements; + for (; pAchievementData < pAchievementStop; ++pAchievementData) { - case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: - nCategory = ra::data::models::AssetCategory::Core; - break; - case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: - nCategory = ra::data::models::AssetCategory::Unofficial; + // if the server has provided an unexpected category (usually 0), ignore it. + ra::data::models::AssetCategory nCategory = ra::data::models::AssetCategory::None; + switch (pAchievementData->public_.category) + { + case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + nCategory = ra::data::models::AssetCategory::Core; + break; - // all unofficial achievements should start inactive. - // rc_client automatically activates them. - pAchievementData->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - break; - default: - continue; - } + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + nCategory = ra::data::models::AssetCategory::Unofficial; - auto vmAchievement = std::make_unique(); - vmAchievement->SetID(pAchievementData->public_.id); - vmAchievement->SetName(ra::Widen(pAchievementData->public_.title)); - vmAchievement->SetDescription(ra::Widen(pAchievementData->public_.description)); - vmAchievement->SetCategory(nCategory); - vmAchievement->SetPoints(pAchievementData->public_.points); - vmAchievement->SetAuthor(ra::Widen(pAchievementData->author)); - vmAchievement->SetBadge(ra::Widen(pAchievementData->public_.badge_name)); - vmAchievement->SetCreationTime(pAchievementData->created_time); - vmAchievement->SetUpdatedTime(pAchievementData->updated_time); + // all unofficial achievements should start inactive. + // rc_client automatically activates them. + pAchievementData->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - const auto sDefinition = mAchievementDefinitions.find(pAchievementData->public_.id); - if (sDefinition != mAchievementDefinitions.end()) - vmAchievement->SetTrigger(sDefinition->second); + if (pAchievementData->trigger) + pAchievementData->trigger->state = RC_TRIGGER_STATE_INACTIVE; + break; - vmAchievement->CreateServerCheckpoint(); - vmAchievement->CreateLocalCheckpoint(); + default: + continue; + } - switch (pAchievementData->public_.state) - { - case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: - vmAchievement->SetState(ra::data::models::AssetState::Active); - break; - case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: - vmAchievement->SetState(ra::data::models::AssetState::Inactive); - break; - case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: - vmAchievement->SetState(ra::data::models::AssetState::Triggered); - break; - case RC_CLIENT_ACHIEVEMENT_STATE_DISABLED: - vmAchievement->SetState(ra::data::models::AssetState::Disabled); - break; - } + auto vmAchievement = std::make_unique(); - m_vAssets.Append(std::move(vmAchievement)); + const auto sDefinition = mAchievementDefinitions.find(pAchievementData->public_.id); + if (sDefinition != mAchievementDefinitions.end()) + vmAchievement->Attach(*pAchievementData, nCategory, sDefinition->second); + else + vmAchievement->Attach(*pAchievementData, nCategory, ""); + + m_vAssets.Append(std::move(vmAchievement)); #ifndef RA_UTEST - // prefetch the achievement image - pImageRepository.FetchImage(ra::ui::ImageType::Badge, pAchievementData->public_.badge_name); + // prefetch the achievement image + pImageRepository.FetchImage(ra::ui::ImageType::Badge, pAchievementData->public_.badge_name); #endif + } } // leaderboards auto* pLeaderboardData = pSubset->leaderboards; - const auto* pLeaderboardStop = pLeaderboardData + pSubset->public_.num_leaderboards; - for (; pLeaderboardData < pLeaderboardStop; ++pLeaderboardData) + if (pLeaderboardData != nullptr) { - auto vmLeaderboard = std::make_unique(); - vmLeaderboard->SetID(pLeaderboardData->public_.id); - vmLeaderboard->SetName(ra::Widen(pLeaderboardData->public_.title)); - vmLeaderboard->SetDescription(ra::Widen(pLeaderboardData->public_.description)); - vmLeaderboard->SetCategory(ra::data::models::AssetCategory::Core); - vmLeaderboard->SetValueFormat(ra::itoe(pLeaderboardData->format)); - vmLeaderboard->SetLowerIsBetter(pLeaderboardData->public_.lower_is_better); - vmLeaderboard->SetHidden(pLeaderboardData->hidden); - - const auto sDefinition = mLeaderboardDefinitions.find(pLeaderboardData->public_.id); - if (sDefinition != mLeaderboardDefinitions.end()) - vmLeaderboard->SetDefinition(sDefinition->second); - - vmLeaderboard->CreateServerCheckpoint(); - vmLeaderboard->CreateLocalCheckpoint(); - - switch (pLeaderboardData->public_.state) + const auto* pLeaderboardStop = pLeaderboardData + pSubset->public_.num_leaderboards; + for (; pLeaderboardData < pLeaderboardStop; ++pLeaderboardData) { - case RC_CLIENT_LEADERBOARD_STATE_ACTIVE: - vmLeaderboard->SetState(ra::data::models::AssetState::Active); - break; - case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: - vmLeaderboard->SetState(ra::data::models::AssetState::Inactive); - break; - case RC_CLIENT_LEADERBOARD_STATE_TRACKING: - vmLeaderboard->SetState(ra::data::models::AssetState::Primed); - break; - case RC_CLIENT_LEADERBOARD_STATE_DISABLED: - vmLeaderboard->SetState(ra::data::models::AssetState::Disabled); - break; - } + auto vmLeaderboard = std::make_unique(); - m_vAssets.Append(std::move(vmLeaderboard)); + const auto nCategory = ra::data::models::AssetCategory::Core; // all published leaderboards are core + + const auto sDefinition = mLeaderboardDefinitions.find(pLeaderboardData->public_.id); + if (sDefinition != mLeaderboardDefinitions.end()) + vmLeaderboard->Attach(*pLeaderboardData, nCategory, sDefinition->second); + else + vmLeaderboard->Attach(*pLeaderboardData, nCategory, ""); + + m_vAssets.Append(std::move(vmLeaderboard)); + } } } } @@ -464,117 +419,6 @@ void GameContext::EndLoad() } } -void GameContext::RefreshUnlocks(bool bUnpause, int nPopup) -{ - std::set vUnlockedAchievements; - - if (m_nGameId != 0 && m_nMode != Mode::CompatibilityTest) - { - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore); - const uint8_t nFlag = gsl::narrow_cast(bHardcore ? ra::etoi(RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) - : ra::etoi(RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE)); - - const auto* pClient = ra::services::ServiceLocator::Get().GetClient(); - if (!pClient->game) // game still loading, ignore - return; - - for (auto* pSubset = pClient->game->subsets; pSubset; pSubset = pSubset->next) - { - // achievements - auto* pAchievementData = pSubset->achievements; - const auto* pAchievementStop = pAchievementData + pSubset->public_.num_achievements; - for (; pAchievementData < pAchievementStop; ++pAchievementData) - { - if (pAchievementData->public_.unlocked & nFlag) - vUnlockedAchievements.insert(pAchievementData->public_.id); - } - } - } - - BeginLoad(); - UpdateUnlocks(vUnlockedAchievements, bUnpause, nPopup); - EndLoad(); -} - -void GameContext::UpdateUnlocks(const std::set& vUnlockedAchievements, bool bUnpause, int nPopup) -{ - // start by identifying all available achievements - std::set vLockedAchievements; - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(Assets().Count()); ++nIndex) - { - const auto* pAchievement = dynamic_cast(Assets().GetItemAt(nIndex)); - if (pAchievement != nullptr) - vLockedAchievements.insert(pAchievement->GetID()); - } - - // deactivate anything the player has already unlocked - size_t nUnlockedCoreAchievements = 0U; - for (const auto nUnlockedAchievement : vUnlockedAchievements) - { - vLockedAchievements.erase(nUnlockedAchievement); - auto* pAchievement = Assets().FindAchievement(nUnlockedAchievement); - - // only core achievements will be automatically activated. also, they're all we want to count - if (pAchievement != nullptr && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core) - { - pAchievement->SetState(ra::data::models::AssetState::Inactive); - ++nUnlockedCoreAchievements; - } - } - -#ifndef RA_UTEST - auto& pImageRepository = ra::services::ServiceLocator::GetMutable(); -#endif - // activate any core achievements the player hasn't earned and pre-fetch the locked image - for (auto nAchievementId : vLockedAchievements) - { - auto* pAchievement = Assets().FindAchievement(nAchievementId); - if (pAchievement && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core) - { - if (!pAchievement->IsActive() && pAchievement->GetState() != ra::data::models::AssetState::Disabled) - { - // modified assets should start in the inactive state - if (pAchievement->GetChanges() != ra::data::models::AssetChanges::None) - pAchievement->SetState(ra::data::models::AssetState::Inactive); - else - pAchievement->SetState(ra::data::models::AssetState::Waiting); - } - -#ifndef RA_UTEST - if (!pAchievement->GetBadge().empty()) - pImageRepository.FetchImage(ra::ui::ImageType::Badge, ra::Narrow(pAchievement->GetBadge()) + "_lock"); -#endif - } - } - - if (bUnpause) - { - ra::services::ServiceLocator::GetMutable().SetPaused(false); -#ifndef RA_UTEST - auto& pAssetList = ra::services::ServiceLocator::GetMutable().AssetList; - pAssetList.SetProcessingActive(true); -#endif - } - - if (nPopup) - { - auto* pPopup = ra::services::ServiceLocator::GetMutable().GetMessage(nPopup); - if (pPopup != nullptr) - { - const auto nDisabledAchievements = ra::services::ServiceLocator::GetMutable().DetectUnsupportedAchievements(); - if (nDisabledAchievements != 0) - { - const auto sDescription = ra::StringPrintf(L"%s (%s unsupported)", pPopup->GetDescription(), nDisabledAchievements); - pPopup->SetDescription(sDescription); - } - - pPopup->SetDetail(ra::StringPrintf(L"You have earned %u achievements", nUnlockedCoreAchievements)); - pPopup->RebuildRenderImage(); - } - } -} - void GameContext::DoFrame() { for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(m_vAssets.Count()); ++nIndex) @@ -585,435 +429,6 @@ void GameContext::DoFrame() } } -void GameContext::AwardAchievement(ra::AchievementID nAchievementId) -{ - auto* pAchievement = Assets().FindAchievement(nAchievementId); - if (pAchievement == nullptr) - return; - - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - bool bTakeScreenshot = pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredScreenshot); - bool bSubmit = false; - bool bIsError = false; - - std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); - vmPopup->SetDescription(ra::StringPrintf(L"%s (%u)", pAchievement->GetName(), pAchievement->GetPoints())); - vmPopup->SetDetail(ra::Widen(pAchievement->GetDescription())); - vmPopup->SetImage(ra::ui::ImageType::Badge, ra::Narrow(pAchievement->GetBadge())); - vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered); - - switch (pAchievement->GetCategory()) - { - case ra::data::models::AssetCategory::Local: - vmPopup->SetTitle(L"Local Achievement Unlocked"); - bSubmit = false; - bTakeScreenshot = false; - break; - - case ra::data::models::AssetCategory::Unofficial: - vmPopup->SetTitle(L"Unofficial Achievement Unlocked"); - bSubmit = false; - break; - - default: - vmPopup->SetTitle(L"Achievement Unlocked"); - bSubmit = true; - break; - } - - if (m_nMode == Mode::CompatibilityTest) - { - auto sHeader = vmPopup->GetTitle(); - sHeader.insert(0, L"Test "); - vmPopup->SetTitle(sHeader); - - RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement->GetID(), "test compatibility mode"); - bSubmit = false; - } - - if (pAchievement->IsModified() || // actual modifications - (bSubmit && pAchievement->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes - { - auto sHeader = vmPopup->GetTitle(); - sHeader.insert(0, L"Modified "); - if (pAchievement->GetCategory() != ra::data::models::AssetCategory::Local) - sHeader.append(L" LOCALLY"); - vmPopup->SetTitle(sHeader); - - RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement->GetID(), pAchievement->IsModified() ? "modified" : "unpublished"); - bIsError = true; - bSubmit = false; - bTakeScreenshot = false; - } - - const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - if (bSubmit && pEmulatorContext.WasMemoryModified()) - { - vmPopup->SetTitle(L"Achievement Unlocked LOCALLY"); - vmPopup->SetErrorDetail(L"Error: RAM tampered with"); - - RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement->GetID(), "RAM tampered with"); - bIsError = true; - bSubmit = false; - } - - if (bSubmit && _RA_HardcoreModeIsActive() && pEmulatorContext.IsMemoryInsecure()) - { - vmPopup->SetTitle(L"Achievement Unlocked LOCALLY"); - vmPopup->SetErrorDetail(L"Error: RAM insecure"); - - RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement->GetID(), "RAM insecure"); - bIsError = true; - bSubmit = false; - } - - if (bSubmit && pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline)) - { - vmPopup->SetTitle(L"Offline Achievement Unlocked"); - bSubmit = false; - } - - int nPopupId = -1; - - ra::services::ServiceLocator::Get().PlayAudioFile( - bIsError ? L"Overlay\\acherror.wav" : L"Overlay\\unlock.wav"); - - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered) != ra::ui::viewmodels::PopupLocation::None) - { - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - nPopupId = pOverlayManager.QueueMessage(vmPopup); - - if (bTakeScreenshot) - { - std::wstring sPath = ra::StringPrintf(L"%s%u.png", pConfiguration.GetScreenshotDirectory(), nAchievementId); - pOverlayManager.CaptureScreenshot(nPopupId, sPath); - } - } - - pAchievement->SetState(ra::data::models::AssetState::Triggered); - - if (!bSubmit) - return; - - ra::api::AwardAchievement::Request request; - request.AchievementId = nAchievementId; - request.Hardcore = _RA_HardcoreModeIsActive(); - request.GameHash = GameHash(); - request.CallAsyncWithRetry([this, nPopupId, nAchievementId](const ra::api::AwardAchievement::Response& response) - { - if (response.Succeeded()) - { - // TODO: just read score from rc_client->user (when client handles unlocks) - // success! update the player's score - ra::services::ServiceLocator::GetMutable().SetScore(response.NewPlayerScore); - - if (response.AchievementsRemaining == 0) - CheckForMastery(); - } - else if (ra::StringStartsWith(response.ErrorMessage, "User already has ")) - { - // if server responds with "Error: User already has hardcore and regular achievements awarded." or - // "Error: User already has this achievement awarded.", just ignore the error. There's no reason to - // notify the user that the unlock failed because it had happened previously. - } - else if (nPopupId != -1) - { - // an error occurred, update the popup to display the error message - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - auto pPopup = pOverlayManager.GetMessage(nPopupId); - if (pPopup != nullptr) - { - pPopup->SetTitle(L"Achievement Unlock FAILED"); - pPopup->SetErrorDetail(response.ErrorMessage.empty() ? - L"Error submitting unlock" : ra::Widen(response.ErrorMessage)); - pPopup->RebuildRenderImage(); - } - else - { - std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); - vmPopup->SetTitle(L"Achievement Unlock FAILED"); - vmPopup->SetErrorDetail(response.ErrorMessage.empty() ? - L"Error submitting unlock" : ra::Widen(response.ErrorMessage)); - vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered); - - const auto& pGameContext = ra::services::ServiceLocator::Get(); - const auto* pAchievement = pGameContext.Assets().FindAchievement(nAchievementId); - if (pAchievement != nullptr) - { - vmPopup->SetDescription(ra::StringPrintf(L"%s (%u)", pAchievement->GetName(), pAchievement->GetPoints())); - vmPopup->SetImage(ra::ui::ImageType::Badge, ra::Narrow(pAchievement->GetBadge())); - } - else - { - vmPopup->SetDescription(ra::StringPrintf(L"Achievement %u", nAchievementId)); - } - - ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\acherror.wav"); - pOverlayManager.QueueMessage(vmPopup); - } - } - }); -} - -void GameContext::CheckForMastery() -{ - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) == ra::ui::viewmodels::PopupLocation::None) - return; - - // if multiple achievements unlock the mastery at the same time, only show the popup once - auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); - if (m_nMasteryPopupId != 0 && pOverlayManager.GetMessage(m_nMasteryPopupId)) - return; - - // validate that all core assets are unlocked - const auto& pAssets = ra::services::ServiceLocator::Get().Assets(); - unsigned nTotalCoreAchievementPoints = 0; - size_t nNumCoreAchievements = -0; - bool bActiveCoreAchievement = false; - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pAssets.Count()); ++nIndex) - { - const auto* pAchievement = dynamic_cast(pAssets.GetItemAt(nIndex)); - if (pAchievement != nullptr && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core) - { - ++nNumCoreAchievements; - nTotalCoreAchievementPoints += pAchievement->GetPoints(); - - if (pAchievement->IsActive()) - { - bActiveCoreAchievement = true; - break; - } - } - } - - // if they are, show the mastery notification - if (!bActiveCoreAchievement && nNumCoreAchievements > 0) - { - const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore); - - const auto nPlayTimeSeconds = ra::services::ServiceLocator::Get().GetTotalPlaytime(m_nGameId); - const auto nPlayTimeMinutes = std::chrono::duration_cast(nPlayTimeSeconds).count(); - - std::unique_ptr vmMessage(new ra::ui::viewmodels::PopupMessageViewModel); - vmMessage->SetTitle(ra::StringPrintf(L"%s %s", bHardcore ? L"Mastered" : L"Completed", m_sGameTitle)); - vmMessage->SetDescription(ra::StringPrintf(L"%u achievements, %u points", nNumCoreAchievements, nTotalCoreAchievementPoints)); - vmMessage->SetDetail(ra::StringPrintf(L"%s | Play time: %dh%02dm", pConfiguration.GetUsername(), nPlayTimeMinutes / 60, nPlayTimeMinutes % 60)); - vmMessage->SetImage(ra::ui::ImageType::Icon, m_sGameImage); - vmMessage->SetPopupType(ra::ui::viewmodels::Popup::Mastery); - - // if multiple achievements unlock the mastery at the same time, only show the popup once - if (m_nMasteryPopupId != 0 && pOverlayManager.GetMessage(m_nMasteryPopupId)) - return; - - ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\unlock.wav"); - m_nMasteryPopupId = pOverlayManager.QueueMessage(vmMessage); - - if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot)) - { - std::wstring sPath = ra::StringPrintf(L"%sGame%u.png", pConfiguration.GetScreenshotDirectory(), m_nGameId); - pOverlayManager.CaptureScreenshot(m_nMasteryPopupId, sPath); - } - } -} - -void GameContext::DeactivateLeaderboards() -{ - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(Assets().Count()); ++nIndex) - { - auto* pLeaderboard = dynamic_cast(Assets().GetItemAt(nIndex)); - if (pLeaderboard != nullptr) - pLeaderboard->SetState(ra::data::models::AssetState::Inactive); - } -} - -void GameContext::ActivateLeaderboards() -{ - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards)) // if not, simply ignore them. - { - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(Assets().Count()); ++nIndex) - { - auto* pLeaderboard = dynamic_cast(Assets().GetItemAt(nIndex)); - if (pLeaderboard != nullptr && pLeaderboard->GetCategory() == ra::data::models::AssetCategory::Core) - pLeaderboard->SetState(ra::data::models::AssetState::Waiting); - } - } -} - -void GameContext::ShowSimplifiedScoreboard(ra::LeaderboardID nLeaderboardId, int nScore) const -{ - auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) == ra::ui::viewmodels::PopupLocation::None) - return; - - const auto* pLeaderboard = Assets().FindLeaderboard(nLeaderboardId); - if (!pLeaderboard) - return; - - ra::ui::viewmodels::ScoreboardViewModel vmScoreboard; - vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard->GetName())); - - const auto& pUserName = ra::services::ServiceLocator::Get().GetDisplayName(); - auto& pEntryViewModel = vmScoreboard.Entries().Add(); - pEntryViewModel.SetRank(0); - pEntryViewModel.SetScore(ra::Widen(pLeaderboard->FormatScore(nScore))); - pEntryViewModel.SetUserName(ra::Widen(pUserName)); - pEntryViewModel.SetHighlighted(true); - - ra::services::ServiceLocator::GetMutable().QueueScoreboard( - pLeaderboard->GetID(), std::move(vmScoreboard)); -} - -void GameContext::SubmitLeaderboardEntry(ra::LeaderboardID nLeaderboardId, int nScore) const -{ - const auto* pLeaderboard = Assets().FindLeaderboard(nLeaderboardId); - if (pLeaderboard == nullptr) - return; - - std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); - vmPopup->SetDescription(ra::Widen(pLeaderboard->GetName())); - std::wstring sTitle = L"Leaderboard NOT Submitted"; - bool bSubmit = true; - - switch (pLeaderboard->GetCategory()) - { - case ra::data::models::AssetCategory::Local: - sTitle.insert(0, L"Local "); - bSubmit = false; - break; - - case ra::data::models::AssetCategory::Unofficial: - sTitle.insert(0, L"Unofficial "); - bSubmit = false; - break; - - default: - break; - } - - if (m_nMode == Mode::CompatibilityTest) - { - vmPopup->SetDetail(L"Leaderboards are not submitted in test mode."); - bSubmit = false; - } - - if (pLeaderboard->IsModified() || // actual modifications - (bSubmit && pLeaderboard->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes - { - sTitle.insert(0, L"Modified "); - bSubmit = false; - } - - const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - if (bSubmit && pEmulatorContext.WasMemoryModified()) - { - vmPopup->SetErrorDetail(L"Error: RAM tampered with"); - bSubmit = false; - } - - const auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (bSubmit) - { - if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)) - { - vmPopup->SetErrorDetail(L"Submission requires Hardcore mode"); - bSubmit = false; - } - else if (pEmulatorContext.IsMemoryInsecure()) - { - vmPopup->SetErrorDetail(L"Error: RAM insecure"); - bSubmit = false; - } - else if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline)) - { - vmPopup->SetDetail(L"Leaderboards are not submitted in offline mode."); - bSubmit = false; - } - } - - if (!bSubmit) - { - vmPopup->SetTitle(sTitle); - ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\info.wav"); - ra::services::ServiceLocator::GetMutable().QueueMessage(vmPopup); - ShowSimplifiedScoreboard(nLeaderboardId, nScore); - return; - } - - ra::api::SubmitLeaderboardEntry::Request request; - request.LeaderboardId = pLeaderboard->GetID(); - request.Score = nScore; - request.GameHash = GameHash(); - request.CallAsyncWithRetry([this, nLeaderboardId = pLeaderboard->GetID()](const ra::api::SubmitLeaderboardEntry::Response& response) - { - const auto* pLeaderboard = Assets().FindLeaderboard(nLeaderboardId); - - if (!response.Succeeded()) - { - std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); - vmPopup->SetTitle(L"Leaderboard NOT Submitted"); - vmPopup->SetDescription(pLeaderboard ? pLeaderboard->GetName() : ra::StringPrintf(L"Leaderboard %u", nLeaderboardId)); - vmPopup->SetDetail(!response.ErrorMessage.empty() ? ra::StringPrintf(L"Error: %s", response.ErrorMessage) : L"Error submitting entry"); - - ra::services::ServiceLocator::GetMutable().QueueMessage(vmPopup); - } - else if (pLeaderboard) - { - auto& pConfiguration = ra::services::ServiceLocator::Get(); - if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) != ra::ui::viewmodels::PopupLocation::None) - { - ra::ui::viewmodels::ScoreboardViewModel vmScoreboard; - vmScoreboard.SetHeaderText(pLeaderboard->GetName()); - - const auto& pUserName = ra::services::ServiceLocator::Get().GetDisplayName(); - constexpr int nEntriesDisplayed = 7; // display is currently hard-coded to show 7 entries - bool bSeenPlayer = false; - - for (const auto& pEntry : response.TopEntries) - { - auto& pEntryViewModel = vmScoreboard.Entries().Add(); - pEntryViewModel.SetRank(pEntry.Rank); - pEntryViewModel.SetScore(ra::Widen(pLeaderboard->FormatScore(pEntry.Score))); - pEntryViewModel.SetUserName(ra::Widen(pEntry.User)); - - if (pEntry.User == pUserName) - { - bSeenPlayer = true; - pEntryViewModel.SetHighlighted(true); - - if (response.BestScore != response.Score) - pEntryViewModel.SetScore(ra::StringPrintf(L"(%s) %s", pLeaderboard->FormatScore(response.Score), pLeaderboard->FormatScore(response.BestScore))); - } - - if (vmScoreboard.Entries().Count() == nEntriesDisplayed) - break; - } - - if (!bSeenPlayer) - { - auto* pEntryViewModel = vmScoreboard.Entries().GetItemAt(6); - if (pEntryViewModel != nullptr) - { - pEntryViewModel->SetRank(response.NewRank); - - if (response.BestScore != response.Score) - pEntryViewModel->SetScore(ra::StringPrintf(L"(%s) %s", pLeaderboard->FormatScore(response.Score), pLeaderboard->FormatScore(response.BestScore))); - else - pEntryViewModel->SetScore(ra::Widen(pLeaderboard->FormatScore(response.BestScore))); - - pEntryViewModel->SetUserName(ra::Widen(pUserName)); - pEntryViewModel->SetHighlighted(true); - } - } - - ra::services::ServiceLocator::GetMutable().QueueScoreboard( - pLeaderboard->GetID(), std::move(vmScoreboard)); - } - } - }); -} - void GameContext::OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring& sNewNote) { if (!m_vNotifyTargets.empty()) diff --git a/src/data/context/GameContext.hh b/src/data/context/GameContext.hh index 6d4ed89d..ccc1cc17 100644 --- a/src/data/context/GameContext.hh +++ b/src/data/context/GameContext.hh @@ -33,10 +33,10 @@ public: virtual void LoadGame(unsigned int nGameId, const std::string& sGameHash, Mode nMode = Mode::Normal); /// - /// Loads the data from the game currently loaded in the RcheevosClient. + /// Loads the data from the game currently loaded in the AchievementRuntime. /// - void InitializeFromRcheevosClient(const std::map mAchievementDefinitions, - const std::map mLeaderboardDefinitions); + void InitializeFromAchievementRuntime(const std::map mAchievementDefinitions, + const std::map mLeaderboardDefinitions); /// /// Determines whether a game is being loaded. @@ -93,24 +93,6 @@ public: GameAssets& Assets() noexcept { return m_vAssets; } const GameAssets& Assets() const noexcept { return m_vAssets; } - /// - /// Shows the popup for earning an achievement and notifies the server if legitimate. - /// - void AwardAchievement(ra::AchievementID nAchievementId); - - void DeactivateLeaderboards(); - void ActivateLeaderboards(); - - /// - /// Submit a new score for the specified leaderboard. - /// - void SubmitLeaderboardEntry(ra::LeaderboardID nLeaderboardId, int nScore) const; - - /// - /// Updates the set of unlocked achievements from the server. - /// - void RefreshUnlocks() { RefreshUnlocks(false, 0); } - class NotifyTarget { public: @@ -138,10 +120,6 @@ private: void FinishLoadGame(int nResult, const char* sErrorMessage, bool bWasPaused, const std::string& sOldRichPresence); protected: - void RefreshUnlocks(bool bUnpause, int nPopup); - void UpdateUnlocks(const std::set& vUnlockedAchievements, bool bUnpause, int nPopup); - void ShowSimplifiedScoreboard(ra::LeaderboardID nLeaderboardId, int nScore) const; - void OnBeforeActiveGameChanged(); void OnActiveGameChanged(); void OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring& sNewNote); @@ -155,8 +133,6 @@ protected: Mode m_nMode{}; private: - void CheckForMastery(); - /// /// A collection of pointers to other objects. These are not allocated object and do not need to be free'd. It's /// impossible to create a set of NotifyTarget references. diff --git a/src/data/context/SessionTracker.cpp b/src/data/context/SessionTracker.cpp index c392c692..71a0133e 100644 --- a/src/data/context/SessionTracker.cpp +++ b/src/data/context/SessionTracker.cpp @@ -5,7 +5,6 @@ #include "RA_md5factory.h" #include "RA_StringUtils.h" -#include "api\Ping.hh" #include "api\StartSession.hh" #include "data\context\GameContext.hh" @@ -153,12 +152,6 @@ void SessionTracker::UpdateSession(time_t tSessionStart) if (tSessionStart != m_tSessionStart) return; - // ping the server (this also updates the player's current activity string) - ra::api::Ping::Request request; - request.GameId = m_nCurrentGameId; - request.CurrentActivity = GetCurrentActivity(); - request.CallAsync([](const ra::api::Ping::Response&) {}); - // update session duration const auto& pClock = ra::services::ServiceLocator::Get(); const auto tSessionDuration = std::chrono::duration_cast(pClock.UpTime() - m_tpSessionStart); diff --git a/src/data/context/UserContext.cpp b/src/data/context/UserContext.cpp index a49771b2..609c0983 100644 --- a/src/data/context/UserContext.cpp +++ b/src/data/context/UserContext.cpp @@ -7,8 +7,8 @@ #include "data\context\EmulatorContext.hh" +#include "services\AchievementRuntime.hh" #include "services\IConfiguration.hh" -#include "services\RcheevosClient.hh" #include "ui\viewmodels\MessageBoxViewModel.hh" #include "ui\viewmodels\OverlayManager.hh" @@ -23,7 +23,7 @@ void UserContext::Logout() if (!IsLoggedIn()) return; - auto* pClient = ra::services::ServiceLocator::Get().GetClient(); + auto* pClient = ra::services::ServiceLocator::Get().GetClient(); rc_client_logout(pClient); m_sUsername.clear(); diff --git a/src/data/models/AchievementModel.cpp b/src/data/models/AchievementModel.cpp index 417209f5..60a706c3 100644 --- a/src/data/models/AchievementModel.cpp +++ b/src/data/models/AchievementModel.cpp @@ -9,6 +9,9 @@ #include "services\IClock.hh" #include "services\ServiceLocator.hh" +#include +#include + namespace ra { namespace data { namespace models { @@ -54,11 +57,13 @@ void AchievementModel::OnValueChanged(const IntModelProperty::ChangeArgs& args) } else if (args.Property == TriggerProperty) { - if (IsActive()) - { - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - pRuntime.ActivateAchievement(GetID(), GetTrigger()); - } + if (m_pAchievement) + SyncTrigger(); + } + else if (args.Property == PointsProperty) + { + if (m_pAchievement) + SyncPoints(); } } @@ -74,7 +79,17 @@ void AchievementModel::OnValueChanged(const StringModelProperty::ChangeArgs& arg if (IsUpdating()) return; - if (args.Property == BadgeProperty) + if (args.Property == DescriptionProperty) + { + if (m_pAchievement) + SyncDescription(); + } + else if (args.Property == NameProperty) + { + if (m_pAchievement) + SyncTitle(); + } + else if (args.Property == BadgeProperty) { if (ra::StringStartsWith(args.tOldValue, L"local\\")) { @@ -97,6 +112,9 @@ void AchievementModel::OnValueChanged(const StringModelProperty::ChangeArgs& arg pLocalBadges->AddReference(args.tNewValue, bCommitted); } } + + if (m_pAchievement) + SyncBadge(); } } @@ -140,8 +158,10 @@ bool AchievementModel::ValidateAsset(std::wstring& sError) void AchievementModel::DoFrame() { - const auto& pRuntime = ra::services::ServiceLocator::Get(); - const auto* pTrigger = pRuntime.GetAchievementTrigger(GetID()); + if (!m_pAchievement) + return; + + const auto* pTrigger = m_pAchievement->trigger; if (pTrigger == nullptr) { if (IsActive()) @@ -178,68 +198,232 @@ void AchievementModel::DoFrame() void AchievementModel::HandleStateChanged(AssetState nOldState, AssetState nNewState) { - if (!ra::services::ServiceLocator::Exists()) + if (!m_pAchievement) return; const bool bWasActive = IsActive(nOldState); const bool bIsActive = IsActive(nNewState); - if (bWasActive == bIsActive) - return; - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - auto* pTrigger = pRuntime.GetAchievementTrigger(GetID()); - - if (bIsActive) + if (!bIsActive && bWasActive) { - const bool bRuntimeActive = (pTrigger != nullptr); - if (!bRuntimeActive) - { - pRuntime.ActivateAchievement(GetID(), GetTrigger()); - pTrigger = pRuntime.GetAchievementTrigger(GetID()); - } + Expects(m_pAchievement->trigger != nullptr); + m_pCapturedTriggerHits.Capture(m_pAchievement->trigger, GetTrigger()); + } + else if (bIsActive && !bWasActive) + { + if (m_pAchievement->trigger) + rc_reset_trigger(m_pAchievement->trigger); else - { - rc_reset_trigger(pTrigger); - } - } - else if (pTrigger) - { - m_pCapturedTriggerHits.Capture(pTrigger, GetTrigger()); - pRuntime.DeactivateAchievement(GetID()); + SyncTrigger(); } - switch (nNewState) + SyncState(); +} + +void AchievementModel::SyncState() +{ + switch (GetState()) { - case AssetState::Waiting: - if (pTrigger) - pTrigger->state = RC_TRIGGER_STATE_WAITING; - break; + case ra::data::models::AssetState::Triggered: { + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; - case AssetState::Active: - if (pTrigger) - pTrigger->state = RC_TRIGGER_STATE_ACTIVE; - break; - - case AssetState::Inactive: - if (pTrigger) - pTrigger->state = RC_TRIGGER_STATE_INACTIVE; - break; - - case AssetState::Primed: - if (pTrigger) - pTrigger->state = RC_TRIGGER_STATE_PRIMED; - break; - - case AssetState::Triggered: const auto& pClock = ra::services::ServiceLocator::Get(); m_tUnlock = pClock.Now(); + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); if (pRuntime.HasRichPresence()) SetUnlockRichPresence(pRuntime.GetRichPresenceDisplayString()); break; + } + + case ra::data::models::AssetState::Disabled: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + m_pAchievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_DISABLED; + break; + + case ra::data::models::AssetState::Inactive: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_INACTIVE; + break; + + case ra::data::models::AssetState::Waiting: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_WAITING; + break; + + case ra::data::models::AssetState::Active: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_ACTIVE; + break; + + case ra::data::models::AssetState::Primed: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + if (m_pAchievement->trigger) + m_pAchievement->trigger->state = RC_TRIGGER_STATE_PRIMED; + break; + + default: + m_pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + break; } } +void AchievementModel::SyncTitle() +{ + m_sTitleBuffer = ra::Narrow(GetName()); + m_pAchievement->public_.title = m_sTitleBuffer.c_str(); +} + +void AchievementModel::SyncDescription() +{ + m_sDescriptionBuffer = ra::Narrow(GetDescription()); + m_pAchievement->public_.description = m_sDescriptionBuffer.c_str(); +} + +void AchievementModel::SyncBadge() +{ + const auto& sBadge = GetBadge(); + if (ra::StringStartsWith(sBadge, L"local\\")) + { + // cannot fit "local/md5.png" into 8 byte buffer. also, client may not understand. + // encode a value that we can intercept in rc_client_achievement_get_image_url. + snprintf(m_pAchievement->public_.badge_name, sizeof(m_pAchievement->public_.badge_name), "L%06x", + m_pAchievement->public_.id); + } + else + { + snprintf(m_pAchievement->public_.badge_name, sizeof(m_pAchievement->public_.badge_name), "%s", + ra::Narrow(sBadge).c_str()); + } +} + +void AchievementModel::SyncPoints() +{ + m_pAchievement->public_.points = GetPoints(); +} + +void AchievementModel::SyncCategory() +{ + switch (GetCategory()) + { + case ra::data::models::AssetCategory::Core: + case ra::data::models::AssetCategory::Local: + m_pAchievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + break; + + default: + m_pAchievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL; + break; + } +} + +void AchievementModel::SyncTrigger() +{ + if (!IsActive()) + { + if (m_pAchievement->trigger) + { + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + if (pRuntime.DetachMemory(m_pAchievement->trigger)) + free(m_pAchievement->trigger); + + m_pAchievement->trigger = nullptr; + } + + memset(m_pAchievement->md5, 0, sizeof(m_pAchievement->md5)); + return; + } + + const auto& sTrigger = GetTrigger(); + uint8_t md5[16]; + rc_runtime_checksum(sTrigger.c_str(), md5); + if (memcmp(m_pAchievement->md5, md5, sizeof(md5)) != 0) + { + memcpy(m_pAchievement->md5, md5, sizeof(md5)); + + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + if (m_pAchievement->trigger && pRuntime.DetachMemory(m_pAchievement->trigger)) + { + free(m_pAchievement->trigger); + m_pAchievement->trigger = nullptr; + } + + const auto nSize = rc_trigger_size(sTrigger.c_str()); + if (nSize > 0) + { + void* trigger_buffer = malloc(nSize); + if (trigger_buffer) + { + // populate the item, using the communal memrefs pool + auto* pGame = pRuntime.GetClient()->game; + + rc_parse_state_t parse; + rc_init_parse_state(&parse, trigger_buffer, nullptr, 0); + parse.first_memref = &pGame->runtime.memrefs; + parse.variables = &pGame->runtime.variables; + + const char* sMemaddr = sTrigger.c_str(); + m_pAchievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(m_pAchievement->trigger, &sMemaddr, &parse); + rc_destroy_parse_state(&parse); + m_pAchievement->trigger->memrefs = nullptr; + + pRuntime.AttachMemory(m_pAchievement->trigger); + } + } + } +} + +void AchievementModel::Attach(struct rc_client_achievement_info_t& pAchievement, + AssetCategory nCategory, const std::string& sTrigger) +{ + SetID(pAchievement.public_.id); + SetName(ra::Widen(pAchievement.public_.title)); + SetDescription(ra::Widen(pAchievement.public_.description)); + SetCategory(nCategory); + SetPoints(pAchievement.public_.points); + if (pAchievement.author) + SetAuthor(ra::Widen(pAchievement.author)); + SetBadge(ra::Widen(pAchievement.public_.badge_name)); + SetCreationTime(pAchievement.created_time); + SetUpdatedTime(pAchievement.updated_time); + SetTrigger(sTrigger); + + CreateServerCheckpoint(); + CreateLocalCheckpoint(); + + m_pAchievement = &pAchievement; + + DoFrame(); // sync state +} + +void AchievementModel::ReplaceAttached(struct rc_client_achievement_info_t& pAchievement) noexcept +{ + m_pAchievement = &pAchievement; +} + +void AchievementModel::AttachAndInitialize(struct rc_client_achievement_info_t& pAchievement) +{ + pAchievement.public_.id = GetID(); + + m_pAchievement = &pAchievement; + + SyncTitle(); + SyncDescription(); + SyncBadge(); + SyncPoints(); + SyncCategory(); + SyncTrigger(); + SyncState(); +} + void AchievementModel::Serialize(ra::services::TextWriter& pWriter) const { WriteQuoted(pWriter, GetLocalAssetDefinition(m_pTrigger)); diff --git a/src/data/models/AchievementModel.hh b/src/data/models/AchievementModel.hh index 6b3934fa..a3b86c11 100644 --- a/src/data/models/AchievementModel.hh +++ b/src/data/models/AchievementModel.hh @@ -6,6 +6,8 @@ #include "CapturedTriggerHits.hh" +struct rc_client_achievement_info_t; + namespace ra { namespace data { namespace models { @@ -127,6 +129,16 @@ public: void Serialize(ra::services::TextWriter& pWriter) const override; bool Deserialize(ra::Tokenizer& pTokenizer) override; + // Attaches an rc_client_achievement_info_t and populates the model from it. + void Attach(struct rc_client_achievement_info_t& pAchievement, + AssetCategory nCategory, const std::string& sTrigger); + // Replaces the attached rc_client_achievement_info_t. Does not update model or source. + void ReplaceAttached(struct rc_client_achievement_info_t& pAchievement) noexcept; + // Attaches an rc_client_achievement_info_t and populates it from the model. + void AttachAndInitialize(struct rc_client_achievement_info_t& pAchievement); + // Gets the attached rc_client_achievement_info_t. + const struct rc_client_achievement_info_t* GetAttached() const noexcept { return m_pAchievement; } + const CapturedTriggerHits& GetCapturedHits() const noexcept { return m_pCapturedTriggerHits; } void ResetCapturedHits() noexcept { m_pCapturedTriggerHits.Reset(); } @@ -149,10 +161,21 @@ private: static const std::array s_vValidPoints; void HandleStateChanged(AssetState nOldState, AssetState nNewState); + void SyncTitle(); + void SyncDescription(); + void SyncBadge(); + void SyncPoints(); + void SyncCategory(); + void SyncState(); + void SyncTrigger(); AssetDefinition m_pTrigger; std::chrono::system_clock::time_point m_tUnlock; + struct rc_client_achievement_info_t* m_pAchievement = nullptr; + std::string m_sTitleBuffer; + std::string m_sDescriptionBuffer; + CapturedTriggerHits m_pCapturedTriggerHits; }; diff --git a/src/data/models/LeaderboardModel.cpp b/src/data/models/LeaderboardModel.cpp index 62b55df2..687c129d 100644 --- a/src/data/models/LeaderboardModel.cpp +++ b/src/data/models/LeaderboardModel.cpp @@ -7,6 +7,9 @@ #include "services\AchievementRuntime.hh" #include "services\ServiceLocator.hh" +#include +#include + namespace ra { namespace data { namespace models { @@ -57,18 +60,34 @@ void LeaderboardModel::OnValueChanged(const IntModelProperty::ChangeArgs& args) else if (args.Property == StartTriggerProperty || args.Property == SubmitTriggerProperty || args.Property == CancelTriggerProperty || args.Property == ValueDefinitionProperty) { - if (IsActive()) - { - const std::string sDefinition = GetDefinition(); - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - pRuntime.ActivateLeaderboard(GetID(), sDefinition); - } + SyncDefinition(); } } AssetModelBase::OnValueChanged(args); } +void LeaderboardModel::OnValueChanged(const StringModelProperty::ChangeArgs& args) +{ + // call base first so the transaction gets updated for GetLocalValue to work properly + AssetModelBase::OnValueChanged(args); + + // if we're still loading the asset, ignore the event. we'll get recalled once loading finishes + if (IsUpdating()) + return; + + if (args.Property == DescriptionProperty) + { + if (m_pLeaderboard) + SyncDescription(); + } + else if (args.Property == NameProperty) + { + if (m_pLeaderboard) + SyncTitle(); + } +} + bool LeaderboardModel::ValidateAsset(std::wstring& sError) { const auto& sStartTrigger = GetAssetDefinition(m_pStartTrigger); @@ -165,56 +184,139 @@ void LeaderboardModel::DoFrame() void LeaderboardModel::HandleStateChanged(AssetState nOldState, AssetState nNewState) { - if (!ra::services::ServiceLocator::Exists()) + if (!m_pLeaderboard) return; const bool bWasActive = IsActive(nOldState); const bool bIsActive = IsActive(nNewState); - if (bWasActive == bIsActive) - return; - auto& pRuntime = ra::services::ServiceLocator::GetMutable(); - auto* pLeaderboard = pRuntime.GetLeaderboardDefinition(GetID()); + if (!bIsActive && bWasActive) + { + const auto* pLeaderboard = m_pLeaderboard->lboard; + Expects(pLeaderboard != nullptr); - if (bIsActive) - { - const bool bRuntimeActive = (pLeaderboard != nullptr); - if (!bRuntimeActive) - { - const std::string sDefinition = GetDefinition(); - pRuntime.ActivateLeaderboard(GetID(), sDefinition); - pLeaderboard = pRuntime.GetLeaderboardDefinition(GetID()); - } - else - { - rc_reset_lboard(pLeaderboard); - } - } - else if (pLeaderboard) - { m_pCapturedStartTriggerHits.Capture(&pLeaderboard->start, GetStartTrigger()); m_pCapturedSubmitTriggerHits.Capture(&pLeaderboard->submit, GetSubmitTrigger()); m_pCapturedCancelTriggerHits.Capture(&pLeaderboard->cancel, GetCancelTrigger()); m_pCapturedValueDefinitionHits.Capture(&pLeaderboard->value, GetValueDefinition()); - pRuntime.DeactivateLeaderboard(GetID()); + + if (pLeaderboard->state == RC_LBOARD_STATE_STARTED) + { + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + pRuntime.ReleaseLeaderboardTracker(m_pLeaderboard->public_.id); + } + } + else if (bIsActive && !bWasActive) + { + if (m_pLeaderboard->lboard) + rc_reset_lboard(m_pLeaderboard->lboard); + else + SyncDefinition(); } switch (nNewState) { - case AssetState::Waiting: - if (pLeaderboard) - pLeaderboard->state = RC_LBOARD_STATE_WAITING; + case ra::data::models::AssetState::Disabled: + m_pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + if (m_pLeaderboard->lboard) + m_pLeaderboard->lboard->state = RC_LBOARD_STATE_DISABLED; break; - case AssetState::Active: - if (pLeaderboard) - pLeaderboard->state = RC_LBOARD_STATE_ACTIVE; + case ra::data::models::AssetState::Inactive: + m_pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + if (m_pLeaderboard->lboard) + m_pLeaderboard->lboard->state = RC_LBOARD_STATE_INACTIVE; break; - case AssetState::Inactive: - if (pLeaderboard) - pLeaderboard->state = RC_LBOARD_STATE_INACTIVE; + case ra::data::models::AssetState::Primed: + m_pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + if (m_pLeaderboard->lboard) + m_pLeaderboard->lboard->state = RC_LBOARD_STATE_STARTED; break; + + case ra::data::models::AssetState::Waiting: + m_pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + if (m_pLeaderboard->lboard) + m_pLeaderboard->lboard->state = RC_LBOARD_STATE_WAITING; + break; + + default: + m_pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + if (m_pLeaderboard->lboard) + m_pLeaderboard->lboard->state = RC_LBOARD_STATE_ACTIVE; + break; + } +} + +void LeaderboardModel::SyncTitle() +{ + m_sTitleBuffer = ra::Narrow(GetName()); + m_pLeaderboard->public_.title = m_sTitleBuffer.c_str(); +} + +void LeaderboardModel::SyncDescription() +{ + m_sDescriptionBuffer = ra::Narrow(GetDescription()); + m_pLeaderboard->public_.description = m_sDescriptionBuffer.c_str(); +} + +void LeaderboardModel::SyncDefinition() +{ + if (!m_pLeaderboard) + return; + + if (!IsActive()) + { + if (m_pLeaderboard->lboard) + { + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + if (pRuntime.DetachMemory(m_pLeaderboard->lboard)) + free(m_pLeaderboard->lboard); + + m_pLeaderboard->lboard = nullptr; + } + + memset(m_pLeaderboard->md5, 0, sizeof(m_pLeaderboard->md5)); + return; + } + + const auto& sMemAddr = GetDefinition(); + uint8_t md5[16]; + rc_runtime_checksum(sMemAddr.c_str(), md5); + if (memcmp(m_pLeaderboard->md5, md5, sizeof(md5)) != 0) + { + memcpy(m_pLeaderboard->md5, md5, sizeof(md5)); + + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + if (m_pLeaderboard->lboard && pRuntime.DetachMemory(m_pLeaderboard->lboard)) + { + free(m_pLeaderboard->lboard); + m_pLeaderboard->lboard = nullptr; + } + + const auto nSize = rc_lboard_size(sMemAddr.c_str()); + if (nSize > 0) + { + void* lboard_buffer = malloc(nSize); + if (lboard_buffer) + { + /* populate the item, using the communal memrefs pool */ + auto* pGame = pRuntime.GetClient()->game; + + rc_parse_state_t parse; + rc_init_parse_state(&parse, lboard_buffer, nullptr, 0); + parse.first_memref = &pGame->runtime.memrefs; + parse.variables = &pGame->runtime.variables; + + m_pLeaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(m_pLeaderboard->lboard, sMemAddr.c_str(), &parse); + rc_destroy_parse_state(&parse); + + m_pLeaderboard->lboard->memrefs = nullptr; + + pRuntime.AttachMemory(m_pLeaderboard->lboard); + } + } } } @@ -247,6 +349,42 @@ std::string LeaderboardModel::GetDefinition() const "::CAN:" + GetCancelTrigger() + "::VAL:" + GetValueDefinition(); } +void LeaderboardModel::Attach(struct rc_client_leaderboard_info_t& pLeaderboard, + AssetCategory nCategory, const std::string& sDefinition) +{ + SetID(pLeaderboard.public_.id); + SetName(ra::Widen(pLeaderboard.public_.title)); + SetDescription(ra::Widen(pLeaderboard.public_.description)); + SetCategory(nCategory); + SetValueFormat(ra::itoe(pLeaderboard.format)); + SetLowerIsBetter(pLeaderboard.public_.lower_is_better); + SetHidden(pLeaderboard.hidden); + SetDefinition(sDefinition); + + CreateServerCheckpoint(); + CreateLocalCheckpoint(); + + m_pLeaderboard = &pLeaderboard; + + DoFrame(); // sync state +} + +void LeaderboardModel::ReplaceAttached(struct rc_client_leaderboard_info_t& pLeaderboard) noexcept +{ + m_pLeaderboard = &pLeaderboard; +} + +void LeaderboardModel::AttachAndInitialize(struct rc_client_leaderboard_info_t& pLeaderboard) +{ + pLeaderboard.public_.id = GetID(); + + m_pLeaderboard = &pLeaderboard; + + SyncTitle(); + SyncDescription(); + SyncDefinition(); +} + void LeaderboardModel::Serialize(ra::services::TextWriter& pWriter) const { WriteQuoted(pWriter, GetLocalAssetDefinition(m_pStartTrigger)); diff --git a/src/data/models/LeaderboardModel.hh b/src/data/models/LeaderboardModel.hh index 561e8538..7ab92efb 100644 --- a/src/data/models/LeaderboardModel.hh +++ b/src/data/models/LeaderboardModel.hh @@ -8,6 +8,8 @@ #include "data\Types.hh" +struct rc_client_leaderboard_info_t; + namespace ra { namespace data { namespace models { @@ -189,6 +191,16 @@ public: void Serialize(ra::services::TextWriter& pWriter) const override; bool Deserialize(ra::Tokenizer& pTokenizer) override; + // Attaches an rc_client_leaderboard_info_t and populates the model from it. + void Attach(struct rc_client_leaderboard_info_t& pLeaderboard, + AssetCategory nCategory, const std::string& sDefinition); + // Replaces the attached rc_client_leaderboard_info_t. Does not update model or source. + void ReplaceAttached(struct rc_client_leaderboard_info_t& pLeaderboard) noexcept; + // Attaches an rc_client_leaderboard_info_t and populates it from the model. + void AttachAndInitialize(struct rc_client_leaderboard_info_t& pLeaderboard); + // Gets the attached rc_client_achievement_info_t. + const struct rc_client_leaderboard_info_t* GetAttached() const noexcept { return m_pLeaderboard; } + const CapturedTriggerHits& GetStartCapturedHits() const noexcept { return m_pCapturedStartTriggerHits; } const CapturedTriggerHits& GetSubmitCapturedHits() const noexcept { return m_pCapturedSubmitTriggerHits; } const CapturedTriggerHits& GetCancelCapturedHits() const noexcept { return m_pCapturedCancelTriggerHits; } @@ -205,17 +217,25 @@ public: protected: void OnValueChanged(const IntModelProperty::ChangeArgs& args) override; + void OnValueChanged(const StringModelProperty::ChangeArgs& args) override; bool ValidateAsset(std::wstring& sErrorMessage) override; private: void HandleStateChanged(AssetState nOldState, AssetState nNewState); + void SyncTitle(); + void SyncDescription(); + void SyncDefinition(); AssetDefinition m_pStartTrigger; AssetDefinition m_pSubmitTrigger; AssetDefinition m_pCancelTrigger; AssetDefinition m_pValueDefinition; + struct rc_client_leaderboard_info_t* m_pLeaderboard = nullptr; + std::string m_sTitleBuffer; + std::string m_sDescriptionBuffer; + CapturedTriggerHits m_pCapturedStartTriggerHits; CapturedTriggerHits m_pCapturedSubmitTriggerHits; CapturedTriggerHits m_pCapturedCancelTriggerHits; diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp index 0954a833..32c1e46d 100644 --- a/src/services/AchievementRuntime.cpp +++ b/src/services/AchievementRuntime.cpp @@ -1,5 +1,6 @@ #include "AchievementRuntime.hh" +#include "Exports.hh" #include "RA_Defs.h" #include "RA_Log.h" #include "RA_StringUtils.h" @@ -8,430 +9,1692 @@ #include "data\context\EmulatorContext.hh" #include "data\context\GameContext.hh" +#include "data\context\SessionTracker.hh" #include "data\context\UserContext.hh" +#include "services\FrameEventQueue.hh" +#include "services\Http.hh" +#include "services\IAudioSystem.hh" +#include "services\IConfiguration.hh" #include "services\IFileSystem.hh" -#include "services\IThreadPool.hh" +#include "services\ILocalStorage.hh" #include "services\ServiceLocator.hh" +#include "ui\viewmodels\MessageBoxViewModel.hh" +#include "ui\viewmodels\OverlayManager.hh" +#include "ui\viewmodels\PopupMessageViewModel.hh" +#include "ui\viewmodels\WindowManager.hh" + +#include +#include // for parsing cached patchdata response #include +#include #include namespace ra { namespace services { -void AchievementRuntime::EnsureInitialized() noexcept +static std::map s_mAchievementPopups; + +static int CanSubmit() { - if (!m_bInitialized) + const auto& pGameContext = ra::services::ServiceLocator::Get(); + if (pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest) + return 0; + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline)) + return 0; + + const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + if (pEmulatorContext.WasMemoryModified()) + return 0; + + if (pEmulatorContext.IsMemoryInsecure()) + return 0; + + return 1; +} + +static int CanSubmitAchievementUnlock(uint32_t nAchievementId, rc_client_t*) +{ + if (!CanSubmit()) + return 0; + + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* vmAchievement = pGameContext.Assets().FindAchievement(nAchievementId); + if (vmAchievement == nullptr || + vmAchievement->GetChanges() != ra::data::models::AssetChanges::None || + vmAchievement->GetCategory() != ra::data::models::AssetCategory::Core) { - rc_runtime_init(&m_pRuntime); - m_bInitialized = true; + return 0; + } + + return 1; +} + +static int CanSubmitLeaderboardEntry(uint32_t nLeaderboardId, rc_client_t*) +{ + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)) + return false; + + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* pLeaderboard = pGameContext.Assets().FindLeaderboard(nLeaderboardId); + if (pLeaderboard == nullptr || + pLeaderboard->GetChanges() != ra::data::models::AssetChanges::None || + pLeaderboard->GetCategory() != ra::data::models::AssetCategory::Core) + { + return 0; + } + + return CanSubmit(); +} + +AchievementRuntime::AchievementRuntime() +{ + m_pClient.reset(rc_client_create(AchievementRuntime::ReadMemory, AchievementRuntime::ServerCallAsync)); + + m_pClient->callbacks.can_submit_achievement_unlock = CanSubmitAchievementUnlock; + m_pClient->callbacks.can_submit_leaderboard_entry = CanSubmitLeaderboardEntry; + +#ifndef RA_UTEST + rc_client_enable_logging(m_pClient.get(), RC_CLIENT_LOG_LEVEL_VERBOSE, AchievementRuntime::LogMessage); +#endif + + rc_client_set_event_handler(m_pClient.get(), EventHandler); + + rc_client_set_unofficial_enabled(m_pClient.get(), 1); +} + +AchievementRuntime::~AchievementRuntime() { Shutdown(); } + +static void DummyEventHandler(const rc_client_event_t*, rc_client_t*) noexcept +{ +} + +void AchievementRuntime::Shutdown() noexcept +{ + if (m_pClient != nullptr) + { + // don't need to handle events while shutting down + rc_client_set_event_handler(m_pClient.get(), DummyEventHandler); + + rc_client_destroy(m_pClient.release()); } } -void AchievementRuntime::ResetRuntime() noexcept +void AchievementRuntime::LogMessage(const char* sMessage, const rc_client_t*) { - if (m_bInitialized) + const auto& pLogger = ra::services::ServiceLocator::Get(); + if (pLogger.IsEnabled(ra::services::LogLevel::Info)) + pLogger.LogMessage(ra::services::LogLevel::Info, sMessage); +} + +uint32_t AchievementRuntime::ReadMemory(uint32_t nAddress, uint8_t* pBuffer, uint32_t nBytes, rc_client_t*) +{ + const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + return pEmulatorContext.ReadMemory(nAddress, pBuffer, nBytes); +} + +static void ConvertHttpResponseToApiServerResponse(rc_api_server_response_t& pResponse, + const ra::services::Http::Response& httpResponse) noexcept +{ + memset(&pResponse, 0, sizeof(pResponse)); + pResponse.http_status_code = ra::etoi(httpResponse.StatusCode()); + pResponse.body = httpResponse.Content().c_str(); + pResponse.body_length = httpResponse.Content().length(); +} + +void AchievementRuntime::ServerCallAsync(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, + void* pCallbackData, rc_client_t*) +{ + ra::services::Http::Request httpRequest(pRequest->url); + httpRequest.SetPostData(pRequest->post_data); + httpRequest.SetContentType(pRequest->content_type); + + httpRequest.CallAsync([fCallback, pCallbackData](const ra::services::Http::Response& httpResponse) noexcept { + rc_api_server_response_t pResponse; + ConvertHttpResponseToApiServerResponse(pResponse, httpResponse); + + fCallback(&pResponse, pCallbackData); + }); +} + +/* ---- ClientSynchronizer ----- */ + +class AchievementRuntime::ClientSynchronizer +{ +public: + ClientSynchronizer() = default; + virtual ~ClientSynchronizer() { - rc_runtime_destroy(&m_pRuntime); - m_bInitialized = false; + m_pPublishedSubset = nullptr; + + for (auto* pMemory : m_vAllocatedMemory) + free(pMemory); } -} + ClientSynchronizer(const ClientSynchronizer&) noexcept = delete; + ClientSynchronizer& operator=(const ClientSynchronizer&) noexcept = delete; + ClientSynchronizer(ClientSynchronizer&&) noexcept = delete; + ClientSynchronizer& operator=(ClientSynchronizer&&) noexcept = delete; -GSL_SUPPRESS_F6 -void AchievementRuntime::ResetActiveAchievements() -{ - if (m_bInitialized) +private: + rc_client_subset_info_t* m_pPublishedSubset = nullptr; + rc_client_t* m_pClient = nullptr; + rc_memref_t** m_pNextMemref = nullptr; + rc_value_t** m_pNextVariable = nullptr; + + typedef struct rc_client_subset_wrapper_t { - rc_runtime_reset(&m_pRuntime); - RA_LOG_INFO("Runtime reset"); - } -} + rc_client_subset_info_t pCoreSubset{}; + rc_client_subset_info_t pLocalSubset{}; + rc_api_buffer_t pBuffer{}; + std::vector vAllocatedMemory; + } SubsetWrapper; + std::unique_ptr m_pSubsetWrapper; -int AchievementRuntime::ActivateAchievement(ra::AchievementID nId, const std::string& sTrigger) -{ - std::lock_guard pLock(m_pMutex); - EnsureInitialized(); + std::vector m_vAllocatedMemory; - // When an achievement is activated, it's set to Waiting. The state of the achievement must evaluate - // to false for at least one frame before it is promoted to Active. This ensures achievements don't - // trigger from uninitialized (or randomly initialized) memory when first starting a game. - return rc_runtime_activate_achievement(&m_pRuntime, nId, sTrigger.c_str(), nullptr, 0); -} - -rc_trigger_t* AchievementRuntime::GetAchievementTrigger(ra::AchievementID nId, const std::string& sTrigger) -{ - if (!m_bInitialized) - return nullptr; - - unsigned char md5[16]{}; - md5_state_t state{}; - const md5_byte_t* bytes; - GSL_SUPPRESS_TYPE1 bytes = reinterpret_cast(sTrigger.c_str()); - - md5_init(&state); - md5_append(&state, bytes, gsl::narrow_cast(sTrigger.length())); - md5_finish(&state, md5); - - std::lock_guard pLock(m_pMutex); - for (size_t i = 0; i < m_pRuntime.trigger_count; ++i) + static bool DetachMemory(std::vector& pAllocatedMemory, void* pMemory) noexcept { - if (m_pRuntime.triggers[i].id == nId && memcmp(m_pRuntime.triggers[i].md5, md5, sizeof(md5)) == 0) + for (auto pIter = pAllocatedMemory.begin(); pIter != pAllocatedMemory.end(); ++pIter) { - if (m_pRuntime.triggers[i].trigger) - return m_pRuntime.triggers[i].trigger; + if (*pIter == pMemory) + { + pAllocatedMemory.erase(pIter); + return true; + } + } - return static_cast(m_pRuntime.triggers[i].buffer); + return false; + } + + bool AllocatedMemrefs() noexcept + { + bool bAllocatedMemref = false; + + /* if at least one memref was allocated within the object, we can't free the buffer when the object is + * deactivated */ + if (*m_pNextMemref != nullptr) + { + bAllocatedMemref = true; + /* advance through the new memrefs so we're ready for the next allocation */ + do + { + m_pNextMemref = &(*m_pNextMemref)->next; + } while (*m_pNextMemref != nullptr); + } + + /* if at least one variable was allocated within the object, we can't free the buffer when the object is + * deactivated */ + if (*m_pNextVariable != nullptr) + { + bAllocatedMemref = true; + /* advance through the new variables so we're ready for the next allocation */ + do + { + m_pNextVariable = &(*m_pNextVariable)->next; + } while (*m_pNextVariable != nullptr); + } + + return bAllocatedMemref; + } + + static rc_client_leaderboard_info_t* FindLeaderboard(rc_client_subset_info_t* pSubset, uint32_t nId) + { + Expects(pSubset != nullptr); + + auto* pLeaderboard = pSubset->leaderboards; + const auto* pLeaderboardStop = pLeaderboard + pSubset->public_.num_leaderboards; + for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard) + { + if (pLeaderboard->public_.id == nId) + return pLeaderboard; + } + + return nullptr; + } + + static rc_client_achievement_info_t* FindAchievement(rc_client_subset_info_t* pSubset, uint32_t nId) + { + Expects(pSubset != nullptr); + + auto* pAchievement = pSubset->achievements; + const auto* pAchievementStop = pAchievement + pSubset->public_.num_achievements; + for (; pAchievement < pAchievementStop; ++pAchievement) + { + if (pAchievement->public_.id == nId) + return pAchievement; + } + + return nullptr; + } + + void SyncSubset(rc_client_subset_info_t* pSubset, SubsetWrapper* pSubsetWrapper, + std::vector& vAchievements, + std::vector& vLeaderboards) + { + pSubset->active = true; + + if (!vAchievements.empty()) + { + pSubset->achievements = static_cast( + rc_buf_alloc(&pSubsetWrapper->pBuffer, vAchievements.size() * sizeof(*pSubset->achievements))); + memset(pSubset->achievements, 0, vAchievements.size() * sizeof(*pSubset->achievements)); + + rc_client_achievement_info_t* pAchievement = pSubset->achievements; + rc_client_achievement_info_t* pSrcAchievement = nullptr; + for (auto* vmAchievement : vAchievements) + { + Expects(vmAchievement != nullptr); + + const auto nAchievementId = vmAchievement->GetID(); + if (vmAchievement->GetChanges() == ra::data::models::AssetChanges::None) + { + rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset; + for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next) + { + pSrcAchievement = FindAchievement(pPublishedSubset, nAchievementId); + if (pSrcAchievement != nullptr) + { + memcpy(pAchievement, pSrcAchievement, sizeof(*pAchievement)); + vmAchievement->ReplaceAttached(*pAchievement++); + break; + } + } + } + else + { + pSrcAchievement = nullptr; + if (m_pSubsetWrapper != nullptr) + { + if (vmAchievement->GetCategory() == ra::data::models::AssetCategory::Local) + pSrcAchievement = FindAchievement(&m_pSubsetWrapper->pLocalSubset, nAchievementId); + else + pSrcAchievement = FindAchievement(&m_pSubsetWrapper->pCoreSubset, nAchievementId); + } + + if (pSrcAchievement != nullptr && pSrcAchievement == vmAchievement->GetAttached()) + { + // transfer the trigger ownership to the new SubsetWrapper + if (pSrcAchievement->trigger && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcAchievement->trigger)) + pSubsetWrapper->vAllocatedMemory.push_back(pSrcAchievement->trigger); + + memcpy(pAchievement, pSrcAchievement, sizeof(*pAchievement)); + vmAchievement->ReplaceAttached(*pAchievement++); + } + else + { + if (pSrcAchievement != nullptr && pSrcAchievement->trigger) + { + const auto& sTrigger = vmAchievement->GetTrigger(); + uint8_t md5[16]; + rc_runtime_checksum(sTrigger.c_str(), md5); + if (memcmp(pSrcAchievement->md5, md5, sizeof(md5)) == 0) + { + memcpy(pAchievement->md5, pSrcAchievement->md5, sizeof(md5)); + pAchievement->trigger = pSrcAchievement->trigger; + + // transfer the trigger ownership to the new SubsetWrapper + if (DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcAchievement->trigger)) + pSubsetWrapper->vAllocatedMemory.push_back(pSrcAchievement->trigger); + } + } + + // no rc_client_achievement_t found for this achievement, populate from the model + vmAchievement->AttachAndInitialize(*pAchievement++); + } + } + } + + pSubset->public_.num_achievements = gsl::narrow_cast(pAchievement - pSubset->achievements); + } + + if (!vLeaderboards.empty()) + { + pSubset->leaderboards = static_cast( + rc_buf_alloc(&pSubsetWrapper->pBuffer, vLeaderboards.size() * sizeof(*pSubset->leaderboards))); + memset(pSubset->leaderboards, 0, vLeaderboards.size() * sizeof(*pSubset->leaderboards)); + + rc_client_leaderboard_info_t* pLeaderboard = pSubset->leaderboards; + rc_client_leaderboard_info_t* pSrcLeaderboard = nullptr; + for (auto* vmLeaderboard : vLeaderboards) + { + Expects(vmLeaderboard != nullptr); + + const auto nLeaderboardId = vmLeaderboard->GetID(); + if (vmLeaderboard->GetChanges() == ra::data::models::AssetChanges::None) + { + rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset; + for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next) + { + pSrcLeaderboard = FindLeaderboard(pPublishedSubset, nLeaderboardId); + if (pSrcLeaderboard != nullptr) + { + memcpy(pLeaderboard, pSrcLeaderboard, sizeof(*pLeaderboard)); + vmLeaderboard->ReplaceAttached(*pLeaderboard++); + break; + } + } + } + else + { + pSrcLeaderboard = nullptr; + if (m_pSubsetWrapper != nullptr && vmLeaderboard->GetAttached() != nullptr) + { + if (vmLeaderboard->GetCategory() == ra::data::models::AssetCategory::Local) + pSrcLeaderboard = FindLeaderboard(&m_pSubsetWrapper->pLocalSubset, nLeaderboardId); + else + pSrcLeaderboard = FindLeaderboard(&m_pSubsetWrapper->pCoreSubset, nLeaderboardId); + } + + if (pSrcLeaderboard != nullptr && pSrcLeaderboard == vmLeaderboard->GetAttached()) + { + // transfer the lboard ownership to the new SubsetWrapper + if (pSrcLeaderboard->lboard && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcLeaderboard->lboard)) + pSubsetWrapper->vAllocatedMemory.push_back(pSrcLeaderboard->lboard); + + memcpy(pLeaderboard, pSrcLeaderboard, sizeof(*pLeaderboard)); + vmLeaderboard->ReplaceAttached(*pLeaderboard++); + } + else + { + if (pSrcLeaderboard != nullptr && pSrcLeaderboard->lboard) + { + const auto& sTrigger = vmLeaderboard->GetDefinition(); + uint8_t md5[16]; + rc_runtime_checksum(sTrigger.c_str(), md5); + if (memcmp(pSrcLeaderboard->md5, md5, sizeof(md5)) == 0) + { + memcpy(pLeaderboard->md5, pSrcLeaderboard->md5, sizeof(md5)); + pLeaderboard->lboard = pSrcLeaderboard->lboard; + + // transfer the trigger ownership to the new SubsetWrapper + if (DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pSrcLeaderboard->lboard)) + pSubsetWrapper->vAllocatedMemory.push_back(pSrcLeaderboard->lboard); + } + } + + // no rc_client_leaderboard_t found for this leaderboard, populate from the model + vmLeaderboard->AttachAndInitialize(*pLeaderboard++); + } + } + } + + pSubset->public_.num_leaderboards = gsl::narrow_cast(pLeaderboard - pSubset->leaderboards); + } + } + +public: + void SyncAssets(rc_client_t* pClient) + { + if (!pClient->game) + return; + + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + auto& vAssets = pGameContext.Assets(); + + if (m_pClient == nullptr) + { + m_pClient = pClient; + m_pNextMemref = &pClient->game->runtime.memrefs; + m_pNextVariable = &pClient->game->runtime.variables; + m_pPublishedSubset = pClient->game->subsets; + AllocatedMemrefs(); // advance pointers + } + + std::vector vCoreAchievements; + std::vector vLocalAchievements; + std::vector vCoreLeaderboards; + std::vector vLocalLeaderboards; + + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(vAssets.Count()); ++nIndex) + { + auto* pAsset = vAssets.GetItemAt(nIndex); + Expects(pAsset != nullptr); + if (pAsset->GetChanges() == ra::data::models::AssetChanges::Deleted) + continue; + + auto* vmAchievement = dynamic_cast(pAsset); + if (vmAchievement != nullptr) + { + if (vmAchievement->GetCategory() == ra::data::models::AssetCategory::Local) + vLocalAchievements.push_back(vmAchievement); + else + vCoreAchievements.push_back(vmAchievement); + + continue; + } + + auto* vmLeaderboard = dynamic_cast(pAsset); + if (vmLeaderboard != nullptr) + { + if (vmLeaderboard->GetCategory() == ra::data::models::AssetCategory::Local) + vLocalLeaderboards.push_back(vmLeaderboard); + else + vCoreLeaderboards.push_back(vmLeaderboard); + + continue; + } + } + + std::unique_ptr pSubsetWrapper; + pSubsetWrapper.reset(new SubsetWrapper); + memset(&pSubsetWrapper->pCoreSubset, 0, sizeof(pSubsetWrapper->pCoreSubset)); + memset(&pSubsetWrapper->pLocalSubset, 0, sizeof(pSubsetWrapper->pLocalSubset)); + rc_buf_init(&pSubsetWrapper->pBuffer); + + pSubsetWrapper->pCoreSubset.public_.id = pClient->game->public_.id; + pSubsetWrapper->pCoreSubset.public_.title = pClient->game->public_.title; + snprintf(pSubsetWrapper->pCoreSubset.public_.badge_name, sizeof(pSubsetWrapper->pCoreSubset.public_.badge_name), + "%s", pClient->game->public_.badge_name); + + SyncSubset(&pSubsetWrapper->pCoreSubset, pSubsetWrapper.get(), vCoreAchievements, vCoreLeaderboards); + + if (!vLocalAchievements.empty() || !vLocalLeaderboards.empty()) + { + pSubsetWrapper->pLocalSubset.public_.id = ra::data::context::GameAssets::LocalSubsetId; + pSubsetWrapper->pLocalSubset.public_.title = "Local"; + snprintf(pSubsetWrapper->pLocalSubset.public_.badge_name, + sizeof(pSubsetWrapper->pLocalSubset.public_.badge_name), "%s", pClient->game->public_.badge_name); + + SyncSubset(&pSubsetWrapper->pLocalSubset, pSubsetWrapper.get(), vLocalAchievements, vLocalLeaderboards); + + pSubsetWrapper->pCoreSubset.next = &pSubsetWrapper->pLocalSubset; + } + + std::swap(pSubsetWrapper, m_pSubsetWrapper); + + if (pSubsetWrapper != nullptr) + { + for (auto* pMemory : pSubsetWrapper->vAllocatedMemory) + free(pMemory); + } + + rc_mutex_lock(&pClient->state.mutex); + pClient->game->subsets = &m_pSubsetWrapper->pCoreSubset; + rc_mutex_unlock(&pClient->state.mutex); + } + + void AttachMemory(void* pMemory) + { + if (AllocatedMemrefs()) + { + // if memrefs were allocated, we can't release this memory until the game is unloaded + m_vAllocatedMemory.push_back(pMemory); + } + else + { + m_pSubsetWrapper->vAllocatedMemory.push_back(pMemory); + } + } + + bool DetachMemory(void* pMemory) noexcept + { + if (!m_pSubsetWrapper) + return false; + + return DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pMemory); + } + +}; + +void AchievementRuntime::SyncAssets() +{ + if (m_pClientSynchronizer == nullptr) + m_pClientSynchronizer.reset(new ClientSynchronizer()); + m_pClientSynchronizer->SyncAssets(GetClient()); +} + +void AchievementRuntime::AttachMemory(void* pMemory) +{ + if (m_pClientSynchronizer != nullptr) + m_pClientSynchronizer->AttachMemory(pMemory); +} + +bool AchievementRuntime::DetachMemory(void* pMemory) noexcept +{ + if (m_pClientSynchronizer != nullptr) + return m_pClientSynchronizer->DetachMemory(pMemory); + + return false; +} + +static rc_client_achievement_info_t* GetAchievementInfo(rc_client_t* pClient, ra::AchievementID nId) noexcept +{ + rc_client_subset_info_t* subset = pClient->game->subsets; + for (; subset; subset = subset->next) + { + rc_client_achievement_info_t* achievement = subset->achievements; + const rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + + for (; achievement < stop; ++achievement) + { + if (achievement->public_.id == nId) + return achievement; } } return nullptr; } -void AchievementRuntime::DeactivateAchievement(ra::AchievementID nId) +rc_trigger_t* AchievementRuntime::GetAchievementTrigger(ra::AchievementID nId) const noexcept { - // NOTE: rc_runtime_deactivate_achievement will either reset the trigger or free it - // we want to keep it so the user can examine the hit counts after an incorrect trigger - // so we're just going to detach it (memory is still held by m_pRuntime.triggers[i].buffer - DetachAchievementTrigger(nId); + rc_client_achievement_info_t* achievement = GetAchievementInfo(GetClient(), nId); + return (achievement != nullptr) ? achievement->trigger : nullptr; } -rc_trigger_t* AchievementRuntime::DetachAchievementTrigger(ra::AchievementID nId) +static rc_client_leaderboard_info_t* GetLeaderboardInfo(rc_client_t* pClient, ra::LeaderboardID nId) noexcept { - if (!m_bInitialized) - return nullptr; - - std::lock_guard pLock(m_pMutex); - for (unsigned i = 0; i < m_pRuntime.trigger_count; ++i) + rc_client_subset_info_t* subset = pClient->game->subsets; + for (; subset; subset = subset->next) { - if (m_pRuntime.triggers[i].id == nId) + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + const rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { - auto* pTrigger = m_pRuntime.triggers[i].trigger; - if (pTrigger) - { - m_pRuntime.triggers[i].trigger = nullptr; - return pTrigger; - } + if (leaderboard->public_.id == nId) + return leaderboard; } } return nullptr; } -void AchievementRuntime::ReleaseAchievementTrigger(ra::AchievementID nId, _In_ rc_trigger_t* pTrigger) +rc_lboard_t* AchievementRuntime::GetLeaderboardDefinition(ra::LeaderboardID nId) const noexcept { - if (!m_bInitialized) - return; - - std::lock_guard pLock(m_pMutex); - -#pragma warning(push) -#pragma warning(disable:6001) // Using uninitialized memory 'pTrigger' - - for (unsigned i = 0; i < m_pRuntime.trigger_count; ++i) - { - if (m_pRuntime.triggers[i].id == nId && static_cast(m_pRuntime.triggers[i].buffer) == pTrigger) - { - // this duplicates logic from rc_runtime_deactivate_trigger_by_index - if (!m_pRuntime.triggers[i].owns_memrefs) - { - free(pTrigger); - - if (--m_pRuntime.trigger_count > i) - memcpy(&m_pRuntime.triggers[i], &m_pRuntime.triggers[m_pRuntime.trigger_count], sizeof(rc_runtime_trigger_t)); - - } - } - } - -#pragma warning(pop) + rc_client_leaderboard_info_t* leaderboard = GetLeaderboardInfo(GetClient(), nId); + return (leaderboard != nullptr) ? leaderboard->lboard : nullptr; } -void AchievementRuntime::UpdateAchievementId(ra::AchievementID nOldId, ra::AchievementID nNewId) +void AchievementRuntime::ReleaseLeaderboardTracker(ra::LeaderboardID nId) noexcept { - if (!m_bInitialized) - return; - - std::lock_guard pLock(m_pMutex); - for (unsigned i = 0; i < m_pRuntime.trigger_count; ++i) - { - if (m_pRuntime.triggers[i].id == nOldId) - m_pRuntime.triggers[i].id = nNewId; - } + rc_client_leaderboard_info_t* leaderboard = GetLeaderboardInfo(GetClient(), nId); + if (leaderboard) + rc_client_release_leaderboard_tracker(GetClient()->game, leaderboard); } -int AchievementRuntime::ActivateLeaderboard(unsigned int nId, const std::string& sDefinition) +bool AchievementRuntime::HasRichPresence() const noexcept { - std::lock_guard pLock(m_pMutex); - EnsureInitialized(); - - return rc_runtime_activate_lboard(&m_pRuntime, nId, sDefinition.c_str(), nullptr, 0); -} - -bool AchievementRuntime::ActivateRichPresence(const std::string& sScript) -{ - std::lock_guard pLock(m_pMutex); - EnsureInitialized(); - - if (sScript.empty()) - { - m_nRichPresenceParseResult = RC_OK; - if (m_pRuntime.richpresence != nullptr) - m_pRuntime.richpresence->richpresence = nullptr; - } - else - { - m_nRichPresenceParseResult = rc_runtime_activate_richpresence(&m_pRuntime, sScript.c_str(), nullptr, 0); - if (m_nRichPresenceParseResult != RC_OK) - m_nRichPresenceParseResult = rc_richpresence_size_lines(sScript.c_str(), &m_nRichPresenceErrorLine); - } - - return (m_nRichPresenceParseResult == RC_OK); + return m_nRichPresenceParseResult != RC_OK || rc_client_has_rich_presence(GetClient()); } std::wstring AchievementRuntime::GetRichPresenceDisplayString() const { - if (m_bInitialized) + if (m_nRichPresenceParseResult != RC_OK) { - std::lock_guard pLock(m_pMutex); + return ra::StringPrintf(L"Parse error %d (line %d): %s", m_nRichPresenceParseResult, + m_nRichPresenceErrorLine, rc_error_str(m_nRichPresenceParseResult)); + } + if (!HasRichPresence()) + return L"No Rich Presence defined."; + + char sRichPresence[256]; + if (rc_client_get_rich_presence_message(GetClient(), sRichPresence, sizeof(sRichPresence)) > 0) + return ra::Widen(sRichPresence); + + return L""; +} + +bool AchievementRuntime::ActivateRichPresence(const std::string& sScript) +{ + auto* game = GetClient()->game; + Expects(game != nullptr); + auto* runtime = &game->runtime; + Expects(runtime != nullptr); + + rc_mutex_lock(&GetClient()->state.mutex); + + if (sScript.empty()) + { + m_nRichPresenceParseResult = RC_OK; + if (runtime->richpresence != nullptr) + runtime->richpresence->richpresence = nullptr; + } + else + { + m_nRichPresenceParseResult = rc_runtime_activate_richpresence(runtime, sScript.c_str(), nullptr, 0); if (m_nRichPresenceParseResult != RC_OK) + m_nRichPresenceParseResult = rc_richpresence_size_lines(sScript.c_str(), &m_nRichPresenceErrorLine); + } + + rc_mutex_unlock(&GetClient()->state.mutex); + + return (m_nRichPresenceParseResult == RC_OK); +} + +/* ---- Login ----- */ + +void AchievementRuntime::BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword, + rc_client_callback_t fCallback, void* pCallbackData) +{ + GSL_SUPPRESS_R3 + auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData); + BeginLoginWithPassword(sUsername.c_str(), sPassword.c_str(), pCallbackWrapper); +} + +rc_client_async_handle_t* AchievementRuntime::BeginLoginWithPassword(const char* sUsername, const char* sPassword, + CallbackWrapper* pCallbackWrapper) noexcept +{ + return rc_client_begin_login_with_password(GetClient(), sUsername, sPassword, AchievementRuntime::LoginCallback, + pCallbackWrapper); +} + +void AchievementRuntime::BeginLoginWithToken(const std::string& sUsername, const std::string& sApiToken, + rc_client_callback_t fCallback, void* pCallbackData) +{ + GSL_SUPPRESS_R3 + auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData); + BeginLoginWithToken(sUsername.c_str(), sApiToken.c_str(), pCallbackWrapper); +} + +rc_client_async_handle_t* AchievementRuntime::BeginLoginWithToken(const char* sUsername, const char* sApiToken, + CallbackWrapper* pCallbackWrapper) noexcept +{ + return rc_client_begin_login_with_token(GetClient(), sUsername, sApiToken, AchievementRuntime::LoginCallback, + pCallbackWrapper); +} + +GSL_SUPPRESS_CON3 +void AchievementRuntime::LoginCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata) +{ + if (nResult == RC_OK) + { + const auto* user = rc_client_get_user_info(pClient); + if (!user) { - return ra::StringPrintf(L"Parse error %d (line %d): %s", m_nRichPresenceParseResult, - m_nRichPresenceErrorLine, rc_error_str(m_nRichPresenceParseResult)); + nResult = RC_INVALID_STATE; + sErrorMessage = "User data not available."; + } + else + { + // initialize the user context + auto& pUserContext = ra::services::ServiceLocator::GetMutable(); + if (pUserContext.IsLoginDisabled()) + { + nResult = RC_INVALID_STATE; + sErrorMessage = "Login has been disabled."; + } + else + { + pUserContext.Initialize(user->username, user->display_name, user->token); + pUserContext.SetScore(user->score); + } + } + } + + auto* wrapper = static_cast(pUserdata); + Expects(wrapper != nullptr); + wrapper->DoCallback(nResult, sErrorMessage); + + delete wrapper; +} + +/* ---- Load Game ----- */ + +void AchievementRuntime::BeginLoadGame(const std::string& sHash, unsigned id, rc_client_callback_t fCallback, + void* pCallbackData) +{ + GSL_SUPPRESS_R3 + auto* pCallbackWrapper = new LoadGameCallbackWrapper(m_pClient.get(), fCallback, pCallbackData); + BeginLoadGame(sHash.c_str(), id, pCallbackWrapper); +} + +static void ExtractPatchData(const rc_api_server_response_t* server_response, uint32_t nGameId) +{ + // extract the PatchData and store a copy in the cache for offline mode + rc_api_response_t api_response{}; + GSL_SUPPRESS_ES47 + rc_json_field_t fields[] = {RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("PatchData")}; + + if (rc_json_parse_server_response(&api_response, server_response, fields, sizeof(fields) / sizeof(fields[0])) == + RC_OK && + fields[2].value_start && fields[2].value_end) + { + auto& pLocalStorage = ra::services::ServiceLocator::GetMutable(); + auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId)); + if (pData != nullptr) + { + std::string sPatchData; + sPatchData.append(fields[2].value_start, fields[2].value_end - fields[2].value_start); + pData->Write(sPatchData); + } + } +} + +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) +{ + auto* wrapper = static_cast(pUserdata); + Expects(wrapper != nullptr); + + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + + auto pRichPresence = std::make_unique(); + pRichPresence->SetScript(game_data_response->rich_presence_script); + pRichPresence->CreateServerCheckpoint(); + 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; +#endif + + const rc_api_achievement_definition_t* pAchievement = game_data_response->achievements; + const rc_api_achievement_definition_t* pAchievementStop = pAchievement + game_data_response->num_achievements; + for (; pAchievement < pAchievementStop; ++pAchievement) + wrapper->m_mAchievementDefinitions[pAchievement->id] = pAchievement->definition; + + const rc_api_leaderboard_definition_t* pLeaderboard = game_data_response->leaderboards; + const rc_api_leaderboard_definition_t* pLeaderboardStop = pLeaderboard + game_data_response->num_leaderboards; + for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard) + wrapper->m_mLeaderboardDefinitions[pLeaderboard->id] = pLeaderboard->definition; + + ExtractPatchData(server_response, game_data_response->id); +} + +rc_client_async_handle_t* AchievementRuntime::BeginLoadGame(const char* sHash, unsigned id, + CallbackWrapper* pCallbackWrapper) noexcept +{ + auto* client = GetClient(); + + // unload the game and free any additional memory we allocated for local changes + rc_client_unload_game(client); + m_pClientSynchronizer.reset(); + s_mAchievementPopups.clear(); + + // if an ID was provided, store the hash->id mapping + if (id != 0) + { + auto* client_hash = rc_client_find_game_hash(client, sHash); + if (ra::to_signed(client_hash->game_id) < 0) + client_hash->game_id = id; + } + + // start the load process + client->callbacks.post_process_game_data_response = PostProcessGameDataResponse; + + return rc_client_begin_load_game(client, sHash, AchievementRuntime::LoadGameCallback, pCallbackWrapper); +} + +GSL_SUPPRESS_CON3 +void AchievementRuntime::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) +{ + auto* wrapper = static_cast(pUserdata); + Expects(wrapper != nullptr); + + if (nResult == RC_OK || nResult == RC_NO_GAME_LOADED) + { + // initialize the game context + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + pGameContext.InitializeFromAchievementRuntime(wrapper->m_mAchievementDefinitions, + wrapper->m_mLeaderboardDefinitions); + } + else + { + } + + wrapper->DoCallback(nResult, sErrorMessage); + + delete wrapper; +} + +/* ---- DoFrame ----- */ + +static void PrepareForPauseOnChangeEvents(rc_client_t* pClient, + std::vector& vAchievementsWithHits, + std::vector& vActiveAchievements, + std::map& mLeaderboardsWithHits, + std::map& mActiveLeaderboards) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pGameContext.Assets().Count()); ++nIndex) + { + auto* vmAsset = pGameContext.Assets().GetItemAt(nIndex); + + const auto* vmAchievement = dynamic_cast(vmAsset); + if (vmAchievement != nullptr) + { + if (vmAchievement->IsPauseOnReset()) + { + auto* pAchievement = GetAchievementInfo(pClient, vmAchievement->GetID()); + if (pAchievement && pAchievement->trigger && pAchievement->trigger->has_hits) + vAchievementsWithHits.push_back(pAchievement); + } + + if (vmAchievement->IsPauseOnTrigger()) + { + auto* pAchievement = GetAchievementInfo(pClient, vmAchievement->GetID()); + if (pAchievement && pAchievement->trigger) + { + switch (pAchievement->trigger->state) + { + case RC_TRIGGER_STATE_DISABLED: + case RC_TRIGGER_STATE_INACTIVE: + case RC_TRIGGER_STATE_PAUSED: + case RC_TRIGGER_STATE_TRIGGERED: + break; + + default: + vActiveAchievements.push_back(pAchievement); + break; + } + } + } + + continue; } - char sRichPresence[256]; - if (rc_runtime_get_richpresence(&m_pRuntime, sRichPresence, sizeof(sRichPresence), rc_peek_callback, nullptr, nullptr) > 0) - return ra::Widen(sRichPresence); - } - - return std::wstring(L"No Rich Presence defined."); -} - -static std::vector* g_pChanges; - -static void map_event_to_change(const rc_runtime_event_t* pRuntimeEvent) -{ - auto nChangeType = AchievementRuntime::ChangeType::None; - - switch (pRuntimeEvent->type) - { - case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - nChangeType = AchievementRuntime::ChangeType::AchievementTriggered; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_RESET: - nChangeType = AchievementRuntime::ChangeType::AchievementReset; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED: - nChangeType = AchievementRuntime::ChangeType::AchievementPaused; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED: - nChangeType = AchievementRuntime::ChangeType::AchievementDisabled; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED: - nChangeType = AchievementRuntime::ChangeType::AchievementPrimed; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED: - nChangeType = AchievementRuntime::ChangeType::AchievementUnprimed; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED: - nChangeType = AchievementRuntime::ChangeType::AchievementActivated; - break; - case RC_RUNTIME_EVENT_LBOARD_STARTED: - nChangeType = AchievementRuntime::ChangeType::LeaderboardStarted; - break; - case RC_RUNTIME_EVENT_LBOARD_CANCELED: - nChangeType = AchievementRuntime::ChangeType::LeaderboardCanceled; - break; - case RC_RUNTIME_EVENT_LBOARD_UPDATED: - nChangeType = AchievementRuntime::ChangeType::LeaderboardUpdated; - break; - case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - nChangeType = AchievementRuntime::ChangeType::LeaderboardTriggered; - break; - case RC_RUNTIME_EVENT_LBOARD_DISABLED: - nChangeType = AchievementRuntime::ChangeType::LeaderboardDisabled; - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED: - nChangeType = AchievementRuntime::ChangeType::AchievementProgressChanged; - break; - default: - // unsupported - return; - } - - g_pChanges->emplace_back(AchievementRuntime::Change{ nChangeType, pRuntimeEvent->id, pRuntimeEvent->value }); -} - -typedef struct LeaderboardPauseFlags -{ - ra::LeaderboardID nID; - ra::data::models::LeaderboardModel::LeaderboardParts nPauseOnReset; - ra::data::models::LeaderboardModel::LeaderboardParts nPauseOnTrigger; -} LeaderboardPauseFlags; - -static void GetLeaderboardPauseFlags(std::vector& vLeaderboardPauseFlags, const rc_runtime_t* pRuntime) -{ - const auto& vAssets = ra::services::ServiceLocator::Get().Assets(); - for (gsl::index i = 0; i < gsl::narrow_cast(vAssets.Count()); ++i) - { - const auto* pLeaderboard = dynamic_cast(vAssets.GetItemAt(i)); - if (pLeaderboard != nullptr) + const auto* vmLeaderboard = dynamic_cast(vmAsset); + if (vmLeaderboard != nullptr) { - LeaderboardPauseFlags pLeaderboardPauseFlags; - pLeaderboardPauseFlags.nPauseOnReset = pLeaderboard->GetPauseOnReset(); - pLeaderboardPauseFlags.nPauseOnTrigger = pLeaderboard->GetPauseOnTrigger(); - if (pLeaderboardPauseFlags.nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None || - pLeaderboardPauseFlags.nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None) + using namespace ra::bitwise_ops; + + auto nPauseOnReset = vmLeaderboard->GetPauseOnReset(); + if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None) { - pLeaderboardPauseFlags.nID = pLeaderboard->GetID(); - - const auto* pLBoard = rc_runtime_get_lboard(pRuntime, pLeaderboardPauseFlags.nID); - if (pLBoard != nullptr) + auto* pLeaderboard = GetLeaderboardInfo(pClient, vmLeaderboard->GetID()); + if (pLeaderboard && pLeaderboard->lboard) { - using namespace ra::bitwise_ops; + if (!pLeaderboard->lboard->start.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; + if (!pLeaderboard->lboard->cancel.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; + if (!pLeaderboard->lboard->submit.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; + if (!pLeaderboard->lboard->value.value.value) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Value; - if (pLeaderboardPauseFlags.nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - if (!pLBoard->start.has_hits) - pLeaderboardPauseFlags.nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; - if (!pLBoard->submit.has_hits) - pLeaderboardPauseFlags.nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; - if (!pLBoard->cancel.has_hits) - pLeaderboardPauseFlags.nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; - if (!pLBoard->value.value.value) - pLeaderboardPauseFlags.nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Value; - } + if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None) + mLeaderboardsWithHits[pLeaderboard] = nPauseOnReset; + } + } - if (pLeaderboardPauseFlags.nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - if (pLBoard->start.state == RC_TRIGGER_STATE_TRIGGERED) - pLeaderboardPauseFlags.nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; - if (pLBoard->submit.state == RC_TRIGGER_STATE_TRIGGERED) - pLeaderboardPauseFlags.nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; - if (pLBoard->cancel.state == RC_TRIGGER_STATE_TRIGGERED) - pLeaderboardPauseFlags.nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; - } + auto nPauseOnTrigger = vmLeaderboard->GetPauseOnTrigger(); + if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + auto* pLeaderboard = GetLeaderboardInfo(pClient, vmLeaderboard->GetID()); + if (pLeaderboard && pLeaderboard->lboard) + { + if (pLeaderboard->lboard->start.state == RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; + if (pLeaderboard->lboard->cancel.state == RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; + if (pLeaderboard->lboard->submit.state == RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; - const auto nMonitorFlags = pLeaderboardPauseFlags.nPauseOnReset | pLeaderboardPauseFlags.nPauseOnTrigger; - if (nMonitorFlags != ra::data::models::LeaderboardModel::LeaderboardParts::None) - vLeaderboardPauseFlags.push_back(pLeaderboardPauseFlags); + if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None) + mActiveLeaderboards[pLeaderboard] = nPauseOnTrigger; } } } } } -static void CheckForLeaderboardPauseChanges(const std::vector& vLeaderboardPauseFlags, - const rc_runtime_t* pRuntime, std::vector& changes) +static void RaisePauseOnChangeEvents(std::vector& vAchievementsWithHits, + std::vector& vActiveAchievements) { - for (const auto& pLeaderboardPauseFlags : vLeaderboardPauseFlags) + for (const auto* pAchievement : vAchievementsWithHits) { - const auto* pLBoard = rc_runtime_get_lboard(pRuntime, pLeaderboardPauseFlags.nID); - if (pLBoard != nullptr) + if (pAchievement != nullptr && pAchievement->trigger && !pAchievement->trigger->has_hits) + { + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); + pFrameEventQueue.QueuePauseOnReset(ra::Widen(pAchievement->public_.title)); + } + } + + for (const auto* pAchievement : vActiveAchievements) + { + if (pAchievement && pAchievement->trigger && pAchievement->trigger->state == RC_TRIGGER_STATE_TRIGGERED) + { + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); + pFrameEventQueue.QueuePauseOnTrigger(ra::Widen(pAchievement->public_.title)); + } + } +} + +static void RaisePauseOnChangeEvents( + std::map& mLeaderboardsWithHits, + std::map& mActiveLeaderboards) +{ + for (auto pair : mLeaderboardsWithHits) + { + if (pair.first && pair.first->lboard) { using namespace ra::bitwise_ops; - if (!pLBoard->start.has_hits && - (pLeaderboardPauseFlags.nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Start) != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardStartReset, pLeaderboardPauseFlags.nID, 0 }); - } + auto nPauseOnReset = pair.second; + const auto* pLeaderboard = pair.first->lboard; + if (pLeaderboard->start.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; + if (pLeaderboard->cancel.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; + if (pLeaderboard->submit.has_hits) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; + if (pLeaderboard->value.value.value) + nPauseOnReset &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Value; - if (!pLBoard->submit.has_hits && - (pLeaderboardPauseFlags.nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) != ra::data::models::LeaderboardModel::LeaderboardParts::None) + if (nPauseOnReset != ra::data::models::LeaderboardModel::LeaderboardParts::None) { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardSubmitReset, pLeaderboardPauseFlags.nID, 0 }); - } + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); - if (!pLBoard->cancel.has_hits && - (pLeaderboardPauseFlags.nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardCancelReset, pLeaderboardPauseFlags.nID, 0 }); - } + if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Start) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Start: %s", pair.first->public_.title)); + } - if (!pLBoard->value.value.value && - (pLeaderboardPauseFlags.nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Value) != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardValueReset, pLeaderboardPauseFlags.nID, 0 }); - } + if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Cancel: %s", pair.first->public_.title)); + } - if (pLBoard->start.state == RC_TRIGGER_STATE_TRIGGERED && - (pLeaderboardPauseFlags.nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Start) != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardStartTriggered, pLeaderboardPauseFlags.nID, 0 }); - } + if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Submit: %s", pair.first->public_.title)); + } - if (pLBoard->submit.state == RC_TRIGGER_STATE_TRIGGERED && - (pLeaderboardPauseFlags.nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) != ra::data::models::LeaderboardModel::LeaderboardParts::None) - { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardSubmitTriggered, pLeaderboardPauseFlags.nID, 0 }); + if ((nPauseOnReset & ra::data::models::LeaderboardModel::LeaderboardParts::Value) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnReset(ra::StringPrintf(L"Value: %s", pair.first->public_.title)); + } } + } + } - if (pLBoard->cancel.state == RC_TRIGGER_STATE_TRIGGERED && - (pLeaderboardPauseFlags.nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) != ra::data::models::LeaderboardModel::LeaderboardParts::None) + for (auto pair : mActiveLeaderboards) + { + if (pair.first && pair.first->lboard) + { + using namespace ra::bitwise_ops; + + auto nPauseOnTrigger = pair.second; + const auto* pLeaderboard = pair.first->lboard; + if (pLeaderboard->start.state != RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Start; + if (pLeaderboard->cancel.state != RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Cancel; + if (pLeaderboard->submit.state != RC_TRIGGER_STATE_TRIGGERED) + nPauseOnTrigger &= ~ra::data::models::LeaderboardModel::LeaderboardParts::Submit; + + if (nPauseOnTrigger != ra::data::models::LeaderboardModel::LeaderboardParts::None) { - changes.emplace_back(AchievementRuntime::Change - { AchievementRuntime::ChangeType::LeaderboardCancelTriggered, pLeaderboardPauseFlags.nID, 0 }); + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); + + if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Start) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Start: %s", pair.first->public_.title)); + } + + if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Cancel) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Cancel: %s", pair.first->public_.title)); + } + + if ((nPauseOnTrigger & ra::data::models::LeaderboardModel::LeaderboardParts::Submit) != + ra::data::models::LeaderboardModel::LeaderboardParts::None) + { + pFrameEventQueue.QueuePauseOnTrigger(ra::StringPrintf(L"Submit: %s", pair.first->public_.title)); + } } } } } -_Use_decl_annotations_ void AchievementRuntime::Process(std::vector& changes) +void AchievementRuntime::DoFrame() { - if (!m_bInitialized || m_bPaused) - return; - - std::vector vLeaderboardPauseFlags; - if (m_pRuntime.lboard_count > 0) - GetLeaderboardPauseFlags(vLeaderboardPauseFlags, &m_pRuntime); - + if (m_bPaused) { - std::lock_guard pLock(m_pMutex); - - g_pChanges = &changes; - rc_runtime_do_frame(&m_pRuntime, map_event_to_change, rc_peek_callback, nullptr, nullptr); - g_pChanges = nullptr; + rc_client_idle(GetClient()); + return; } - if (!vLeaderboardPauseFlags.empty()) - CheckForLeaderboardPauseChanges(vLeaderboardPauseFlags, &m_pRuntime, changes); + std::vector vAchievementsWithHits; + std::vector vActiveAchievements; + std::map mLeaderboardsWithHits; + std::map mActiveLeaderboards; + + PrepareForPauseOnChangeEvents(GetClient(), vAchievementsWithHits, vActiveAchievements, mLeaderboardsWithHits, mActiveLeaderboards); + + rc_client_do_frame(GetClient()); + + if (!vAchievementsWithHits.empty() || !vActiveAchievements.empty()) + RaisePauseOnChangeEvents(vAchievementsWithHits, vActiveAchievements); + if (!mLeaderboardsWithHits.empty() || !mActiveLeaderboards.empty()) + RaisePauseOnChangeEvents(mLeaderboardsWithHits, mActiveLeaderboards); } -_NODISCARD static char ComparisonSizeFromPrefix(_In_ char cPrefix) noexcept +void AchievementRuntime::InvalidateAddress(ra::ByteAddress nAddress) noexcept { - char buffer[] = "0xH0"; - const char* ptr = buffer; - buffer[2] = cPrefix; - - char size = RC_MEMSIZE_16_BITS; - unsigned address = 0; - rc_parse_memref(&ptr, &size, &address); - return size; + // this should only be called from DetectUnsupportedAchievements or indirectly via Process, + // both of which aquire the lock, so we shouldn't try to acquire it here. + // we can also avoid checking m_bInitialized + rc_runtime_invalidate_address(&GetClient()->game->runtime, nAddress); } -static bool IsMemoryOperand(int nOperandType) noexcept +static void HandleAchievementTriggeredEvent(const rc_client_achievement_t& pAchievement) { - switch (nOperandType) + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id); + if (!vmAchievement) { - case RC_OPERAND_ADDRESS: - case RC_OPERAND_DELTA: - case RC_OPERAND_PRIOR: - case RC_OPERAND_BCD: - case RC_OPERAND_INVERTED: - return true; + RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id); + return; + } + + // immediately set the state to Triggered (instead of waiting for AssetListViewModel::DoFrame to do it). + vmAchievement->SetState(ra::data::models::AssetState::Triggered); + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + bool bTakeScreenshot = pConfiguration.IsFeatureEnabled(ra::services::Feature::AchievementTriggeredScreenshot); + bool bSubmit = false; + bool bIsError = false; + + std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); + vmPopup->SetDescription(ra::StringPrintf(L"%s (%u)", pAchievement.title, pAchievement.points)); + vmPopup->SetDetail(ra::Widen(pAchievement.description)); + vmPopup->SetImage(ra::ui::ImageType::Badge, pAchievement.badge_name); + vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered); + + switch (vmAchievement->GetCategory()) + { + case ra::data::models::AssetCategory::Local: + vmPopup->SetTitle(L"Local Achievement Unlocked"); + bSubmit = false; + bTakeScreenshot = false; + break; + + case ra::data::models::AssetCategory::Unofficial: + vmPopup->SetTitle(L"Unofficial Achievement Unlocked"); + bSubmit = false; + break; default: - return false; + vmPopup->SetTitle(L"Achievement Unlocked"); + bSubmit = true; + break; } + + if (vmAchievement->IsModified() || // actual modifications + (bSubmit && vmAchievement->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes + { + auto sHeader = vmPopup->GetTitle(); + sHeader.insert(0, L"Modified "); + if (vmAchievement->GetCategory() != ra::data::models::AssetCategory::Local) + sHeader.append(L" LOCALLY"); + vmPopup->SetTitle(sHeader); + + RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, + vmAchievement->IsModified() ? "modified" : "unpublished"); + bIsError = true; + bSubmit = false; + bTakeScreenshot = false; + } + + if (bSubmit && pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest) + { + auto sHeader = vmPopup->GetTitle(); + sHeader.insert(0, L"Test "); + vmPopup->SetTitle(sHeader); + + RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "test compatibility mode"); + bSubmit = false; + } + + const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + if (bSubmit && pEmulatorContext.WasMemoryModified()) + { + vmPopup->SetTitle(L"Achievement Unlocked LOCALLY"); + vmPopup->SetErrorDetail(L"Error: RAM tampered with"); + + RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "RAM tampered with"); + bIsError = true; + bSubmit = false; + } + + if (bSubmit && _RA_HardcoreModeIsActive() && pEmulatorContext.IsMemoryInsecure()) + { + vmPopup->SetTitle(L"Achievement Unlocked LOCALLY"); + vmPopup->SetErrorDetail(L"Error: RAM insecure"); + + RA_LOG_INFO("Achievement %u not unlocked - %s", pAchievement.id, "RAM insecure"); + bIsError = true; + bSubmit = false; + } + + if (bSubmit && pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline)) + { + vmPopup->SetTitle(L"Offline Achievement Unlocked"); + bSubmit = false; + } + +#ifndef NDEBUG + if (!bSubmit) + Expects(!CanSubmitAchievementUnlock(pAchievement.id, nullptr)); +#endif + + ra::services::ServiceLocator::Get().PlayAudioFile(bIsError ? L"Overlay\\acherror.wav" + : L"Overlay\\unlock.wav"); + + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered) != + ra::ui::viewmodels::PopupLocation::None) + { + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + const auto nPopupId = pOverlayManager.QueueMessage(vmPopup); + + if (bTakeScreenshot) + { + std::wstring sPath = + ra::StringPrintf(L"%s%u.png", pConfiguration.GetScreenshotDirectory(), pAchievement.id); + pOverlayManager.CaptureScreenshot(nPopupId, sPath); + } + + s_mAchievementPopups[pAchievement.id] = nPopupId; + } + + if (vmAchievement->IsPauseOnTrigger()) + { + auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable(); + pFrameEventQueue.QueuePauseOnTrigger(vmAchievement->GetName()); + } +} + +static void HandleChallengeIndicatorShowEvent(const rc_client_achievement_t& pAchievement) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + const auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id); + if (!vmAchievement) + { + RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id); + return; + } + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Challenge) != + ra::ui::viewmodels::PopupLocation::None) + { + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.AddChallengeIndicator(vmAchievement->GetID(), ra::ui::ImageType::Badge, + ra::Narrow(vmAchievement->GetBadge())); + } +} + +static void HandleChallengeIndicatorHideEvent(const rc_client_achievement_t& pAchievement) +{ + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.RemoveChallengeIndicator(pAchievement.id); +} + +static void HandleProgressIndicatorUpdateEvent(const rc_client_achievement_t& pAchievement) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + const auto* vmAchievement = pGameContext.Assets().FindAchievement(pAchievement.id); + if (!vmAchievement) + { + RA_LOG_ERR("Received achievement triggered event for unknown achievement %u", pAchievement.id); + return; + } + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Progress) != ra::ui::viewmodels::PopupLocation::None) + { + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + + // use locked badge if available + auto sBadgeName = ra::Narrow(vmAchievement->GetBadge()); + if (!ra::StringStartsWith(sBadgeName, "local\\")) + sBadgeName += "_lock"; + + pOverlayManager.UpdateProgressTracker(ra::ui::ImageType::Badge, sBadgeName, + ra::Widen(pAchievement.measured_progress)); + } +} + +static void HandleProgressIndicatorShowEvent(const rc_client_achievement_t& pAchievement) +{ + HandleProgressIndicatorUpdateEvent(pAchievement); +} + +static void HandleProgressIndicatorHideEvent() +{ + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.UpdateProgressTracker(ra::ui::ImageType::None, "", L""); +} + +static void HandleGameCompletedEvent(const rc_client_t& pClient) +{ + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) == ra::ui::viewmodels::PopupLocation::None) + return; + + const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore); + const auto* pGame = rc_client_get_game_info(&pClient); + Expects(pGame != nullptr); + + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(&pClient, &summary); + + const auto nPlayTimeSeconds = + ra::services::ServiceLocator::Get().GetTotalPlaytime(pGame->id); + const auto nPlayTimeMinutes = std::chrono::duration_cast(nPlayTimeSeconds).count(); + + std::unique_ptr vmMessage(new ra::ui::viewmodels::PopupMessageViewModel); + vmMessage->SetTitle(ra::StringPrintf(L"%s %s", bHardcore ? L"Mastered" : L"Completed", pGame->title)); + vmMessage->SetDescription(ra::StringPrintf(L"%u achievements, %u points", summary.num_core_achievements, summary.points_core)); + vmMessage->SetDetail(ra::StringPrintf(L"%s | Play time: %dh%02dm", rc_client_get_user_info(&pClient)->display_name, + nPlayTimeMinutes / 60, nPlayTimeMinutes % 60)); + vmMessage->SetImage(ra::ui::ImageType::Icon, pGame->badge_name); + vmMessage->SetPopupType(ra::ui::viewmodels::Popup::Mastery); + + ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\unlock.wav"); + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + const auto nPopupId = pOverlayManager.QueueMessage(vmMessage); + + if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot)) + { + std::wstring sPath = ra::StringPrintf(L"%sGame%u.png", pConfiguration.GetScreenshotDirectory(), pGame->id); + pOverlayManager.CaptureScreenshot(nPopupId, sPath); + } +} + +static void HandleLeaderboardStartedEvent(const rc_client_leaderboard_t& pLeaderboard) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id); + if (!vmLeaderboard) + { + RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id); + return; + } + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted) != + ra::ui::viewmodels::PopupLocation::None) + { + ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\lb.wav"); + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardStarted, L"Leaderboard attempt started", + ra::Widen(pLeaderboard.title), ra::Widen(pLeaderboard.description)); + } +} + +static void HandleLeaderboardFailedEvent(const rc_client_leaderboard_t& pLeaderboard) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id); + if (!vmLeaderboard) + { + RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id); + return; + } + + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled) != + ra::ui::viewmodels::PopupLocation::None) + { + ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\lbcancel.wav"); + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.QueueMessage(ra::ui::viewmodels::Popup::LeaderboardCanceled, L"Leaderboard attempt failed", + ra::Widen(pLeaderboard.title), ra::Widen(pLeaderboard.description)); + } +} + +static void ShowSimplifiedScoreboard(const rc_client_leaderboard_t& pLeaderboard) +{ + auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) == + ra::ui::viewmodels::PopupLocation::None) + { + return; + } + + ra::ui::viewmodels::ScoreboardViewModel vmScoreboard; + vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard.title)); + + const auto& pUserName = ra::services::ServiceLocator::Get().GetDisplayName(); + auto& pEntryViewModel = vmScoreboard.Entries().Add(); + pEntryViewModel.SetRank(0); + pEntryViewModel.SetScore(ra::Widen(pLeaderboard.tracker_value)); + pEntryViewModel.SetUserName(ra::Widen(pUserName)); + pEntryViewModel.SetHighlighted(true); + + ra::services::ServiceLocator::GetMutable().QueueScoreboard( + pLeaderboard.id, std::move(vmScoreboard)); +} + +static void HandleLeaderboardSubmittedEvent(const rc_client_leaderboard_t& pLeaderboard) +{ + auto& pGameContext = ra::services::ServiceLocator::GetMutable(); + const auto* vmLeaderboard = pGameContext.Assets().FindLeaderboard(pLeaderboard.id); + if (!vmLeaderboard) + { + RA_LOG_ERR("Received leaderboard started event for unknown leaderboard %u", pLeaderboard.id); + return; + } + + std::unique_ptr vmPopup(new ra::ui::viewmodels::PopupMessageViewModel); + vmPopup->SetDescription(ra::Widen(pLeaderboard.title)); + std::wstring sTitle = L"Leaderboard Submitted"; + bool bSubmit = true; + + switch (vmLeaderboard->GetCategory()) + { + case ra::data::models::AssetCategory::Local: + sTitle.insert(0, L"Local "); + vmPopup->SetDetail(L"Local leaderboards are not submitted."); + bSubmit = false; + break; + + case ra::data::models::AssetCategory::Unofficial: + sTitle.insert(0, L"Unofficial "); + vmPopup->SetDetail(L"Unofficial leaderboards are not submitted."); + bSubmit = false; + break; + + default: + break; + } + + if (vmLeaderboard->IsModified() || // actual modifications + (bSubmit && vmLeaderboard->GetChanges() != ra::data::models::AssetChanges::None)) // unpublished changes + { + sTitle.insert(0, L"Modified "); + vmPopup->SetDetail(L"Modified leaderboards are not submitted."); + bSubmit = false; + } + + if (bSubmit && pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest) + { + vmPopup->SetDetail(L"Leaderboards are not submitted in compatibility test mode."); + bSubmit = false; + } + + const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + if (bSubmit && pEmulatorContext.WasMemoryModified()) + { + vmPopup->SetErrorDetail(L"Error: RAM tampered with"); + bSubmit = false; + } + + if (bSubmit) + { + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (!pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)) + { + vmPopup->SetErrorDetail(L"Submission requires Hardcore mode"); + bSubmit = false; + } + else if (pEmulatorContext.IsMemoryInsecure()) + { + vmPopup->SetErrorDetail(L"Error: RAM insecure"); + bSubmit = false; + } + else if (pConfiguration.IsFeatureEnabled(ra::services::Feature::Offline)) + { + vmPopup->SetDetail(L"Leaderboards are not submitted in offline mode."); + bSubmit = false; + } + } + + if (!bSubmit) + { + vmPopup->SetTitle(sTitle); + ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\info.wav"); + ra::services::ServiceLocator::GetMutable().QueueMessage(vmPopup); + +#ifndef NDEBUG + Expects(!CanSubmitLeaderboardEntry(pLeaderboard.id, nullptr)); +#endif + + ShowSimplifiedScoreboard(pLeaderboard); + return; + } + + // no popup shown here, the scoreboard will be shown instead +} + +static void HandleLeaderboardTrackerUpdateEvent(const rc_client_leaderboard_tracker_t& pTracker) +{ + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + auto* pScoreTracker = pOverlayManager.GetScoreTracker(pTracker.id); + if (pScoreTracker) + pScoreTracker->SetDisplayText(ra::Widen(pTracker.display)); +} + +static void HandleLeaderboardTrackerShowEvent(const rc_client_leaderboard_tracker_t& pTracker) +{ + const auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker) != + ra::ui::viewmodels::PopupLocation::None) + { + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + auto& pScoreTracker = pOverlayManager.AddScoreTracker(pTracker.id); + pScoreTracker.SetDisplayText(ra::Widen(pTracker.display)); + } +} + +static void HandleLeaderboardTrackerHideEvent(const rc_client_leaderboard_tracker_t& pTracker) +{ + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + pOverlayManager.RemoveScoreTracker(pTracker.id); +} + +static void HandleLeaderboardScoreboardEvent(const rc_client_leaderboard_scoreboard_t& pScoreboard, + const rc_client_leaderboard_t& pLeaderboard) +{ + auto& pConfiguration = ra::services::ServiceLocator::Get(); + if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard) == + ra::ui::viewmodels::PopupLocation::None) + { + return; + } + + ra::ui::viewmodels::ScoreboardViewModel vmScoreboard; + vmScoreboard.SetHeaderText(ra::Widen(pLeaderboard.title)); + + const auto& pUserName = ra::services::ServiceLocator::Get().GetDisplayName(); + constexpr uint32_t nEntriesDisplayed = 7; // display is currently hard-coded to show 7 entries + bool bSeenPlayer = false; + + const auto* pEntry = pScoreboard.top_entries; + const auto* pStop = pEntry + std::min(pScoreboard.num_top_entries, nEntriesDisplayed); + for (; pEntry < pStop; ++pEntry) + { + auto& pEntryViewModel = vmScoreboard.Entries().Add(); + pEntryViewModel.SetRank(pEntry->rank); + pEntryViewModel.SetScore(ra::Widen(pEntry->score)); + pEntryViewModel.SetUserName(ra::Widen(pEntry->username)); + + if (pEntry->username == pUserName) + { + bSeenPlayer = true; + pEntryViewModel.SetHighlighted(true); + + if (strcmp(pScoreboard.best_score, pScoreboard.submitted_score) != 0) + pEntryViewModel.SetScore(ra::StringPrintf(L"(%s) %s", pScoreboard.submitted_score, pScoreboard.best_score)); + } + } + + if (!bSeenPlayer) + { + auto* pEntryViewModel = vmScoreboard.Entries().GetItemAt(6); + if (pEntryViewModel != nullptr) + { + pEntryViewModel->SetRank(pScoreboard.new_rank); + + if (strcmp(pScoreboard.best_score, pScoreboard.submitted_score) != 0) + pEntryViewModel->SetScore( + ra::StringPrintf(L"(%s) %s", pScoreboard.submitted_score, pScoreboard.best_score)); + else + pEntryViewModel->SetScore(ra::Widen(pScoreboard.best_score)); + + pEntryViewModel->SetUserName(ra::Widen(pUserName)); + pEntryViewModel->SetHighlighted(true); + } + } + + ra::services::ServiceLocator::GetMutable().QueueScoreboard( + pLeaderboard.id, std::move(vmScoreboard)); +} + +static void HandleServerError(const rc_client_server_error_t& pServerError) +{ + if (strcmp(pServerError.api, "award_achievement")) + { + const auto nAchievementId = pServerError.related_id; + const auto nPopupId = s_mAchievementPopups[nAchievementId]; + + const auto sErrorMessage = + pServerError.error_message ? ra::Widen(pServerError.error_message) : L"Error submitting unlock"; + auto& pOverlayManager = ra::services::ServiceLocator::GetMutable(); + auto pPopup = pOverlayManager.GetMessage(nPopupId); + if (pPopup != nullptr) + { + pPopup->SetTitle(L"Achievement Unlock FAILED"); + pPopup->SetErrorDetail(sErrorMessage); + pPopup->RebuildRenderImage(); + } + else + { + std::unique_ptr vmPopup( + new ra::ui::viewmodels::PopupMessageViewModel); + vmPopup->SetTitle(L"Achievement Unlock FAILED"); + vmPopup->SetErrorDetail(sErrorMessage); + vmPopup->SetPopupType(ra::ui::viewmodels::Popup::AchievementTriggered); + + const auto& pGameContext = ra::services::ServiceLocator::Get(); + const auto* pAchievement = pGameContext.Assets().FindAchievement(nAchievementId); + if (pAchievement != nullptr) + { + vmPopup->SetDescription( + ra::StringPrintf(L"%s (%u)", pAchievement->GetName(), pAchievement->GetPoints())); + vmPopup->SetImage(ra::ui::ImageType::Badge, ra::Narrow(pAchievement->GetBadge())); + } + else + { + vmPopup->SetDescription(ra::StringPrintf(L"Achievement %u", nAchievementId)); + } + + ra::services::ServiceLocator::Get().PlayAudioFile(L"Overlay\\acherror.wav"); + pOverlayManager.QueueMessage(vmPopup); + } + + return; + } + + if (strcmp(pServerError.api, "submit_lboard_entry")) + {} + + ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage( + ra::StringPrintf(L"%s:%u %s", pServerError.api, pServerError.related_id, pServerError.error_message)); +} + +static void HandleResetEvent() +{ + auto& pEmulatorContext = ra::services::ServiceLocator::Get(); + pEmulatorContext.Reset(); +} + +GSL_SUPPRESS_CON3 +void AchievementRuntime::EventHandler(const rc_client_event_t* pEvent, rc_client_t* pClient) +{ + Expects(pClient != nullptr); + Expects(pEvent != nullptr); + switch (pEvent->type) + { + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + HandleLeaderboardTrackerUpdateEvent(*pEvent->leaderboard_tracker); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + HandleLeaderboardTrackerShowEvent(*pEvent->leaderboard_tracker); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + HandleLeaderboardTrackerHideEvent(*pEvent->leaderboard_tracker); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + HandleChallengeIndicatorShowEvent(*pEvent->achievement); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + HandleChallengeIndicatorHideEvent(*pEvent->achievement); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + HandleProgressIndicatorShowEvent(*pEvent->achievement); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + HandleProgressIndicatorHideEvent(); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + HandleProgressIndicatorUpdateEvent(*pEvent->achievement); + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + HandleAchievementTriggeredEvent(*pEvent->achievement); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + HandleLeaderboardStartedEvent(*pEvent->leaderboard); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + HandleLeaderboardFailedEvent(*pEvent->leaderboard); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + HandleLeaderboardSubmittedEvent(*pEvent->leaderboard); + break; + + case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: + HandleLeaderboardScoreboardEvent(*pEvent->leaderboard_scoreboard, *pEvent->leaderboard); + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + HandleGameCompletedEvent(*pClient); + break; + + case RC_CLIENT_EVENT_RESET: + HandleResetEvent(); + break; + + case RC_CLIENT_EVENT_SERVER_ERROR: + HandleServerError(*pEvent->server_error); + break; + + default: + RA_LOG_WARN("Unhandled rc_client_event %u", pEvent->type); + break; + } +} + +/* ---- Runtime State ----- */ + +void AchievementRuntime::ResetRuntime() noexcept +{ + rc_client_reset(GetClient()); } static void ProcessStateString(Tokenizer& pTokenizer, unsigned int nId, rc_trigger_t* pTrigger, @@ -528,13 +1791,13 @@ static void ProcessStateString(Tokenizer& pTokenizer, unsigned int nId, rc_trigg { const auto& condSource = vConditions.at(nCondition++); pCondition->current_hits = condSource.nHits; - if (IsMemoryOperand(pCondition->operand1.type)) + if (rc_operand_is_memref(&pCondition->operand1)) { pCondition->operand1.value.memref->value.value = condSource.nSourceVal; pCondition->operand1.value.memref->value.prior = condSource.nSourcePrev; pCondition->operand1.value.memref->value.changed = 0; } - if (IsMemoryOperand(pCondition->operand2.type)) + if (rc_operand_is_memref(&pCondition->operand2)) { pCondition->operand2.value.memref->value.value = condSource.nTargetVal; pCondition->operand2.value.memref->value.prior = condSource.nTargetPrev; @@ -554,7 +1817,7 @@ static void ProcessStateString(Tokenizer& pTokenizer, unsigned int nId, rc_trigg } } -bool AchievementRuntime::LoadProgressV1(const std::string& sProgress, std::set& vProcessedAchievementIds) +static bool LoadProgressV1(rc_client_t* pClient, const std::string& sProgress) { const auto& pGameContext = ra::services::ServiceLocator::Get(); const auto& pUserContext = ra::services::ServiceLocator::Get(); @@ -564,10 +1827,9 @@ bool AchievementRuntime::LoadProgressV1(const std::string& sProgress, std::setGetTrigger() : ""; - ProcessStateString(pTokenizer, nId, pTrigger, pUserContext.GetUsername(), sMemString); - pTrigger->state = RC_TRIGGER_STATE_ACTIVE; + const auto* vmAchievement = pGameContext.Assets().FindAchievement(nId); + std::string sMemString = vmAchievement ? vmAchievement->GetTrigger() : ""; + ProcessStateString(pTokenizer, nId, pAchievement->trigger, pUserContext.GetUsername(), sMemString); + pAchievement->trigger->state = RC_TRIGGER_STATE_ACTIVE; + pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; } } return true; } -bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::set& vProcessedAchievementIds) +_NODISCARD static char ComparisonSizeFromPrefix(_In_ char cPrefix) noexcept { + char buffer[] = "0xH0"; + const char* ptr = buffer; + buffer[2] = cPrefix; + + char size = RC_MEMSIZE_16_BITS; + unsigned address = 0; + rc_parse_memref(&ptr, &size, &address); + return size; +} + +static bool LoadProgressV2(rc_client_t* pClient, ra::services::TextReader& pFile) +{ + auto& pRuntime = pClient->game->runtime; + std::string sLine; while (pFile.GetLine(sLine)) { @@ -618,9 +1895,15 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se switch (cField) { - case 'v': pMemRef.value.value = nValue; break; - case 'd': delta = nValue; break; - case 'p': pMemRef.value.prior = nValue; break; + case 'v': + pMemRef.value.value = nValue; + break; + case 'd': + delta = nValue; + break; + case 'p': + pMemRef.value.prior = nValue; + break; } } pMemRef.value.changed = (pMemRef.value.value != delta); @@ -635,7 +1918,7 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se if (tokenizer.PeekChar() == sLineMD5.at(31)) { // match! attempt to store it - rc_memref_t* pMemoryReference = m_pRuntime.memrefs; + rc_memref_t* pMemoryReference = pRuntime.memrefs; while (pMemoryReference) { if (pMemoryReference->address == pMemRef.address && @@ -659,11 +1942,11 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se tokenizer.Advance(); rc_runtime_trigger_t* pRuntimeTrigger = nullptr; - for (size_t i = 0; i < m_pRuntime.trigger_count; ++i) + for (size_t i = 0; i < pRuntime.trigger_count; ++i) { - if (m_pRuntime.triggers[i].id == nId && m_pRuntime.triggers[i].trigger) + if (pRuntime.triggers[i].id == nId && pRuntime.triggers[i].trigger) { - pRuntimeTrigger = &m_pRuntime.triggers[i]; + pRuntimeTrigger = &pRuntime.triggers[i]; break; } } @@ -733,7 +2016,6 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se if (bValid) { pTrigger->state = RC_TRIGGER_STATE_ACTIVE; - vProcessedAchievementIds.insert(nId); } else { @@ -747,120 +2029,57 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se bool AchievementRuntime::LoadProgressFromFile(const char* sLoadStateFilename) { - if (!m_bInitialized) - return true; - - std::lock_guard pLock(m_pMutex); - - // clear out any captured hits for active achievements so they get re-evaluated from the runtime - auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pGameContext.Assets().Count()); ++nIndex) + if (sLoadStateFilename == nullptr) { - auto* pAsset = pGameContext.Assets().GetItemAt(nIndex); - Expects(pAsset != nullptr); - switch (pAsset->GetType()) - { - case ra::data::models::AssetType::Achievement: - { - auto* pAchievement = dynamic_cast(pAsset); - if (pAchievement != nullptr && pAchievement->IsActive()) - pAchievement->ResetCapturedHits(); - break; - } - - case ra::data::models::AssetType::Leaderboard: - { - auto* pLeaderboard = dynamic_cast(pAsset); - if (pLeaderboard != nullptr && pLeaderboard->IsActive()) - pLeaderboard->ResetCapturedHits(); - break; - } - - default: - break; - } + rc_client_deserialize_progress(GetClient(), nullptr); + return false; } - // reset the runtime state, then apply state from file - ResetActiveAchievements(); - - if (sLoadStateFilename == nullptr) - return false; + std::string sContents; std::wstring sAchievementStateFile = ra::Widen(sLoadStateFilename) + L".rap"; const auto& pFileSystem = ra::services::ServiceLocator::Get(); auto pFile = pFileSystem.OpenTextFile(sAchievementStateFile); - if (pFile == nullptr) + if (pFile == nullptr || !pFile->GetLine(sContents)) + { + rc_client_deserialize_progress(GetClient(), nullptr); return false; - - std::string sContents; - if (!pFile->GetLine(sContents)) - return false; - - std::set vProcessedAchievementIds; + } if (sContents == "RAP") { const auto nSize = pFile->GetSize(); std::vector pBuffer; pBuffer.resize(nSize); - pFile->SetPosition({ 0 }); + pFile->SetPosition({0}); pFile->GetBytes(&pBuffer.front(), nSize); - if (rc_runtime_deserialize_progress(&m_pRuntime, &pBuffer.front(), nullptr) == RC_OK) + if (rc_client_deserialize_progress(GetClient(), &pBuffer.front()) == RC_OK) { RA_LOG_INFO("Runtime state loaded from %s", sLoadStateFilename); } } else if (sContents == "v2") { - const auto nVersion = std::stoi(sContents.substr(1)); - switch (nVersion) + if (LoadProgressV2(GetClient(), *pFile)) { - case 2: - if (LoadProgressV2(*pFile, vProcessedAchievementIds)) - { - RA_LOG_INFO("Runtime state (v2) loaded from %s", sLoadStateFilename); - } - else - { - vProcessedAchievementIds.clear(); - } - break; - - default: - assert(!"Unknown persistence file version"); - return false; + RA_LOG_INFO("Runtime state (v2) loaded from %s", sLoadStateFilename); } } else { - if (LoadProgressV1(sContents, vProcessedAchievementIds)) + if (LoadProgressV1(GetClient(), sContents)) { RA_LOG_INFO("Runtime state (v1) loaded from %s", sLoadStateFilename); } - else - { - vProcessedAchievementIds.clear(); - } } return true; } -bool AchievementRuntime::LoadProgressFromBuffer(const char* pBuffer) +bool AchievementRuntime::LoadProgressFromBuffer(const uint8_t* pBuffer) { - if (!m_bInitialized) - return true; - - std::lock_guard pLock(m_pMutex); - - // reset the runtime state, then apply state from file - ResetActiveAchievements(); - - const unsigned char* pBytes; - GSL_SUPPRESS_TYPE1 pBytes = reinterpret_cast(pBuffer); - if (rc_runtime_deserialize_progress(&m_pRuntime, pBytes, nullptr) == RC_OK) + if (rc_client_deserialize_progress(GetClient(), pBuffer) == RC_OK) { RA_LOG_INFO("Runtime state loaded from buffer"); } @@ -873,9 +2092,6 @@ void AchievementRuntime::SaveProgressToFile(const char* sSaveStateFilename) cons if (sSaveStateFilename == nullptr) return; - if (!m_bInitialized) - return; - std::wstring sAchievementStateFile = ra::Widen(sSaveStateFilename) + L".rap"; auto pFile = ra::services::ServiceLocator::Get().CreateTextFile(sAchievementStateFile); if (pFile == nullptr) @@ -884,28 +2100,22 @@ void AchievementRuntime::SaveProgressToFile(const char* sSaveStateFilename) cons return; } - std::lock_guard pLock(m_pMutex); - - const auto nSize = rc_runtime_progress_size(&m_pRuntime, nullptr); + const auto nSize = rc_client_progress_size(GetClient()); std::string sSerialized; sSerialized.resize(nSize); - rc_runtime_serialize_progress(sSerialized.data(), &m_pRuntime, nullptr); + GSL_SUPPRESS_TYPE1 const auto pData = reinterpret_cast(sSerialized.data()); + rc_client_serialize_progress(GetClient(), pData); pFile->Write(sSerialized); RA_LOG_INFO("Runtime state written to %s", sSaveStateFilename); } -int AchievementRuntime::SaveProgressToBuffer(char* pBuffer, int nBufferSize) const +int AchievementRuntime::SaveProgressToBuffer(uint8_t* pBuffer, int nBufferSize) const { - if (!m_bInitialized) - return 0; - - std::lock_guard pLock(m_pMutex); - - const auto nSize = rc_runtime_progress_size(&m_pRuntime, nullptr); + const int nSize = gsl::narrow_cast(rc_client_progress_size(GetClient())); if (nSize <= nBufferSize) { - rc_runtime_serialize_progress(pBuffer, &m_pRuntime, nullptr); + rc_client_serialize_progress(GetClient(), pBuffer); RA_LOG_INFO("Runtime state written to buffer (%d/%d bytes)", nSize, nBufferSize); } else if (nBufferSize > 0) // 0 size buffer indicates caller is asking for size, don't log - we'll capture the actual save soon. @@ -916,92 +2126,47 @@ int AchievementRuntime::SaveProgressToBuffer(char* pBuffer, int nBufferSize) con return nSize; } -void AchievementRuntime::OnTotalMemorySizeChanged() -{ - auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); - pEmulatorContext.RemoveNotifyTarget(*this); +/* ---- Exports ----- */ - // schedule the callback for 100ms from now in case more changes are coming. - // we've unsubscribed from the event, so the callback will only be called once. - auto& pThreadPool = ra::services::ServiceLocator::GetMutable(); - pThreadPool.ScheduleAsync(std::chrono::milliseconds(100), [this]() +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +class AchievementRuntimeExports : private AchievementRuntime +{ +public: + 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) { - // re-validate the assets in case they have invalid memory references - auto& pGameAssets = ra::services::ServiceLocator::GetMutable().Assets(); - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(pGameAssets.Count()); ++nIndex) - { - auto* pAsset = pGameAssets.GetItemAt(nIndex); - if (pAsset != nullptr) - pAsset->Validate(); - } + auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - DetectUnsupportedAchievements(); - }); -} - -void AchievementRuntime::InvalidateAddress(ra::ByteAddress nAddress) noexcept -{ - // this should only be called from DetectUnsupportedAchievements or indirectly via Process, - // both of which aquire the lock, so we shouldn't try to acquire it here. - // we can also avoid checking m_bInitialized - rc_runtime_invalidate_address(&m_pRuntime, nAddress); -} - -#pragma warning(push) -#pragma warning(disable : 5045) - -size_t AchievementRuntime::DetectUnsupportedAchievements() -{ - // if memory hasn't been registered yet, wait for it - auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable(); - if (pEmulatorContext.TotalMemorySize() == 0) - { - // no memory registered yet, wait - pEmulatorContext.AddNotifyTarget(*this); - return 0; + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginLoginWithPassword(username, password, pCallbackData); } - std::lock_guard pLock(m_pMutex); - - const rc_memref_t* memref = m_pRuntime.memrefs; - while (memref) + 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) { - // ignore indirect memory references as their value is calculated from other memory and can easily point at - // invalid addresses. - if (!memref->value.is_indirect) - { - // attempt to read one byte from the memory address (assume multi-byte reads won't span regions). if the - // read fails, EmulatorContext will call InvalidateAddress, which will flag all associated achievements. - if (!pEmulatorContext.IsValidAddress(memref->address)) - InvalidateAddress(memref->address); - } + auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - memref = memref->next; + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return pClient.BeginLoginWithToken(username, token, pCallbackData); } - size_t nDisabled = 0; - const auto* pTrigger = m_pRuntime.triggers; - const auto* pStop = pTrigger + m_pRuntime.trigger_count; - while (pTrigger < pStop) + static const rc_client_user_t* get_user_info() { - if (pTrigger->invalid_memref) - { - // before rc_runtime_do_frame called - ++nDisabled; - } - else if (pTrigger->trigger && pTrigger->trigger->state == RC_TRIGGER_STATE_DISABLED) - { - // after rc_runtime_do_frame called - ++nDisabled; - } - - ++pTrigger; + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_get_user_info(pClient.GetClient()); } - return nDisabled; -} + static void logout() + { + auto& pClient = ra::services::ServiceLocator::GetMutable(); + return rc_client_logout(pClient.GetClient()); + } +}; -#pragma warning(pop) +#endif RC_CLIENT_SUPPORTS_EXTERNAL } // namespace services } // namespace ra @@ -1021,3 +2186,37 @@ extern "C" unsigned int rc_peek_callback(unsigned int nAddress, unsigned int nBy return 0U; } } + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +#include "Exports.hh" +#include "rcheevos/src/rcheevos/rc_client_external.h" + +#ifdef __cplusplus +extern "C" { +#endif + +API int CCONV _Rcheevos_GetExternalClient(rc_client_external_t* pClient, int nVersion) +{ + switch (nVersion) + { + default: + RA_LOG_WARN("Unknown rc_client_external interface version: %s", nVersion); + __fallthrough; + + case 1: + pClient->begin_login_with_password = ra::services::AchievementRuntimeExports::begin_login_with_password; + pClient->begin_login_with_token = ra::services::AchievementRuntimeExports::begin_login_with_token; + pClient->logout = ra::services::AchievementRuntimeExports::logout; + pClient->get_user_info = ra::services::AchievementRuntimeExports::get_user_info; + break; + } + + return 1; +} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/src/services/AchievementRuntime.hh b/src/services/AchievementRuntime.hh index bb8301bc..85b90852 100644 --- a/src/services/AchievementRuntime.hh +++ b/src/services/AchievementRuntime.hh @@ -6,122 +6,75 @@ #include "data\Types.hh" #include "data\context\EmulatorContext.hh" +#include "data\models\AchievementModel.hh" +#include "data\models\LeaderboardModel.hh" #include "services\TextReader.hh" #include -#include +#include + +struct rc_api_fetch_game_data_response_t; namespace ra { namespace services { -class AchievementRuntime : protected ra::data::context::EmulatorContext::NotifyTarget +class AchievementRuntime { public: - GSL_SUPPRESS_F6 AchievementRuntime() = default; - virtual ~AchievementRuntime() - { - ResetRuntime(); - } + GSL_SUPPRESS_F6 AchievementRuntime(); + virtual ~AchievementRuntime(); AchievementRuntime(const AchievementRuntime&) noexcept = delete; AchievementRuntime& operator=(const AchievementRuntime&) noexcept = delete; AchievementRuntime(AchievementRuntime&&) noexcept = delete; AchievementRuntime& operator=(AchievementRuntime&&) noexcept = delete; + /// + /// Shuts down the runtime. + /// + void Shutdown() noexcept; + + /// + /// Gets the underlying rc_client object for directly calling into rcheevos. + /// + rc_client_t* GetClient() const noexcept { return m_pClient.get(); } + + void BeginLoginWithToken(const std::string& sUsername, const std::string& sApiToken, + rc_client_callback_t fCallback, void* pCallbackData); + void BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword, + rc_client_callback_t fCallback, void* pCallbackData); + + void BeginLoadGame(const std::string& sHash, unsigned id, + rc_client_callback_t fCallback, void* pCallbackData); + + /// + /// Syncs data in Assets to rc_client. + /// + void SyncAssets(); + /// /// Clears all active achievements/leaderboards/rich presence from the runtime. /// void ResetRuntime() noexcept; /// - /// Adds an achievement to the processing queue. - /// - int ActivateAchievement(ra::AchievementID nId, const std::string& sTrigger); - - /// - /// Removes an achievement from the processing queue. - /// - void DeactivateAchievement(ra::AchievementID nId); - - /// - /// Updates any references using the provided ID to a new value + /// Updates any references using the provided ID to a new value. /// void UpdateAchievementId(ra::AchievementID nOldId, ra::AchievementID nNewId); /// - /// Gets the raw trigger for the achievement (if active) + /// Gets the raw trigger for the achievement. /// - rc_trigger_t* GetAchievementTrigger(ra::AchievementID nId) noexcept - { - if (!m_bInitialized) - return nullptr; - - return rc_runtime_get_achievement(&m_pRuntime, nId); - } + rc_trigger_t* GetAchievementTrigger(ra::AchievementID nId) const noexcept; /// - /// Gets the raw trigger for the achievement (if active) + /// Gets the raw definition for the leaderboard. /// - const rc_trigger_t* GetAchievementTrigger(ra::AchievementID nId) const noexcept - { - if (!m_bInitialized) - return nullptr; + rc_lboard_t* GetLeaderboardDefinition(ra::LeaderboardID nId) const noexcept; - return rc_runtime_get_achievement(&m_pRuntime, nId); - } - - /// - /// Gets the raw trigger for the achievement (if ever active) - /// - rc_trigger_t* GetAchievementTrigger(ra::AchievementID nId, const std::string& sTrigger); - - /// - /// Gets the raw trigger for the achievement (if active) - /// - rc_trigger_t* DetachAchievementTrigger(ra::AchievementID nId); - - /// - /// Removes an achievement from the processing queue. - /// - void ReleaseAchievementTrigger(ra::AchievementID nId, _In_ rc_trigger_t* pTrigger); - - /// - /// Adds a leaderboard to the processing queue. - /// - int ActivateLeaderboard(unsigned int nId, const std::string& sDefinition); - - /// - /// Removes a leaderboard from the processing queue. - /// - void DeactivateLeaderboard(unsigned int nId) noexcept - { - if (m_bInitialized) - rc_runtime_deactivate_lboard(&m_pRuntime, nId); - } - - /// - /// Gets the raw definition for the leaderboard (if active) - /// - rc_lboard_t* GetLeaderboardDefinition(ra::LeaderboardID nId) noexcept - { - if (!m_bInitialized) - return nullptr; - - return rc_runtime_get_lboard(&m_pRuntime, nId); - } - - /// - /// Gets the raw definition for the leaderboard (if active) - /// - const rc_lboard_t* GetLeaderboardDefinition(ra::LeaderboardID nId) const noexcept - { - if (!m_bInitialized) - return nullptr; - - return rc_runtime_get_lboard(&m_pRuntime, nId); - } + void ReleaseLeaderboardTracker(ra::LeaderboardID nId) noexcept; /// /// Specifies the rich presence to process each frame. @@ -133,67 +86,19 @@ public: /// /// Gets whether or not the loaded game has a rich presence script. /// - bool HasRichPresence() const noexcept - { - if (!m_bInitialized) - return false; - - /* valid rich presence */ - if (m_pRuntime.richpresence != nullptr && m_pRuntime.richpresence->richpresence != nullptr) - return true; - - /* invalid rich presence */ - if (m_nRichPresenceParseResult != RC_OK) - return true; - - /* no rich presence */ - return false; - } + bool HasRichPresence() const noexcept; /// /// Gets the current rich presence display string. /// std::wstring GetRichPresenceDisplayString() const; - - enum class ChangeType - { - None = 0, - AchievementTriggered, - AchievementReset, - AchievementPaused, - LeaderboardStarted, - LeaderboardCanceled, - LeaderboardUpdated, - LeaderboardTriggered, - AchievementDisabled, - LeaderboardDisabled, - AchievementPrimed, - AchievementActivated, - AchievementUnprimed, - AchievementProgressChanged, - - // runtime only changes - LeaderboardStartReset, - LeaderboardSubmitReset, - LeaderboardCancelReset, - LeaderboardValueReset, - LeaderboardStartTriggered, - LeaderboardSubmitTriggered, - LeaderboardCancelTriggered, - }; - - struct Change - { - ChangeType nType; - unsigned int nId; - int nValue; - }; + void InvalidateAddress(ra::ByteAddress nAddress) noexcept; /// /// Processes all active achievements for the current frame. /// - virtual void Process(_Inout_ std::vector& changes); + void DoFrame(); /// /// Loads HitCount data for active achievements from a save state file. @@ -207,7 +112,7 @@ public: /// /// The buffer to read from. /// true if the achievement HitCounts were modified, false if not. - bool LoadProgressFromBuffer(const char* pBuffer); + bool LoadProgressFromBuffer(const uint8_t* pBuffer); /// /// Writes HitCount data for active achievements to a save state file. @@ -225,7 +130,7 @@ public: /// nBufferSize - in which case the caller should allocate the specified amount /// and call again. /// - int SaveProgressToBuffer(char* pBuffer, int nBufferSize) const; + int SaveProgressToBuffer(uint8_t* pBuffer, int nBufferSize) const; /// /// Gets whether achievement processing is temporarily suspended. @@ -237,32 +142,122 @@ public: /// void SetPaused(bool bValue) noexcept { m_bPaused = bValue; } - /// - /// Resets any active achievements and disables them until their triggers are false. - /// - void ResetActiveAchievements(); + class Synchronizer + { + public: + void Wait() + { +#ifdef RA_UTEST + // unit tests are single-threaded. if m_bWaiting is true, it would wait indefinitely. + if (m_bWaiting) + Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(L"Sycnhronous request was not handled."); +#else + std::unique_lock lock(m_pMutex); + if (m_bWaiting) + m_pCondVar.wait(lock); +#endif + } - void InvalidateAddress(ra::ByteAddress nAddress) noexcept; + void Notify() noexcept + { + m_bWaiting = false; + m_pCondVar.notify_all(); + } - size_t DetectUnsupportedAchievements(); + void CaptureResult(int nResult, const char* sErrorMessage) + { + m_nResult = nResult; -protected: - bool m_bPaused = false; - rc_runtime_t m_pRuntime{}; - mutable std::mutex m_pMutex; + if (sErrorMessage) + m_sErrorMessage = sErrorMessage; + else + m_sErrorMessage.clear(); + } - // EmulatorContext::NotifyTarget - void OnTotalMemorySizeChanged() override; + int GetResult() const noexcept { return m_nResult; } + + const std::string& GetErrorMessage() const noexcept { return m_sErrorMessage; } + + private: + std::mutex m_pMutex; + std::condition_variable m_pCondVar; + std::string m_sErrorMessage; + int m_nResult = 0; + bool m_bWaiting = true; + }; + + void AttachMemory(void* pMemory); + bool DetachMemory(void* pMemory) noexcept; private: - bool LoadProgressV1(const std::string& sProgress, std::set& vProcessedAchievementIds); - bool LoadProgressV2(ra::services::TextReader& pFile, std::set& vProcessedAchievementIds); + bool m_bPaused = false; + std::unique_ptr m_pClient; - void EnsureInitialized() noexcept; + class ClientSynchronizer; + std::unique_ptr m_pClientSynchronizer; int m_nRichPresenceParseResult = RC_OK; int m_nRichPresenceErrorLine = 0; - bool m_bInitialized = false; + + static void LogMessage(const char* sMessage, const rc_client_t* pClient); + static uint32_t ReadMemory(uint32_t nAddress, uint8_t* pBuffer, uint32_t nBytes, rc_client_t* pClient); + static void ServerCallAsync(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, + void* pCallbackData, rc_client_t* pClient); + static void EventHandler(const rc_client_event_t* pEvent, rc_client_t* pClient); + + class CallbackWrapper + { + public: + CallbackWrapper(rc_client_t* client, rc_client_callback_t callback, void* callback_userdata) noexcept : + m_pClient(client), m_fCallback(callback), m_pCallbackUserdata(callback_userdata) + {} + + void DoCallback(int nResult, const char* sErrorMessage) noexcept + { + m_fCallback(nResult, sErrorMessage, m_pClient, m_pCallbackUserdata); + } + + static void Dispatch(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) + { + auto* pWrapper = static_cast(pUserdata); + Expects(pWrapper != nullptr); + + pWrapper->DoCallback(nResult, sErrorMessage); + + delete pWrapper; + } + + private: + rc_client_t* m_pClient; + rc_client_callback_t m_fCallback; + void* m_pCallbackUserdata; + }; + + friend class AchievementRuntimeExports; + + rc_client_async_handle_t* BeginLoginWithPassword(const char* sUsername, const char* sPassword, + CallbackWrapper* pCallbackWrapper) noexcept; + rc_client_async_handle_t* BeginLoginWithToken(const char* sUsername, const char* sApiToken, + CallbackWrapper* pCallbackWrapper) noexcept; + static void LoginCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata); + + class LoadGameCallbackWrapper : public CallbackWrapper + { + public: + LoadGameCallbackWrapper(rc_client_t* client, rc_client_callback_t callback, void* callback_userdata) noexcept : + CallbackWrapper(client, callback, callback_userdata) + {} + + std::map m_mAchievementDefinitions; + std::map m_mLeaderboardDefinitions; + }; + + rc_client_async_handle_t* BeginLoadGame(const char* sHash, unsigned id, CallbackWrapper* pCallbackWrapper) noexcept; + static void LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata); + + static void 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); }; } // namespace services diff --git a/src/services/Initialization.cpp b/src/services/Initialization.cpp index d5f8ca8a..8781833f 100644 --- a/src/services/Initialization.cpp +++ b/src/services/Initialization.cpp @@ -12,7 +12,6 @@ #include "services\FrameEventQueue.hh" #include "services\GameIdentifier.hh" #include "services\PerformanceCounter.hh" -#include "services\RcheevosClient.hh" #include "services\ServiceLocator.hh" #include "services\impl\Clock.hh" #include "services\impl\FileLocalStorage.hh" @@ -138,9 +137,6 @@ void Initialization::RegisterServices(EmulatorID nEmulatorId, const char* sClien ra::services::ServiceLocator::Provide(std::move(pPerformanceCounter)); #endif - auto pRcheevosClient = std::make_unique(); - ra::services::ServiceLocator::Provide(std::move(pRcheevosClient)); - auto pUserContext = std::make_unique(); ra::services::ServiceLocator::Provide(std::move(pUserContext)); diff --git a/src/services/RcheevosClient.cpp b/src/services/RcheevosClient.cpp deleted file mode 100644 index 594a281f..00000000 --- a/src/services/RcheevosClient.cpp +++ /dev/null @@ -1,876 +0,0 @@ -#include "RcheevosClient.hh" - -#include "data/context/EmulatorContext.hh" -#include "data/context/SessionTracker.hh" -#include "data/context/UserContext.hh" - -#include "services/Http.hh" -#include "services/IAudioSystem.hh" -#include "services/ILocalStorage.hh" -#include "services/ServiceLocator.hh" - -#include "ui/viewmodels/MessageBoxViewModel.hh" -#include "ui/viewmodels/OverlayManager.hh" -#include "ui/viewmodels/PopupMessageViewModel.hh" -#include "ui/viewmodels/WindowManager.hh" - -#include "RA_Log.h" -#include "RA_StringUtils.h" - -#include -#include // for parsing cached patchdata response -#include -#include - -#undef RC_CLIENT_SUPPORTS_EXTERNAL - -namespace ra { -namespace services { - -static int CanSubmit() -{ - const auto& pGameContext = ra::services::ServiceLocator::Get(); - if (pGameContext.GetMode() == ra::data::context::GameContext::Mode::CompatibilityTest) - return 0; - - const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - if (pEmulatorContext.WasMemoryModified()) - return 0; - - if (pEmulatorContext.IsMemoryInsecure()) - return 0; - - return 1; -} - -static int CanSubmitAchievementUnlock(uint32_t nAchievementId, rc_client_t*) -{ - if (!CanSubmit()) - return 0; - - const auto& pGameContext = ra::services::ServiceLocator::Get(); - const auto* pAchievement = pGameContext.Assets().FindAchievement(nAchievementId); - if (pAchievement == nullptr || pAchievement->GetChanges() != ra::data::models::AssetChanges::None) - return 0; - - return 1; -} - -static int CanSubmitLeaderboardEntry(uint32_t, rc_client_t*) -{ - return CanSubmit(); -} - -RcheevosClient::RcheevosClient() -{ - m_pClient.reset(rc_client_create(RcheevosClient::ReadMemory, RcheevosClient::ServerCallAsync)); - - m_pClient->callbacks.can_submit_achievement_unlock = CanSubmitAchievementUnlock; - m_pClient->callbacks.can_submit_leaderboard_entry = CanSubmitLeaderboardEntry; - -#ifndef RA_UTEST - rc_client_enable_logging(m_pClient.get(), RC_CLIENT_LOG_LEVEL_VERBOSE, RcheevosClient::LogMessage); -#endif - - rc_client_set_unofficial_enabled(m_pClient.get(), 1); -} - -RcheevosClient::~RcheevosClient() -{ - Shutdown(); -} - -void RcheevosClient::Shutdown() noexcept -{ - if (m_pClient != nullptr) - rc_client_destroy(m_pClient.release()); -} - -void RcheevosClient::LogMessage(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); -} - -uint32_t RcheevosClient::ReadMemory(uint32_t nAddress, uint8_t* pBuffer, uint32_t nBytes, rc_client_t*) -{ - const auto& pEmulatorContext = ra::services::ServiceLocator::Get(); - return pEmulatorContext.ReadMemory(nAddress, pBuffer, nBytes); -} - -static void ConvertHttpResponseToApiServerResponse(rc_api_server_response_t& pResponse, - const ra::services::Http::Response& httpResponse) noexcept -{ - memset(&pResponse, 0, sizeof(pResponse)); - pResponse.http_status_code = ra::etoi(httpResponse.StatusCode()); - pResponse.body = httpResponse.Content().c_str(); - pResponse.body_length = httpResponse.Content().length(); -} - -void RcheevosClient::ServerCallAsync(const rc_api_request_t* pRequest, - rc_client_server_callback_t fCallback, void* pCallbackData, rc_client_t*) -{ - ra::services::Http::Request httpRequest(pRequest->url); - httpRequest.SetPostData(pRequest->post_data); - httpRequest.SetContentType(pRequest->content_type); - - httpRequest.CallAsync([fCallback, pCallbackData](const ra::services::Http::Response& httpResponse) noexcept { - rc_api_server_response_t pResponse; - ConvertHttpResponseToApiServerResponse(pResponse, httpResponse); - - fCallback(&pResponse, pCallbackData); - }); -} - -/* ---- ClientSynchronizer ----- */ - -class RcheevosClient::ClientSynchronizer -{ -public: - ClientSynchronizer() = default; - virtual ~ClientSynchronizer() - { - for (auto* pMemory : m_vAllocatedMemory) - free(pMemory); - } - ClientSynchronizer(const ClientSynchronizer&) noexcept = delete; - ClientSynchronizer& operator=(const ClientSynchronizer&) noexcept = delete; - ClientSynchronizer(ClientSynchronizer&&) noexcept = delete; - ClientSynchronizer& operator=(ClientSynchronizer&&) noexcept = delete; - -private: - rc_client_subset_info_t* m_pPublishedSubset = nullptr; - rc_client_t* m_pClient = nullptr; - rc_memref_t** m_pNextMemref = nullptr; - rc_value_t** m_pNextVariable = nullptr; - - typedef struct rc_client_subset_wrapper_t - { - rc_client_subset_info_t pCoreSubset{}; - rc_client_subset_info_t pLocalSubset{}; - rc_api_buffer_t pBuffer{}; - std::vector vAllocatedMemory; - } SubsetWrapper; - std::unique_ptr m_pSubsetWrapper; - - std::vector m_vAllocatedMemory; - - static bool DetachMemory(std::vector& pAllocatedMemory, void* pMemory) noexcept - { - for (auto pIter = pAllocatedMemory.begin(); pIter != pAllocatedMemory.end(); ++pIter) - { - if (*pIter == pMemory) - { - pAllocatedMemory.erase(pIter); - return true; - } - } - - return false; - } - - bool AllocatedMemrefs() noexcept - { - bool bAllocatedMemref = false; - - /* if at least one memref was allocated within the object, we can't free the buffer when the object is - * deactivated */ - if (*m_pNextMemref != nullptr) - { - bAllocatedMemref = true; - /* advance through the new memrefs so we're ready for the next allocation */ - do - { - m_pNextMemref = &(*m_pNextMemref)->next; - } while (*m_pNextMemref != nullptr); - } - - /* if at least one variable was allocated within the object, we can't free the buffer when the object is - * deactivated */ - if (*m_pNextVariable != nullptr) - { - bAllocatedMemref = true; - /* advance through the new variables so we're ready for the next allocation */ - do - { - m_pNextVariable = &(*m_pNextVariable)->next; - } while (*m_pNextVariable != nullptr); - } - - return bAllocatedMemref; - } - - static rc_client_leaderboard_info_t* FindLeaderboard(rc_client_subset_info_t* pSubset, uint32_t nId) - { - Expects(pSubset != nullptr); - - auto* pLeaderboard = pSubset->leaderboards; - const auto* pLeaderboardStop = pLeaderboard + pSubset->public_.num_leaderboards; - for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard) - { - if (pLeaderboard->public_.id == nId) - return pLeaderboard; - } - - return nullptr; - } - - void SyncLeaderboard(rc_client_leaderboard_info_t* pLeaderboard, SubsetWrapper* pSubsetWrapper, - const ra::data::models::LeaderboardModel* vmLeaderboard) - { - Expects(vmLeaderboard != nullptr); - - const auto sTitle = ra::Narrow(vmLeaderboard->GetName()); - if (!pLeaderboard->public_.title || pLeaderboard->public_.title != sTitle) - pLeaderboard->public_.title = rc_buf_strcpy(&pSubsetWrapper->pBuffer, sTitle.c_str()); - - const auto sDescription = ra::Narrow(vmLeaderboard->GetDescription()); - if (!pLeaderboard->public_.description || pLeaderboard->public_.description != sDescription) - pLeaderboard->public_.description = rc_buf_strcpy(&pSubsetWrapper->pBuffer, sDescription.c_str()); - - pLeaderboard->public_.id = vmLeaderboard->GetID(); - - switch (vmLeaderboard->GetState()) - { - case ra::data::models::AssetState::Disabled: - pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; - break; - case ra::data::models::AssetState::Inactive: - pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; - break; - case ra::data::models::AssetState::Primed: - pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; - break; - default: - pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; - break; - } - - const auto& sMemAddr = vmLeaderboard->GetDefinition(); - uint8_t md5[16]; - rc_runtime_checksum(sMemAddr.c_str(), md5); - if (memcmp(pLeaderboard->md5, md5, sizeof(md5)) != 0) - { - memcpy(pLeaderboard->md5, md5, sizeof(md5)); - - if (m_pSubsetWrapper && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pLeaderboard->lboard)) - free(pLeaderboard->lboard); - - const auto nSize = rc_lboard_size(sMemAddr.c_str()); - if (nSize > 0) - { - pLeaderboard->lboard = static_cast(malloc(nSize)); - if (pLeaderboard->lboard) - { - /* populate the item, using the communal memrefs pool */ - rc_parse_state_t parse; - rc_init_parse_state(&parse, pLeaderboard->lboard, nullptr, 0); - parse.first_memref = &m_pClient->game->runtime.memrefs; - parse.variables = &m_pClient->game->runtime.variables; - rc_parse_lboard_internal(pLeaderboard->lboard, sMemAddr.c_str(), &parse); - rc_destroy_parse_state(&parse); - - if (AllocatedMemrefs()) - { - // if memrefs were allocated, we can't release this memory until the game is unloaded - m_vAllocatedMemory.push_back(pLeaderboard->lboard); - } - else - { - pSubsetWrapper->vAllocatedMemory.push_back(pLeaderboard->lboard); - } - } - } - } - } - - static rc_client_achievement_info_t* FindAchievement(rc_client_subset_info_t* pSubset, uint32_t nId) - { - Expects(pSubset != nullptr); - - auto* pAchievement = pSubset->achievements; - const auto* pAchievementStop = pAchievement + pSubset->public_.num_achievements; - for (; pAchievement < pAchievementStop; ++pAchievement) - { - if (pAchievement->public_.id == nId) - return pAchievement; - } - - return nullptr; - } - - void SyncAchievement(rc_client_achievement_info_t* pAchievement, SubsetWrapper* pSubsetWrapper, - const ra::data::models::AchievementModel* vmAchievement) - { - Expects(pAchievement != nullptr); - Expects(pSubsetWrapper != nullptr); - Expects(vmAchievement != nullptr); - - const auto sTitle = ra::Narrow(vmAchievement->GetName()); - if (!pAchievement->public_.title || pAchievement->public_.title != sTitle) - pAchievement->public_.title = rc_buf_strcpy(&pSubsetWrapper->pBuffer, sTitle.c_str()); - - const auto sDescription = ra::Narrow(vmAchievement->GetDescription()); - if (!pAchievement->public_.description || pAchievement->public_.description != sDescription) - pAchievement->public_.description = rc_buf_strcpy(&pSubsetWrapper->pBuffer, sDescription.c_str()); - - const auto& sBadge = vmAchievement->GetBadge(); - if (ra::StringStartsWith(sBadge, L"local\\")) - { - // cannot fit "local/md5.png" into 8 byte buffer. also, client may not understand. - // encode a value that we can intercept in rc_client_achievement_get_image_url. - snprintf(pAchievement->public_.badge_name, sizeof(pAchievement->public_.badge_name), "L%06x", pAchievement->public_.id); - } - else - { - snprintf(pAchievement->public_.badge_name, sizeof(pAchievement->public_.badge_name), "%s", ra::Narrow(sBadge).c_str()); - } - - pAchievement->public_.id = vmAchievement->GetID(); - pAchievement->public_.points = vmAchievement->GetPoints(); - - switch (vmAchievement->GetCategory()) - { - case ra::data::models::AssetCategory::Core: - case ra::data::models::AssetCategory::Local: - pAchievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; - break; - - default: - pAchievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL; - break; - } - - switch (vmAchievement->GetState()) - { - case ra::data::models::AssetState::Triggered: - pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; - break; - case ra::data::models::AssetState::Disabled: - pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; - pAchievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; - break; - case ra::data::models::AssetState::Inactive: - pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - break; - default: - pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - break; - } - - const auto& sTrigger = vmAchievement->GetTrigger(); - uint8_t md5[16]; - rc_runtime_checksum(sTrigger.c_str(), md5); - if (memcmp(pAchievement->md5, md5, sizeof(md5)) != 0) - { - memcpy(pAchievement->md5, md5, sizeof(md5)); - - if (m_pSubsetWrapper && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pAchievement->trigger)) - free(pAchievement->trigger); - - const auto nSize = rc_trigger_size(sTrigger.c_str()); - if (nSize > 0) - { - pAchievement->trigger = static_cast(malloc(nSize)); - if (pAchievement->trigger) - { - /* populate the item, using the communal memrefs pool */ - rc_parse_state_t parse; - rc_init_parse_state(&parse, pAchievement->trigger, nullptr, 0); - parse.first_memref = &m_pClient->game->runtime.memrefs; - parse.variables = &m_pClient->game->runtime.variables; - const char* sMemaddr = sTrigger.c_str(); - rc_parse_trigger_internal(pAchievement->trigger, &sMemaddr, &parse); - rc_destroy_parse_state(&parse); - - if (AllocatedMemrefs()) - { - // if memrefs were allocated, we can't release this memory until the game is unloaded - m_vAllocatedMemory.push_back(pAchievement->trigger); - } - else - { - pSubsetWrapper->vAllocatedMemory.push_back(pAchievement->trigger); - } - } - } - } - } - - void SyncSubset(rc_client_subset_info_t* pSubset, SubsetWrapper* pSubsetWrapper, - std::vector& vAchievements, - std::vector& vLeaderboards) - { - pSubset->active = true; - - if (!vAchievements.empty()) - { - pSubset->achievements = static_cast(rc_buf_alloc( - &pSubsetWrapper->pBuffer, vAchievements.size() * sizeof(*pSubset->achievements))); - memset(pSubset->achievements, 0, vAchievements.size() * sizeof(*pSubset->achievements)); - - rc_client_achievement_info_t* pAchievement = pSubset->achievements; - rc_client_achievement_info_t* pSrcAchievement = nullptr; - for (const auto* vmAchievement : vAchievements) - { - Expects(vmAchievement != nullptr); - - const auto nAchievementId = vmAchievement->GetID(); - if (vmAchievement->GetChanges() == ra::data::models::AssetChanges::None) - { - rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset; - for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next) - { - pSrcAchievement = FindAchievement(pPublishedSubset, nAchievementId); - if (pSrcAchievement != nullptr) - { - memcpy(pAchievement++, pSrcAchievement, sizeof(*pAchievement)); - break; - } - } - } - else - { - pSrcAchievement = nullptr; - if (m_pSubsetWrapper != nullptr) - { - if (vmAchievement->GetCategory() == ra::data::models::AssetCategory::Local) - pSrcAchievement = FindAchievement(&m_pSubsetWrapper->pLocalSubset, nAchievementId); - else - pSrcAchievement = FindAchievement(&m_pSubsetWrapper->pCoreSubset, nAchievementId); - } - - if (pSrcAchievement != nullptr) - { - // transfer the trigger ownership to the new SubsetWrapper - if (pAchievement->trigger && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pAchievement->trigger)) - pSubsetWrapper->vAllocatedMemory.push_back(pAchievement->trigger); - - memcpy(pAchievement++, pSrcAchievement, sizeof(*pAchievement)); - } - else - { - SyncAchievement(pAchievement++, pSubsetWrapper, vmAchievement); - } - } - } - - pSubset->public_.num_achievements = gsl::narrow_cast(pAchievement - pSubset->achievements); - } - - if (!vLeaderboards.empty()) - { - pSubset->leaderboards = static_cast(rc_buf_alloc( - &pSubsetWrapper->pBuffer, vLeaderboards.size() * sizeof(*pSubset->leaderboards))); - memset(pSubset->leaderboards, 0, vLeaderboards.size() * sizeof(*pSubset->leaderboards)); - - rc_client_leaderboard_info_t* pLeaderboard = pSubset->leaderboards; - rc_client_leaderboard_info_t* pSrcLeaderboard = nullptr; - for (const auto* vmLeaderboard : vLeaderboards) - { - Expects(vmLeaderboard != nullptr); - - const auto nLeaderboardId = vmLeaderboard->GetID(); - if (vmLeaderboard->GetChanges() == ra::data::models::AssetChanges::None) - { - rc_client_subset_info_t* pPublishedSubset = m_pPublishedSubset; - for (; pPublishedSubset; pPublishedSubset = pPublishedSubset->next) - { - pSrcLeaderboard = FindLeaderboard(pPublishedSubset, nLeaderboardId); - if (pSrcLeaderboard != nullptr) - { - memcpy(pLeaderboard++, pSrcLeaderboard, sizeof(*pLeaderboard)); - break; - } - } - } - else - { - pSrcLeaderboard = nullptr; - if (m_pSubsetWrapper != nullptr) - { - if (vmLeaderboard->GetCategory() == ra::data::models::AssetCategory::Local) - pSrcLeaderboard = FindLeaderboard(&m_pSubsetWrapper->pLocalSubset, nLeaderboardId); - else - pSrcLeaderboard = FindLeaderboard(&m_pSubsetWrapper->pCoreSubset, nLeaderboardId); - } - - if (pSrcLeaderboard != nullptr) - { - // transfer the lboard ownership to the new SubsetWrapper - if (pLeaderboard->lboard && DetachMemory(m_pSubsetWrapper->vAllocatedMemory, pLeaderboard->lboard)) - pSubsetWrapper->vAllocatedMemory.push_back(pLeaderboard->lboard); - - memcpy(pLeaderboard++, pSrcLeaderboard, sizeof(*pLeaderboard)); - } - else - { - SyncLeaderboard(pLeaderboard++, pSubsetWrapper, vmLeaderboard); - } - } - } - - pSubset->public_.num_leaderboards = gsl::narrow_cast(pLeaderboard - pSubset->leaderboards); - } - } - -public: - void SyncAssets(rc_client_t* pClient) - { - const auto& pGameContext = ra::services::ServiceLocator::Get(); - const auto& vAssets = pGameContext.Assets(); - - m_pClient = pClient; - m_pNextMemref = &pClient->game->runtime.memrefs; - m_pNextVariable = &pClient->game->runtime.variables; - AllocatedMemrefs(); // advance pointers - - if (m_pPublishedSubset == nullptr) - m_pPublishedSubset = pClient->game->subsets; - - std::vector vCoreAchievements; - std::vector vLocalAchievements; - std::vector vCoreLeaderboards; - std::vector vLocalLeaderboards; - - for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast(vAssets.Count()); ++nIndex) - { - const auto* pAsset = vAssets.GetItemAt(nIndex); - Expects(pAsset != nullptr); - if (pAsset->GetChanges() == ra::data::models::AssetChanges::Deleted) - continue; - - switch (pAsset->GetType()) - { - case ra::data::models::AssetType::Achievement: - if (pAsset->GetCategory() == ra::data::models::AssetCategory::Local) - vLocalAchievements.push_back(dynamic_cast(pAsset)); - else - vCoreAchievements.push_back(dynamic_cast(pAsset)); - break; - - case ra::data::models::AssetType::Leaderboard: - if (pAsset->GetCategory() == ra::data::models::AssetCategory::Local) - vLocalLeaderboards.push_back(dynamic_cast(pAsset)); - else - vCoreLeaderboards.push_back(dynamic_cast(pAsset)); - break; - - default: - break; - } - } - - std::unique_ptr pSubsetWrapper; - pSubsetWrapper.reset(new SubsetWrapper); - memset(&pSubsetWrapper->pCoreSubset, 0, sizeof(pSubsetWrapper->pCoreSubset)); - memset(&pSubsetWrapper->pLocalSubset, 0, sizeof(pSubsetWrapper->pLocalSubset)); - rc_buf_init(&pSubsetWrapper->pBuffer); - - pSubsetWrapper->pCoreSubset.public_.id = pClient->game->public_.id; - pSubsetWrapper->pCoreSubset.public_.title = pClient->game->public_.title; - snprintf(pSubsetWrapper->pCoreSubset.public_.badge_name, sizeof(pSubsetWrapper->pCoreSubset.public_.badge_name), - "%s", pClient->game->public_.badge_name); - - SyncSubset(&pSubsetWrapper->pCoreSubset, pSubsetWrapper.get(), vCoreAchievements, vCoreLeaderboards); - - if (!vLocalAchievements.empty() || !vLocalLeaderboards.empty()) - { - pSubsetWrapper->pLocalSubset.public_.id = ra::data::context::GameAssets::LocalSubsetId; - pSubsetWrapper->pLocalSubset.public_.title = "Local"; - snprintf(pSubsetWrapper->pLocalSubset.public_.badge_name, - sizeof(pSubsetWrapper->pLocalSubset.public_.badge_name), "%s", pClient->game->public_.badge_name); - - SyncSubset(&pSubsetWrapper->pLocalSubset, pSubsetWrapper.get(), vLocalAchievements, vLocalLeaderboards); - - pSubsetWrapper->pCoreSubset.next = &pSubsetWrapper->pLocalSubset; - } - - std::swap(pSubsetWrapper, m_pSubsetWrapper); - - if (pSubsetWrapper != nullptr) - { - for (auto* pMemory : pSubsetWrapper->vAllocatedMemory) - free(pMemory); - } - - rc_mutex_lock(&pClient->state.mutex); - pClient->game->subsets = &m_pSubsetWrapper->pCoreSubset; - rc_mutex_unlock(&pClient->state.mutex); - } -}; - -void RcheevosClient::SyncAssets() -{ - if (m_pClientSynchronizer == nullptr) - m_pClientSynchronizer.reset(new ClientSynchronizer()); - m_pClientSynchronizer->SyncAssets(GetClient()); -} - -/* ---- Login ----- */ - -void RcheevosClient::BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword, - rc_client_callback_t fCallback, void* pCallbackData) -{ - GSL_SUPPRESS_R3 - auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData); - BeginLoginWithPassword(sUsername.c_str(), sPassword.c_str(), pCallbackWrapper); -} - -rc_client_async_handle_t* RcheevosClient::BeginLoginWithPassword(const char* sUsername, const char* sPassword, - CallbackWrapper* pCallbackWrapper) noexcept -{ - return rc_client_begin_login_with_password(GetClient(), sUsername, sPassword, - RcheevosClient::LoginCallback, pCallbackWrapper); -} - -void RcheevosClient::BeginLoginWithToken(const std::string& sUsername, const std::string& sApiToken, - rc_client_callback_t fCallback, void* pCallbackData) -{ - GSL_SUPPRESS_R3 - auto* pCallbackWrapper = new CallbackWrapper(m_pClient.get(), fCallback, pCallbackData); - BeginLoginWithToken(sUsername.c_str(), sApiToken.c_str(), pCallbackWrapper); -} - -rc_client_async_handle_t* RcheevosClient::BeginLoginWithToken(const char* sUsername, const char* sApiToken, - CallbackWrapper* pCallbackWrapper) noexcept -{ - return rc_client_begin_login_with_token(GetClient(), sUsername, sApiToken, - RcheevosClient::LoginCallback, pCallbackWrapper); -} - -GSL_SUPPRESS_CON3 -void RcheevosClient::LoginCallback(int nResult, const char* sErrorMessage, - rc_client_t* pClient, void* pUserdata) -{ - if (nResult == RC_OK) - { - const auto* user = rc_client_get_user_info(pClient); - if (!user) - { - nResult = RC_INVALID_STATE; - sErrorMessage = "User data not available."; - } - else - { - // initialize the user context - auto& pUserContext = ra::services::ServiceLocator::GetMutable(); - if (pUserContext.IsLoginDisabled()) - { - nResult = RC_INVALID_STATE; - sErrorMessage = "Login has been disabled."; - } - else - { - pUserContext.Initialize(user->username, user->display_name, user->token); - pUserContext.SetScore(user->score); - } - } - } - - auto* wrapper = static_cast(pUserdata); - Expects(wrapper != nullptr); - wrapper->DoCallback(nResult, sErrorMessage); - - delete wrapper; -} - -/* ---- Load Game ----- */ - -void RcheevosClient::BeginLoadGame(const std::string& sHash, unsigned id, - rc_client_callback_t fCallback, void* pCallbackData) -{ - GSL_SUPPRESS_R3 - auto* pCallbackWrapper = new LoadGameCallbackWrapper(m_pClient.get(), fCallback, pCallbackData); - BeginLoadGame(sHash.c_str(), id, pCallbackWrapper); -} - -static void ExtractPatchData(const rc_api_server_response_t* server_response, uint32_t nGameId) -{ - // extract the PatchData and store a copy in the cache for offline mode - rc_api_response_t api_response{}; - GSL_SUPPRESS_ES47 - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("PatchData") - }; - - - if (rc_json_parse_server_response(&api_response, server_response, fields, sizeof(fields) / sizeof(fields[0])) == RC_OK && - fields[2].value_start && fields[2].value_end) - { - auto& pLocalStorage = ra::services::ServiceLocator::GetMutable(); - auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::GameData, std::to_wstring(nGameId)); - if (pData != nullptr) - { - std::string sPatchData; - sPatchData.append(fields[2].value_start, fields[2].value_end - fields[2].value_start); - pData->Write(sPatchData); - } - } -} - -GSL_SUPPRESS_CON3 -void RcheevosClient::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) -{ - auto* wrapper = static_cast(pUserdata); - Expects(wrapper != nullptr); - - auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - - auto pRichPresence = std::make_unique(); - pRichPresence->SetScript(game_data_response->rich_presence_script); - pRichPresence->CreateServerCheckpoint(); - 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; -#endif - - const rc_api_achievement_definition_t* pAchievement = game_data_response->achievements; - const rc_api_achievement_definition_t* pAchievementStop = pAchievement + game_data_response->num_achievements; - for (; pAchievement < pAchievementStop; ++pAchievement) - wrapper->m_mAchievementDefinitions[pAchievement->id] = pAchievement->definition; - - const rc_api_leaderboard_definition_t* pLeaderboard = game_data_response->leaderboards; - const rc_api_leaderboard_definition_t* pLeaderboardStop = pLeaderboard + game_data_response->num_leaderboards; - for (; pLeaderboard < pLeaderboardStop; ++pLeaderboard) - wrapper->m_mLeaderboardDefinitions[pLeaderboard->id] = pLeaderboard->definition; - - ExtractPatchData(server_response, game_data_response->id); -} - -rc_client_async_handle_t* RcheevosClient::BeginLoadGame(const char* sHash, unsigned id, - CallbackWrapper* pCallbackWrapper) noexcept -{ - auto* client = GetClient(); - - // unload the game and free any additional memory we allocated for local changes - rc_client_unload_game(client); - m_pClientSynchronizer.reset(); - - // if an ID was provided, store the hash->id mapping - if (id != 0) - { - auto* client_hash = rc_client_find_game_hash(client, sHash); - if (ra::to_signed(client_hash->game_id) < 0) - client_hash->game_id = id; - } - - // start the load process - client->callbacks.post_process_game_data_response = PostProcessGameDataResponse; - - return rc_client_begin_load_game(client, sHash, RcheevosClient::LoadGameCallback, pCallbackWrapper); -} - -GSL_SUPPRESS_CON3 -void RcheevosClient::LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) -{ - auto* wrapper = static_cast(pUserdata); - Expects(wrapper != nullptr); - - if (nResult == RC_OK || nResult == RC_NO_GAME_LOADED) - { - // initialize the game context - auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - pGameContext.InitializeFromRcheevosClient(wrapper->m_mAchievementDefinitions, - wrapper->m_mLeaderboardDefinitions); - } - else - { - - } - - wrapper->DoCallback(nResult, sErrorMessage); - - delete wrapper; -} - -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL - -class RcheevosClientExports : private RcheevosClient -{ -public: - 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) - { - 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) - { - auto* pCallbackData = new CallbackWrapper(client, callback, callback_userdata); - - auto& pClient = ra::services::ServiceLocator::GetMutable(); - return pClient.BeginLoginWithToken(username, token, pCallbackData); - } - - 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()); - } -}; - -#endif RC_CLIENT_SUPPORTS_EXTERNAL - -} // namespace services -} // namespace ra - -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL - -#include "Exports.hh" -#include "rcheevos/src/rcheevos/rc_client_external.h" - -#ifdef __cplusplus -extern "C" { -#endif - -API int CCONV _Rcheevos_GetExternalClient(rc_client_external_t* pClient, int nVersion) -{ - switch (nVersion) - { - default: - RA_LOG_WARN("Unknown rc_client_external interface version: %s", nVersion); - __fallthrough; - - case 1: - pClient->begin_login_with_password = ra::services::RcheevosClientExports::begin_login_with_password; - pClient->begin_login_with_token = ra::services::RcheevosClientExports::begin_login_with_token; - pClient->logout = ra::services::RcheevosClientExports::logout; - pClient->get_user_info = ra::services::RcheevosClientExports::get_user_info; - break; - } - - return 1; -} - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/src/services/RcheevosClient.hh b/src/services/RcheevosClient.hh deleted file mode 100644 index d204f29a..00000000 --- a/src/services/RcheevosClient.hh +++ /dev/null @@ -1,160 +0,0 @@ -#ifndef RA_SERVICES_RCHEEVOS_CLIENT_HH -#define RA_SERVICES_RCHEEVOS_CLIENT_HH -#pragma once - -#include "ra_fwd.h" - -#include "data\Types.hh" -#include "data\context\EmulatorContext.hh" - -#include "services\TextReader.hh" - -#include - -#include - -struct rc_api_fetch_game_data_response_t; - - -namespace ra { -namespace services { - -class RcheevosClient -{ -public: - GSL_SUPPRESS_F6 RcheevosClient(); - virtual ~RcheevosClient(); - - RcheevosClient(const RcheevosClient&) noexcept = delete; - RcheevosClient& operator=(const RcheevosClient&) noexcept = delete; - RcheevosClient(RcheevosClient&&) noexcept = delete; - RcheevosClient& operator=(RcheevosClient&&) noexcept = delete; - - void Shutdown() noexcept; - - rc_client_t* GetClient() const noexcept { return m_pClient.get(); } - - void SyncAssets(); - - void BeginLoginWithToken(const std::string& sUsername, const std::string& sApiToken, - rc_client_callback_t fCallback, void* pCallbackData); - void BeginLoginWithPassword(const std::string& sUsername, const std::string& sPassword, - rc_client_callback_t fCallback, void* pCallbackData); - - void BeginLoadGame(const std::string& sHash, unsigned id, - rc_client_callback_t fCallback, void* pCallbackData); - - class Synchronizer - { - public: - void Wait() - { -#ifdef RA_UTEST - // unit tests are single-threaded. if m_bWaiting is true, it would wait indefinitely. - if (m_bWaiting) - Microsoft::VisualStudio::CppUnitTestFramework::Assert::Fail(L"Sycnhronous request was not handled."); -#else - std::unique_lock lock(m_pMutex); - if (m_bWaiting) - m_pCondVar.wait(lock); -#endif - } - - void Notify() noexcept - { - m_bWaiting = false; - m_pCondVar.notify_all(); - } - - void CaptureResult(int nResult, const char* sErrorMessage) - { - m_nResult = nResult; - - if (sErrorMessage) - m_sErrorMessage = sErrorMessage; - else - m_sErrorMessage.clear(); - } - - int GetResult() const noexcept { return m_nResult; } - - const std::string& GetErrorMessage() const noexcept { return m_sErrorMessage; } - - private: - std::mutex m_pMutex; - std::condition_variable m_pCondVar; - std::string m_sErrorMessage; - int m_nResult = 0; - bool m_bWaiting = true; - }; - -private: - std::unique_ptr m_pClient; - - class ClientSynchronizer; - std::unique_ptr m_pClientSynchronizer; - - static void LogMessage(const char* sMessage, const rc_client_t* pClient); - static uint32_t ReadMemory(uint32_t nAddress, uint8_t* pBuffer, uint32_t nBytes, rc_client_t* pClient); - static void ServerCallAsync(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, - void* pCallbackData, rc_client_t* pClient); - - class CallbackWrapper - { - public: - CallbackWrapper(rc_client_t* client, rc_client_callback_t callback, void* callback_userdata) noexcept : - m_pClient(client), m_fCallback(callback), m_pCallbackUserdata(callback_userdata) - {} - - void DoCallback(int nResult, const char* sErrorMessage) noexcept - { - m_fCallback(nResult, sErrorMessage, m_pClient, m_pCallbackUserdata); - } - - static void Dispatch(int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) - { - auto* pWrapper = static_cast(pUserdata); - Expects(pWrapper != nullptr); - - pWrapper->DoCallback(nResult, sErrorMessage); - - delete pWrapper; - } - - private: - rc_client_t* m_pClient; - rc_client_callback_t m_fCallback; - void* m_pCallbackUserdata; - }; - - friend class RcheevosClientExports; - - rc_client_async_handle_t* BeginLoginWithPassword(const char* sUsername, const char* sPassword, - CallbackWrapper* pCallbackWrapper) noexcept; - rc_client_async_handle_t* BeginLoginWithToken(const char* sUsername, const char* sApiToken, - CallbackWrapper* pCallbackWrapper) noexcept; - static void LoginCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata); - - class LoadGameCallbackWrapper : public CallbackWrapper - { - public: - LoadGameCallbackWrapper(rc_client_t* client, rc_client_callback_t callback, void* callback_userdata) noexcept : - CallbackWrapper(client, callback, callback_userdata) - {} - - std::map m_mAchievementDefinitions; - std::map m_mLeaderboardDefinitions; - }; - - rc_client_async_handle_t* BeginLoadGame(const char* sHash, unsigned id, CallbackWrapper* pCallbackWrapper) noexcept; - static void LoadGameCallback(int nResult, const char* sErrorMessage, rc_client_t* pClient, void* pUserdata); - - static void 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); -}; - -} // namespace services -} // namespace ra - -#endif // !RA_SERVICES_RCHEEVOS_CLIENT_HH diff --git a/src/ui/viewmodels/AssetEditorViewModel.cpp b/src/ui/viewmodels/AssetEditorViewModel.cpp index 7384baf7..43098f52 100644 --- a/src/ui/viewmodels/AssetEditorViewModel.cpp +++ b/src/ui/viewmodels/AssetEditorViewModel.cpp @@ -985,7 +985,7 @@ void AssetEditorViewModel::UpdateMeasuredValue() const auto* pLeaderboard = dynamic_cast(m_pAsset); if (pLeaderboard != nullptr) { - auto* pLeaderboardDefinition = ra::services::ServiceLocator::Get().GetLeaderboardDefinition(pLeaderboard->GetID()); + const auto* pLeaderboardDefinition = ra::services::ServiceLocator::Get().GetLeaderboardDefinition(pLeaderboard->GetID()); if (pLeaderboardDefinition) { const unsigned nValue = pLeaderboardDefinition->value.value.value; diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp index d18e90a5..0e81b3ae 100644 --- a/src/ui/viewmodels/AssetListViewModel.cpp +++ b/src/ui/viewmodels/AssetListViewModel.cpp @@ -1477,6 +1477,9 @@ void AssetListViewModel::ResetSelected() pGameContext.Assets().EndUpdate(); + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + pRuntime.SyncAssets(); + // if any achievements were deleted, disable their challenge indicators for (auto nId : vPrimedAchievementsBefore) { diff --git a/src/ui/viewmodels/IntegrationMenuViewModel.cpp b/src/ui/viewmodels/IntegrationMenuViewModel.cpp index 32a17566..5e215692 100644 --- a/src/ui/viewmodels/IntegrationMenuViewModel.cpp +++ b/src/ui/viewmodels/IntegrationMenuViewModel.cpp @@ -246,16 +246,15 @@ void IntegrationMenuViewModel::ToggleLeaderboards() const bool bLeaderboardsActive = !pConfiguration.IsFeatureEnabled(ra::services::Feature::Leaderboards); pConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, bLeaderboardsActive); - auto& pGameContext = ra::services::ServiceLocator::GetMutable(); - if (!bLeaderboardsActive) { - pGameContext.DeactivateLeaderboards(); + // TODO: disallow disabling leaderboards + //pGameContext.DeactivateLeaderboards(); ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Leaderboards are now disabled."); } else { - pGameContext.ActivateLeaderboards(); + //pGameContext.ActivateLeaderboards(); ra::ui::viewmodels::MessageBoxViewModel::ShowMessage(L"Leaderboards are now enabled."); } @@ -385,7 +384,6 @@ void IntegrationMenuViewModel::ShowGameHash() Expects(!vmUnknownGame.GetTestMode()); pGameContext.SetMode(ra::data::context::GameContext::Mode::Normal); - pGameContext.RefreshUnlocks(); } } else diff --git a/src/ui/viewmodels/LoginViewModel.cpp b/src/ui/viewmodels/LoginViewModel.cpp index 1dd39134..420a23b9 100644 --- a/src/ui/viewmodels/LoginViewModel.cpp +++ b/src/ui/viewmodels/LoginViewModel.cpp @@ -10,8 +10,8 @@ #include "data\context\SessionTracker.hh" #include "data\context\UserContext.hh" +#include "services\AchievementRuntime.hh" #include "services\IConfiguration.hh" -#include "services\RcheevosClient.hh" #include "ui\viewmodels\MessageBoxViewModel.hh" #include "ui\viewmodels\WindowManager.hh" @@ -49,12 +49,12 @@ bool LoginViewModel::Login() const return false; } - ra::services::RcheevosClient::Synchronizer pSynchronizer; + ra::services::AchievementRuntime::Synchronizer pSynchronizer; - auto& pClient = ra::services::ServiceLocator::GetMutable(); - pClient.BeginLoginWithPassword(ra::Narrow(GetUsername()), ra::Narrow(GetPassword()), + auto& pRuntime = ra::services::ServiceLocator::GetMutable(); + pRuntime.BeginLoginWithPassword(ra::Narrow(GetUsername()), ra::Narrow(GetPassword()), [](int nResult, const char* sErrorMessage, rc_client_t*, void* pUserdata) { - auto* pSynchronizer = static_cast(pUserdata); + auto* pSynchronizer = static_cast(pUserdata); Expects(pSynchronizer != nullptr); pSynchronizer->CaptureResult(nResult, sErrorMessage); diff --git a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp index fe0f9654..ee8d53fe 100644 --- a/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp +++ b/src/ui/viewmodels/OverlayAchievementsPageViewModel.cpp @@ -8,8 +8,8 @@ #include "data\context\SessionTracker.hh" #include "data\context\UserContext.hh" +#include "services\AchievementRuntime.hh" #include "services\IClock.hh" -#include "services\RcheevosClient.hh" #include "ui\OverlayTheme.hh" @@ -104,7 +104,7 @@ void OverlayAchievementsPageViewModel::Refresh() SetListTitle(pGameContext.GameTitle()); // achievement list - const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); + const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); std::vector vDeactivatedSubsets; int nCategory = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL; @@ -314,7 +314,7 @@ void OverlayAchievementsPageViewModel::FetchItemDetail(ItemViewModel& vmItem) if (m_vAchievementDetails.find(vmItem.GetId()) != m_vAchievementDetails.end()) // already populated return; - const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); + const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); const auto nAchievementID = vmItem.GetId(); GSL_SUPPRESS_TYPE1 const auto* pAchievement = reinterpret_cast(rc_client_get_achievement_info(pClient, nAchievementID)); diff --git a/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp b/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp index a9e37d05..23f4aed7 100644 --- a/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp +++ b/src/ui/viewmodels/OverlayLeaderboardsPageViewModel.cpp @@ -7,7 +7,7 @@ #include "data\context\GameContext.hh" #include "data\context\UserContext.hh" -#include "services\RcheevosClient.hh" +#include "services\AchievementRuntime.hh" #include "ui\OverlayTheme.hh" @@ -66,7 +66,7 @@ void OverlayLeaderboardsPageViewModel::Refresh() SetListTitle(pGameContext.GameTitle()); // leaderboard list - const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); + const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); std::vector vDeactivatedSubsets; const auto& pAssetList = ra::services::ServiceLocator::Get().AssetList; @@ -203,7 +203,7 @@ void OverlayLeaderboardsPageViewModel::FetchItemDetail(ItemViewModel& vmItem) if (m_vLeaderboardRanks.find(vmItem.GetId()) != m_vLeaderboardRanks.end()) // already populated return; - const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); + const auto& pClient = ra::services::ServiceLocator::Get().GetClient(); const auto nLeaderboardId = vmItem.GetId(); GSL_SUPPRESS_TYPE1 const auto* pLeaderboard = reinterpret_cast(rc_client_get_leaderboard_info(pClient, nLeaderboardId)); diff --git a/src/ui/viewmodels/OverlayManager.cpp b/src/ui/viewmodels/OverlayManager.cpp index 1d275571..ee763983 100644 --- a/src/ui/viewmodels/OverlayManager.cpp +++ b/src/ui/viewmodels/OverlayManager.cpp @@ -239,13 +239,13 @@ void OverlayManager::Render(ra::ui::drawing::ISurface& pSurface, bool bRedrawAll } } -ScoreTrackerViewModel& OverlayManager::AddScoreTracker(ra::LeaderboardID nLeaderboardId) +ScoreTrackerViewModel& OverlayManager::AddScoreTracker(uint32_t nTrackerId) { ScoreTrackerViewModel* vmTracker; { std::lock_guard pGuard(m_pPopupQueueMutex); auto pScoreTracker = std::make_unique(); - pScoreTracker->SetPopupId(nLeaderboardId); + pScoreTracker->SetPopupId(nTrackerId); pScoreTracker->UpdateRenderImage(0.0); vmTracker = m_vScoreTrackers.emplace_back(std::move(pScoreTracker)).get(); } @@ -255,14 +255,14 @@ ScoreTrackerViewModel& OverlayManager::AddScoreTracker(ra::LeaderboardID nLeader return *vmTracker; } -void OverlayManager::RemoveScoreTracker(ra::LeaderboardID nLeaderboardId) +void OverlayManager::RemoveScoreTracker(uint32_t nTrackerId) { bool bRequestRender = false; { std::lock_guard pGuard(m_pPopupQueueMutex); for (auto pIter = m_vScoreTrackers.begin(); pIter != m_vScoreTrackers.end(); ++pIter) { - if (ra::to_unsigned((*pIter)->GetPopupId()) == nLeaderboardId && !(*pIter)->IsDestroyPending()) + if (ra::to_unsigned((*pIter)->GetPopupId()) == nTrackerId && !(*pIter)->IsDestroyPending()) { (*pIter)->SetDestroyPending(); bRequestRender = true; @@ -335,14 +335,24 @@ void OverlayManager::RemoveChallengeIndicator(ra::AchievementID nAchievementId) RequestRender(); } -void OverlayManager::UpdateProgressTracker(ra::ui::ImageType imageType, const std::string& sImageName, unsigned nValue, unsigned nTarget, bool bAsPercent) +void OverlayManager::UpdateProgressTracker(ra::ui::ImageType imageType, const std::string& sImageName, const std::wstring& sProgress) { - if (m_vmProgressTracker == nullptr) - m_vmProgressTracker.reset(new ProgressTrackerViewModel()); + if (sProgress.empty()) + { + if (m_vmProgressTracker == nullptr) + return; - m_vmProgressTracker->SetImage(imageType, sImageName); - m_vmProgressTracker->SetProgress(nValue, nTarget, bAsPercent); - m_vmProgressTracker->BeginAnimation(); + m_vmProgressTracker->SetDestroyPending(); + } + else + { + if (m_vmProgressTracker == nullptr) + m_vmProgressTracker.reset(new ProgressTrackerViewModel()); + + m_vmProgressTracker->SetImage(imageType, sImageName); + m_vmProgressTracker->SetProgress(sProgress); + m_vmProgressTracker->BeginAnimation(); + } RequestRender(); } @@ -705,9 +715,6 @@ void OverlayManager::UpdateProgressTracker(ra::ui::drawing::ISurface& pSurface, UpdatePopup(pSurface, pPopupLocations, fElapsed, *m_vmProgressTracker); - if (m_vmProgressTracker->IsAnimationComplete()) - m_vmProgressTracker->SetDestroyPending(); - if (m_vmProgressTracker->IsDestroyPending()) { // call UpdatePopup again to force it to "unpaint" diff --git a/src/ui/viewmodels/OverlayManager.hh b/src/ui/viewmodels/OverlayManager.hh index 4f8ce986..c0ee2a04 100644 --- a/src/ui/viewmodels/OverlayManager.hh +++ b/src/ui/viewmodels/OverlayManager.hh @@ -201,27 +201,27 @@ public: void AdvanceFrame() noexcept { ++m_nFrameId; } /// - /// Adds a score tracker for a leaderboard. + /// Adds a score tracker. /// - ScoreTrackerViewModel& AddScoreTracker(ra::LeaderboardID nLeaderboardId); + ScoreTrackerViewModel& AddScoreTracker(uint32_t nTrackerId); /// - /// Removes the score tracker associated to the specified leaderboard. + /// Removes the specified score tracker. /// - /// The unique identifier of the leaderboard associated to the tracker. - void RemoveScoreTracker(ra::LeaderboardID nLeaderboardId); + /// The unique identifier of the leaderboard tracker. + void RemoveScoreTracker(uint32_t nTrackerId); /// /// Gets the score tracker associated to the specified leaderboard. /// /// The unique identifier of the leaderboard associated to the tracker. /// Requested score tracker view model, nullptr if not found. - ScoreTrackerViewModel* GetScoreTracker(ra::LeaderboardID nLeaderboardId) + ScoreTrackerViewModel* GetScoreTracker(uint32_t nTrackerId) { std::lock_guard pGuard(m_pPopupQueueMutex); for (auto& pTracker : m_vScoreTrackers) { - if (ra::to_unsigned(pTracker->GetPopupId()) == nLeaderboardId) + if (ra::to_unsigned(pTracker->GetPopupId()) == nTrackerId) return pTracker.get(); } @@ -281,7 +281,7 @@ public: /// Updates the progress tracker indicator /// void UpdateProgressTracker(ra::ui::ImageType imageType, const std::string& sImageName, - unsigned nValue, unsigned nTarget, bool bAsPercent); + const std::wstring& sProgress); /// /// Clears all popups. diff --git a/src/ui/viewmodels/ProgressTrackerViewModel.cpp b/src/ui/viewmodels/ProgressTrackerViewModel.cpp index 2a611ea1..9d76df79 100644 --- a/src/ui/viewmodels/ProgressTrackerViewModel.cpp +++ b/src/ui/viewmodels/ProgressTrackerViewModel.cpp @@ -17,26 +17,8 @@ ProgressTrackerViewModel::ProgressTrackerViewModel() noexcept GSL_SUPPRESS_F6 SetVerticalOffset(10); } -void ProgressTrackerViewModel::SetProgress(unsigned nValue, unsigned nTarget, bool bAsPercent) +bool ProgressTrackerViewModel::UpdateRenderImage(double) { - if (bAsPercent) - { - const auto nProgressPercent = gsl::narrow_cast(static_cast(nValue) * 100 / nTarget); - m_sProgress = ra::StringPrintf(L"%d%%", nProgressPercent); - } - else - { - m_sProgress = ra::StringPrintf(L"%u/%u", nValue, nTarget); - } -} - -bool ProgressTrackerViewModel::UpdateRenderImage(double fElapsed) -{ - if (m_fAnimationProgress >= TOTAL_ANIMATION_TIME) - return false; - - m_fAnimationProgress += fElapsed; - bool bUpdated = false; if (m_pSurface == nullptr || m_bRegenerate) diff --git a/src/ui/viewmodels/ProgressTrackerViewModel.hh b/src/ui/viewmodels/ProgressTrackerViewModel.hh index 77cd65ba..04171c68 100644 --- a/src/ui/viewmodels/ProgressTrackerViewModel.hh +++ b/src/ui/viewmodels/ProgressTrackerViewModel.hh @@ -26,7 +26,7 @@ public: /// /// Sets the percentage of the image to highlight /// - void SetProgress(unsigned nValue, unsigned nTarget, bool bAsPercent); + void SetProgress(const std::wstring& sProgress) { m_sProgress = sProgress; } /// /// Gets the image to display. @@ -43,13 +43,9 @@ public: /// bool UpdateRenderImage(double fElapsed) override; - void BeginAnimation() noexcept override { m_fAnimationProgress = 0.0; } + void BeginAnimation() noexcept override { } bool IsAnimationStarted() const noexcept override { return true; } - - bool IsAnimationComplete() const noexcept override - { - return m_fAnimationProgress >= TOTAL_ANIMATION_TIME; - } + bool IsAnimationComplete() const noexcept override { return false; } private: ra::ui::ImageReference m_hImage; diff --git a/tests/Exports_Tests.cpp b/tests/Exports_Tests.cpp index f68a0bdd..5d70e68e 100644 --- a/tests/Exports_Tests.cpp +++ b/tests/Exports_Tests.cpp @@ -6,6 +6,7 @@ #include "RA_Emulators.h" #include "RA_Resource.h" +#include "tests\mocks\MockAchievementRuntime.hh" #include "tests\mocks\MockAudioSystem.hh" #include "tests\mocks\MockClock.hh" #include "tests\mocks\MockConfiguration.hh" @@ -15,7 +16,6 @@ #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockOverlayManager.hh" #include "tests\mocks\MockOverlayTheme.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockSessionTracker.hh" #include "tests\mocks\MockSurface.hh" @@ -37,11 +37,11 @@ using ra::data::context::mocks::MockEmulatorContext; using ra::data::context::mocks::MockGameContext; using ra::data::context::mocks::MockSessionTracker; using ra::data::context::mocks::MockUserContext; +using ra::services::mocks::MockAchievementRuntime; using ra::services::mocks::MockAudioSystem; using ra::services::mocks::MockClock; using ra::services::mocks::MockConfiguration; using ra::services::mocks::MockFrameEventQueue; -using ra::services::mocks::MockRcheevosClient; using ra::services::mocks::MockThreadPool; using ra::ui::mocks::MockDesktop; using ra::ui::mocks::MockOverlayTheme; @@ -101,7 +101,7 @@ private: MockUserContext mockUserContext; MockSessionTracker mockSessionTracker; MockConfiguration mockConfiguration; - MockRcheevosClient mockRcheevosClient; + MockAchievementRuntime mockRcheevosClient; MockAudioSystem mockAudioSystem; MockServer mockServer; MockOverlayManager mockOverlayManager; @@ -383,28 +383,6 @@ public: } private: - class MockAchievementRuntime : public ra::services::AchievementRuntime - { - public: - MockAchievementRuntime() noexcept : m_Override(this) {} - - void QueueChange(ChangeType nType, unsigned int nId, int nValue = 0) - { - m_vChanges.emplace_back(Change{ nType, nId, nValue }); - } - - void Process(_Inout_ std::vector& changes) noexcept override - { - GSL_SUPPRESS_F6 - changes.insert(changes.begin(), m_vChanges.begin(), m_vChanges.end()); - m_vChanges.clear(); - } - - private: - std::vector m_vChanges; - ra::services::ServiceLocator::ServiceOverride m_Override; - }; - class DoAchievementsFrameHarness { public: @@ -421,27 +399,6 @@ private: MockWindowManager mockWindowManager; MockClock mockClock; - DoAchievementsFrameHarness() noexcept - { - GSL_SUPPRESS_F6 mockServer.HandleRequest([this] - (const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - m_vUnlockedAchievements.insert(request.AchievementId); - response.Result = ra::api::ApiResult::Success; - - return true; - }); - - GSL_SUPPRESS_F6 mockServer.HandleRequest([this] - (const ra::api::SubmitLeaderboardEntry::Request& request, ra::api::SubmitLeaderboardEntry::Response& response) - { - m_vSubmittedLeaderboardEntries.insert_or_assign(request.LeaderboardId, request.Score); - response.Result = ra::api::ApiResult::Success; - - return true; - }); - } - ra::data::models::AchievementModel& MockAchievement(unsigned int nId) { auto& pAch = mockGameContext.Assets().NewAchievement(); @@ -453,637 +410,12 @@ private: return pAch; } - bool WasUnlocked(unsigned int nId) - { - mockThreadPool.ExecuteNextTask(); - return m_vUnlockedAchievements.find(nId) != m_vUnlockedAchievements.end(); - } - - unsigned int GetSubmittedScore(ra::LeaderboardID nId) - { - mockThreadPool.ExecuteNextTask(); - const auto pIter = m_vSubmittedLeaderboardEntries.find(nId); - return (pIter != m_vSubmittedLeaderboardEntries.end()) ? pIter->second : 0U; - } - private: MockThreadPool mockThreadPool; MockServer mockServer; MockUserContext mockUserContext; - - std::set m_vUnlockedAchievements; - std::map m_vSubmittedLeaderboardEntries; }; -public: - TEST_METHOD(TestDoAchievementsFrameAchievementTriggered) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 1U); - harness.mockGameContext.SetRichPresenceDisplayString(L"Titles"); - harness.mockGameContext.Assets().FindRichPresence()->Activate(); - - _RA_DoAchievementsFrame(); - - Assert::IsTrue(harness.WasUnlocked(1U)); - Assert::IsFalse(harness.mockDesktop.WasDialogShown()); - Assert::AreEqual(std::wstring(L"Titles"), harness.mockGameContext.Assets().FindAchievement(1U)->GetUnlockRichPresence()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementTriggeredNoRichPresence) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 1U); - - _RA_DoAchievementsFrame(); - - Assert::IsTrue(harness.WasUnlocked(1U)); - Assert::IsFalse(harness.mockDesktop.WasDialogShown()); - Assert::AreEqual(std::wstring(), harness.mockGameContext.Assets().FindAchievement(1U)->GetUnlockRichPresence()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementPrimed) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementPrimed, 1U); - - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetChallengeIndicator(1U); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::AreEqual(ra::ui::ImageType::Badge, pIndicator->GetImage().Type()); - Assert::AreEqual(std::string("55223"), pIndicator->GetImage().Name()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementPrimedNoTracker) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, ra::ui::viewmodels::PopupLocation::None); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementPrimed, 1U); - - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetChallengeIndicator(1U); - Assert::IsNull(pIndicator); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementUnprimed) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockOverlayManager.AddChallengeIndicator(1U, ra::ui::ImageType::Badge, "55223"); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementUnprimed, 1U); - - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetChallengeIndicator(1U); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::IsTrue(pIndicator->IsDestroyPending()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementProgress) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockRuntime.ActivateAchievement(1U, "M:0xH0001=10"); - auto* pTrigger = harness.mockRuntime.GetAchievementTrigger(1U); - pTrigger->measured_value = 5; - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 1U, pTrigger->measured_value); - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetProgressTracker(); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::AreEqual(ra::ui::ImageType::Badge, pIndicator->GetImage().Type()); - Assert::AreEqual(std::string("55223_lock"), pIndicator->GetImage().Name()); - Assert::AreEqual(std::wstring(L"5/10"), pIndicator->GetText()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementProgressNoTracker) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::None); - harness.mockRuntime.ActivateAchievement(1U, "M:0xH0001=10"); - auto* pTrigger = harness.mockRuntime.GetAchievementTrigger(1U); - pTrigger->measured_value = 5; - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 1U, pTrigger->measured_value); - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetProgressTracker(); - Assert::IsNull(pIndicator); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementProgressLocalBadge) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"local\\GUID"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockRuntime.ActivateAchievement(1U, "M:0xH0001=10"); - auto* pTrigger = harness.mockRuntime.GetAchievementTrigger(1U); - pTrigger->measured_value = 5; - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 1U, pTrigger->measured_value); - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetProgressTracker(); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::AreEqual(ra::ui::ImageType::Badge, pIndicator->GetImage().Type()); - Assert::AreEqual(std::string("local\\GUID"), pIndicator->GetImage().Name()); // no "_lock" - Assert::AreEqual(std::wstring(L"5/10"), pIndicator->GetText()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementProgressHighestPercent) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.MockAchievement(1U).SetBadge(L"55221"); - harness.mockRuntime.ActivateAchievement(1U, "M:0xH0001=10"); - harness.MockAchievement(2U).SetBadge(L"55222"); - harness.mockRuntime.ActivateAchievement(2U, "M:0xH0001=10"); - harness.MockAchievement(3U).SetBadge(L"55223"); - harness.mockRuntime.ActivateAchievement(3U, "M:0xH0001=10"); - harness.mockRuntime.GetAchievementTrigger(1U)->measured_value = 5; - harness.mockRuntime.GetAchievementTrigger(2U)->measured_value = 7; - harness.mockRuntime.GetAchievementTrigger(3U)->measured_value = 3; - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 1U, 5); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 2U, 7); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 3U, 3); - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetProgressTracker(); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::AreEqual(ra::ui::ImageType::Badge, pIndicator->GetImage().Type()); - Assert::AreEqual(std::string("55222_lock"), pIndicator->GetImage().Name()); - Assert::AreEqual(std::wstring(L"7/10"), pIndicator->GetText()); - } - - TEST_METHOD(TestDoAchievementsFrameAchievementProgressAsPercent) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U).SetBadge(L"55223"); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockRuntime.ActivateAchievement(1U, "G:0xH0001=10"); - auto* pTrigger = harness.mockRuntime.GetAchievementTrigger(1U); - pTrigger->measured_value = 5; - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementProgressChanged, 1U, pTrigger->measured_value); - _RA_DoAchievementsFrame(); - - const auto* pIndicator = harness.mockOverlayManager.GetProgressTracker(); - Assert::IsNotNull(pIndicator); - Ensures(pIndicator != nullptr); - - Assert::AreEqual(ra::ui::ImageType::Badge, pIndicator->GetImage().Type()); - Assert::AreEqual(std::string("55223_lock"), pIndicator->GetImage().Name()); - Assert::AreEqual(std::wstring(L"50%"), pIndicator->GetText()); - } - - TEST_METHOD(TestDoAchievementsFramePaused) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 1U); - harness.mockRuntime.SetPaused(true); - - _RA_DoAchievementsFrame(); - Assert::IsFalse(harness.WasUnlocked(1U)); - - harness.mockRuntime.SetPaused(false); - _RA_DoAchievementsFrame(); - Assert::IsTrue(harness.WasUnlocked(1U)); - } - - TEST_METHOD(TestDoAchievementsFramePauseOnTrigger) - { - DoAchievementsFrameHarness harness; - auto& pAch = harness.MockAchievement(1U); - pAch.SetName(L"AchievementTitle"); - pAch.UpdateServerCheckpoint(); - pAch.SetPauseOnTrigger(true); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have triggered:\n* AchievementTitle"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(harness.WasUnlocked(1U)); - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFramePauseOnReset) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - harness.mockGameContext.Assets().FindAchievement(1U)->SetPauseOnReset(true); - harness.mockGameContext.Assets().FindAchievement(1U)->SetName(L"AchievementTitle"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementReset, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have been reset:\n* AchievementTitle"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardStart) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::BottomLeft); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - pLeaderboard.SetDescription(L"Description"); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 1U, 1234U); - _RA_DoAchievementsFrame(); - - const auto* pPopup = harness.mockOverlayManager.GetMessage(1); - Assert::IsNotNull(pPopup); - Ensures(pPopup != nullptr); - Assert::IsTrue(harness.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\lb.wav")); - Assert::AreEqual(std::wstring(L"Leaderboard attempt started"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"Title"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Description"), pPopup->GetDetail()); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNotNull(pScore); - Ensures(pScore != nullptr); - Assert::AreEqual(std::wstring(L"1234"), pScore->GetDisplayText()); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardStartNoPopups) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::None); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::None); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - pLeaderboard.SetDescription(L"Description"); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 1U, 1234U); - _RA_DoAchievementsFrame(); - - const auto* pPopup = harness.mockOverlayManager.GetMessage(1); - Assert::IsNull(pPopup); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNull(pScore); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardStartPopupDisabled) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, ra::ui::viewmodels::PopupLocation::None); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 1U, 1234U); - _RA_DoAchievementsFrame(); - - const auto* pPopup = harness.mockOverlayManager.GetMessage(1); - Assert::IsNull(pPopup); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNotNull(pScore); - Ensures(pScore != nullptr); - Assert::AreEqual(std::wstring(L"1234"), pScore->GetDisplayText()); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardCanceled) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, ra::ui::viewmodels::PopupLocation::BottomLeft); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - pLeaderboard.SetDescription(L"Description"); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardCanceled, 1U); - _RA_DoAchievementsFrame(); - - const auto* pPopup = harness.mockOverlayManager.GetMessage(1); - Assert::IsNotNull(pPopup); - Ensures(pPopup != nullptr); - Assert::IsTrue(harness.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\lbcancel.wav")); - Assert::AreEqual(std::wstring(L"Leaderboard attempt failed"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"Title"), pPopup->GetDescription()); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNull(pScore); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardCaneledPopupDisabled) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, ra::ui::viewmodels::PopupLocation::None); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - pLeaderboard.SetDescription(L"Description"); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardCanceled, 1U); - _RA_DoAchievementsFrame(); - - const auto* pPopup = harness.mockOverlayManager.GetMessage(1); - Assert::IsNull(pPopup); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNull(pScore); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardUpdated) - { - DoAchievementsFrameHarness harness; - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - harness.mockOverlayManager.AddScoreTracker(1U); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardUpdated, 1U, 1235U); - _RA_DoAchievementsFrame(); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNotNull(pScore); - Ensures(pScore != nullptr); - Assert::AreEqual(std::wstring(L"1235"), pScore->GetDisplayText()); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardTriggered) - { - DoAchievementsFrameHarness harness; - harness.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardTriggered, 1U, 1236U); - _RA_DoAchievementsFrame(); - - Assert::AreEqual(1236U, harness.GetSubmittedScore(1U)); - - const auto* pScore = harness.mockOverlayManager.GetScoreTracker(1U); - Assert::IsNull(pScore); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardStartPauseOnReset) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStartReset, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have been reset:\n* Start: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardSubmitPauseOnReset) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardSubmitReset, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have been reset:\n* Submit: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardCancelPauseOnReset) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardCancelReset, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have been reset:\n* Cancel: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardValuePauseOnReset) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardValueReset, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have been reset:\n* Value: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardStartPauseOnTriggered) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStartTriggered, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have triggered:\n* Start: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardSubmitPauseOnTriggered) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardSubmitTriggered, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have triggered:\n* Submit: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameLeaderboardCancelPauseOnTriggered) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - auto& pLeaderboard = harness.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(1U); - pLeaderboard.SetName(L"Title"); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardCancelTriggered, 1U); - bool bWasPaused = false; - harness.mockEmulatorContext.SetPauseFunction([&bWasPaused] { bWasPaused = true; }); - - bool bMessageSeen = false; - harness.mockDesktop.ExpectWindow([&bMessageSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - bMessageSeen = true; - Assert::AreEqual(std::wstring(L"The following triggers have triggered:\n* Cancel: Title"), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::OK; - }); - - _RA_DoAchievementsFrame(); - harness.mockFrameEventQueue.DoFrame(); // UpdateUIForFrameChange isn't called in the unit test build - - Assert::IsTrue(bMessageSeen); - Assert::IsTrue(bWasPaused); - } - - TEST_METHOD(TestDoAchievementsFrameMultiple) - { - DoAchievementsFrameHarness harness; - harness.MockAchievement(1U); - harness.MockAchievement(2U); - harness.mockGameContext.Assets().NewLeaderboard().SetID(3U); - harness.mockGameContext.Assets().NewLeaderboard().SetID(4U); - harness.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, ra::ui::viewmodels::PopupLocation::BottomRight); - - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 3U, 1234U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 1U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::AchievementTriggered, 2U); - harness.mockRuntime.QueueChange(ra::services::AchievementRuntime::ChangeType::LeaderboardStarted, 4U, 5678U); - _RA_DoAchievementsFrame(); - - Assert::IsTrue(harness.WasUnlocked(1U)); - Assert::IsTrue(harness.WasUnlocked(2U)); - - const auto* pScore3 = harness.mockOverlayManager.GetScoreTracker(3U); - Assert::IsNotNull(pScore3); - Ensures(pScore3 != nullptr); - Assert::AreEqual(std::wstring(L"1234"), pScore3->GetDisplayText()); - - const auto* pScore4 = harness.mockOverlayManager.GetScoreTracker(4U); - Assert::IsNotNull(pScore4); - Ensures(pScore4 != nullptr); - Assert::AreEqual(std::wstring(L"5678"), pScore4->GetDisplayText()); - } - -private: void AssertMenuItem(const RA_MenuItem* pItem, LPARAM nId, const wchar_t* sLabel, bool bChecked = false) { Assert::AreEqual((int)nId, (int)pItem->nID); diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj index db038db6..09a7876c 100644 --- a/tests/RA_Integration.Tests.vcxproj +++ b/tests/RA_Integration.Tests.vcxproj @@ -318,7 +318,6 @@ - @@ -382,7 +381,7 @@ - + @@ -445,7 +444,6 @@ - diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters index b404634e..98a2c0ad 100644 --- a/tests/RA_Integration.Tests.vcxproj.filters +++ b/tests/RA_Integration.Tests.vcxproj.filters @@ -459,10 +459,7 @@ Tests\Data\Models - - Code - - + Mocks @@ -560,8 +557,5 @@ Code - - Mocks - \ No newline at end of file diff --git a/tests/api/ConnectedServer_Tests.cpp b/tests/api/ConnectedServer_Tests.cpp index 964db0c7..e727f036 100644 --- a/tests/api/ConnectedServer_Tests.cpp +++ b/tests/api/ConnectedServer_Tests.cpp @@ -31,97 +31,6 @@ TEST_CLASS(ConnectedServer_Tests) public: // ConnectedServer manages converting the Http error codes to Incomplete responses, this just tests the handling // of Incomplete responses by the ApiRequestBase. - TEST_METHOD(TestCallAsyncWithRetry) - { - MockThreadPool mockThreadPool; - MockServer mockServer; - bool bHandlerCalled = false; - mockServer.HandleRequest([&bHandlerCalled](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - bHandlerCalled = true; - Assert::AreEqual(1234U, request.AchievementId); - Assert::AreEqual(std::string("HASH"), request.GameHash); - Assert::IsTrue(request.Hardcore); - - response.Result = ra::api::ApiResult::Incomplete; - return true; - }); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - bool bCallbackCalled = false; - request.CallAsyncWithRetry([&bCallbackCalled](const ra::api::AwardAchievement::Response& response) - { - Assert::AreEqual(ra::api::ApiResult::Success, response.Result); - bCallbackCalled = true; - }); - - // call should be async - Assert::IsFalse(bCallbackCalled); - Assert::IsFalse(bHandlerCalled); - Assert::AreEqual({ 1U }, mockThreadPool.PendingTasks()); - - // async call should be incomplete and requeued - mockThreadPool.ExecuteNextTask(); - Assert::IsFalse(bCallbackCalled); - Assert::IsTrue(bHandlerCalled); - Assert::AreEqual({ 1U }, mockThreadPool.PendingTasks()); - Assert::AreEqual(std::chrono::milliseconds(500), mockThreadPool.NextTaskDelay()); - - // callback called again, still incomplete, requeue with longer delay - bHandlerCalled = false; - mockThreadPool.AdvanceTime(std::chrono::milliseconds(500)); - Assert::IsFalse(bCallbackCalled); - Assert::IsTrue(bHandlerCalled); - Assert::AreEqual({ 1U }, mockThreadPool.PendingTasks()); - Assert::AreEqual(std::chrono::milliseconds(1000), mockThreadPool.NextTaskDelay()); - - // response succeeded, should complete transaction - mockServer.HandleRequest([&bHandlerCalled](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - bHandlerCalled = true; - Assert::AreEqual(1234U, request.AchievementId); - Assert::AreEqual(std::string("HASH"), request.GameHash); - Assert::IsTrue(request.Hardcore); - - response.Result = ra::api::ApiResult::Success; - return true; - }); - - bHandlerCalled = false; - mockThreadPool.AdvanceTime(std::chrono::milliseconds(1000)); - Assert::IsTrue(bCallbackCalled); - Assert::IsTrue(bHandlerCalled); - Assert::AreEqual({ 0U }, mockThreadPool.PendingTasks()); - } - - TEST_METHOD(TestCallAsyncWithRetryMaxDelay) - { - MockThreadPool mockThreadPool; - MockServer mockServer; - mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.Result = ra::api::ApiResult::Incomplete; - return true; - }); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - request.CallAsyncWithRetry([](const ra::api::AwardAchievement::Response&) {}); - - // call should be async - mockThreadPool.ExecuteNextTask(); - for (int i = 0; i < 100; i++) - mockThreadPool.AdvanceTime(mockThreadPool.NextTaskDelay()); - - Assert::AreEqual(std::chrono::duration_cast(std::chrono::minutes(2)), mockThreadPool.NextTaskDelay()); - } // ==================================================== // These tests validate the generic handling of errors - they just happen to be written using the LatestClient API @@ -207,106 +116,6 @@ public: Assert::AreEqual(std::string(""), response.MinimumVersion); } - TEST_METHOD(TestAwardAchievementFailed403) - { - MockUserContext mockUserContext; - mockUserContext.Initialize("Username", "ApiToken"); - - MockHttpRequester mockHttp([]([[maybe_unused]] const Http::Request& /*request*/) - { - return Http::Response(Http::StatusCode::Forbidden, ""); - }); - - ConnectedServer server("host.com"); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - auto response = server.AwardAchievement(request); - - Assert::AreEqual(ApiResult::Error, response.Result); - Assert::AreEqual(std::string("HTTP error code: 403 (err403)"), response.ErrorMessage); - } - - TEST_METHOD(TestAwardAchievementFailed403WithMessage) - { - MockUserContext mockUserContext; - mockUserContext.Initialize("Username", "ApiToken"); - - MockHttpRequester mockHttp([]([[maybe_unused]] const Http::Request& /*request*/) - { - return Http::Response(Http::StatusCode::Forbidden, "{\"Success\":false, \"Error\":\"Invalid API token\"}"); - }); - - ConnectedServer server("host.com"); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - auto response = server.AwardAchievement(request); - - Assert::AreEqual(ApiResult::Error, response.Result); - Assert::AreEqual(std::string("Invalid API token"), response.ErrorMessage); - } - - TEST_METHOD(TestAwardAchievementFailed429) - { - MockUserContext mockUserContext; - mockUserContext.Initialize("Username", "ApiToken"); - - MockHttpRequester mockHttp([]([[maybe_unused]] const Http::Request& /*request*/) - { - return Http::Response(Http::StatusCode::TooManyRequests, - "\n" - "429 Too Many Requests\n" - "\n" - "

429 Too Many Requests

\n" - "
nginx
\n" - "\n" - ""); - }); - - ConnectedServer server("host.com"); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - auto response = server.AwardAchievement(request); - - Assert::AreEqual(ApiResult::Error, response.Result); - Assert::AreEqual(std::string("HTTP error code: 429 (err429)"), response.ErrorMessage); - } - - TEST_METHOD(TestAwardAchievementFailed429Json) - { - MockUserContext mockUserContext; - mockUserContext.Initialize("Username", "ApiToken"); - - MockHttpRequester mockHttp([]([[maybe_unused]] const Http::Request& /*request*/) - { - return Http::Response(Http::StatusCode::TooManyRequests, - "{\"Success\": false,\"Error\": \"Too Many Attempts\"}"); - }); - - ConnectedServer server("host.com"); - - AwardAchievement::Request request; - request.GameHash = "HASH"; - request.AchievementId = 1234U; - request.Hardcore = true; - - auto response = server.AwardAchievement(request); - - Assert::AreEqual(ApiResult::Error, response.Result); - Assert::AreEqual(std::string("Too Many Attempts"), response.ErrorMessage); - } - TEST_METHOD(TestLatestClientUnknownServer) { MockHttpRequester mockHttp([]([[maybe_unused]] const Http::Request& /*request*/) @@ -410,35 +219,6 @@ public: // End of generic validation tests // ==================================================== - - // ==================================================== - // SubmitLeaderboardEntry - - TEST_METHOD(TestSubmitLeaderboardEntrySigned) - { - MockUserContext mockUserContext; - mockUserContext.Initialize("Username", "ApiToken"); - - MockHttpRequester mockHttp([](const Http::Request&) - { - return Http::Response(Http::StatusCode::OK, - "{\"Success\":true,\"Response\":{\"Score\":1234,\"BestScore\":2345," - "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":8765,\"Rank\":1},{\"User\":\"Player2\",\"Score\":7654,\"Rank\":2}]," - "\"RankInfo\":{\"Rank\":5,\"NumEntries\":\"17\"}}}"); - }); - - ra::services::ServiceLocator::ServiceOverride serviceOverride(new ConnectedServer("host.com"), true); - auto& server = ra::services::ServiceLocator::GetMutable(); - - SubmitLeaderboardEntry::Request request; - request.LeaderboardId = 234; - request.Score = -55667788; - auto response = server.SubmitLeaderboardEntry(request); - - Assert::AreEqual(ApiResult::Success, response.Result); - Assert::AreEqual(std::string(), response.ErrorMessage); - } - // ==================================================== // FetchCodeNotes diff --git a/tests/data/context/EmulatorContext_Tests.cpp b/tests/data/context/EmulatorContext_Tests.cpp index aca34ff9..fb803338 100644 --- a/tests/data/context/EmulatorContext_Tests.cpp +++ b/tests/data/context/EmulatorContext_Tests.cpp @@ -14,7 +14,6 @@ #include "tests\mocks\MockHttpRequester.hh" #include "tests\mocks\MockLocalStorage.hh" #include "tests\mocks\MockOverlayManager.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockThreadPool.hh" #include "tests\mocks\MockUserContext.hh" @@ -42,15 +41,15 @@ private: ra::services::mocks::MockFileSystem mockFileSystem; ra::services::mocks::MockHttpRequester mockHttpRequester; ra::services::mocks::MockThreadPool mockThreadPool; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; ra::ui::mocks::MockDesktop mockDesktop; ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager; ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager; GSL_SUPPRESS_F6 EmulatorContextHarness() noexcept - : mockHttpRequester([](const ra::services::Http::Request&) { return ra::services::Http::Response(ra::services::Http::StatusCode::OK, ""); }) + : mockHttpRequester([](const ra::services::Http::Request&) { return ra::services::Http::Response(ra::services::Http::StatusCode::OK, ""); }), + m_Override(this) { - mockRcheevosClient.MockGame(); + mockAchievementRuntime.MockGame(); } void MockVersions(const std::string& sClientVersion, const std::string& sServerVersion, const std::string& sMinimumVersion) @@ -83,6 +82,9 @@ private: pAch.UpdateServerCheckpoint(); return pAch; } + + private: + ra::services::ServiceLocator::ServiceOverride m_Override; }; class EmulatorContextNotifyHarness : public EmulatorContext::NotifyTarget @@ -655,44 +657,43 @@ public: TEST_METHOD(TestDisableHardcoreMode) { EmulatorContextHarness emulator; - emulator.mockGameContext.SetGameId(1234U); + emulator.mockAchievementRuntime.MockGame(); emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); bool bWasReset = false; emulator.SetResetFunction([&bWasReset]() { bWasReset = true; }); - auto& pLeaderboard = emulator.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(32U); - pLeaderboard.SetState(ra::data::models::AssetState::Active); - Assert::IsTrue(pLeaderboard.IsActive()); + emulator.mockAchievementRuntime.MockLeaderboardWithLboard(32U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(44U); + auto* pAchievement45 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(45U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement45, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + auto* pAchievement46 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(46U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement46, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + emulator.mockGameContext.InitializeFromAchievementRuntime(); - emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; - emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; - emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; + auto* vmAchievement44 = emulator.mockGameContext.Assets().FindAchievement(44U); + auto* vmAchievement45 = emulator.mockGameContext.Assets().FindAchievement(45U); + auto* vmAchievement46 = emulator.mockGameContext.Assets().FindAchievement(46U); + auto* vmLeaderboard32 = emulator.mockGameContext.Assets().FindLeaderboard(32U); + Expects(vmAchievement44 && vmAchievement45 && vmAchievement46 && vmLeaderboard32); - auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement44.SetID(44U); - pAchievement44.SetState(ra::data::models::AssetState::Active); - auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement45.SetID(45U); - auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement46.SetID(46U); - - Assert::IsTrue(pAchievement44.IsActive()); - Assert::IsFalse(pAchievement45.IsActive()); - Assert::IsFalse(pAchievement46.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsTrue(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); + Assert::IsTrue(vmLeaderboard32->IsActive()); emulator.DisableHardcoreMode(); // ensure mode was deactivated Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsFalse(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient())); // deactivating hardcore mode should not disable leaderboards - Assert::IsTrue(pLeaderboard.IsActive()); + Assert::IsTrue(vmLeaderboard32->IsActive()); // deactivating hardcore mode should update active achievements - Assert::IsTrue(pAchievement44.IsActive()); - Assert::IsFalse(pAchievement45.IsActive()); - Assert::IsFalse(pAchievement46.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsFalse(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); // deactivating hardcore mode should not reset the emulator Assert::IsFalse(bWasReset); @@ -709,7 +710,7 @@ public: Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::IsFalse(emulator.mockDesktop.WasDialogShown()); - Assert::IsTrue(bWasReset); + Assert::IsFalse(bWasReset); // if no game loaded, rc_client won't raise a reset event } TEST_METHOD(TestEnableHardcoreModeAlreadyEnabled) @@ -734,28 +735,36 @@ public: emulator.MockVersions("0.57", "0.57"); emulator.mockGameContext.SetGameId(1U); emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) - { - Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); - Assert::AreEqual(std::wstring(L"Enable hardcore mode?"), vmMessageBox.GetHeader()); - Assert::AreEqual(std::wstring(L"Enabling hardcore mode will reset the emulator. You will lose any progress that has not been saved through the game."), vmMessageBox.GetMessage()); - return ra::ui::DialogResult::Yes; - }); + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { + Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::AreEqual(std::wstring(L"Enable hardcore mode?"), vmMessageBox.GetHeader()); + Assert::AreEqual(std::wstring(L"Enabling hardcore mode will reset the emulator. You will lose any " + L"progress that has not been saved through the game."), + vmMessageBox.GetMessage()); + return ra::ui::DialogResult::Yes; + }); - emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; - emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; - emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; + emulator.mockAchievementRuntime.MockLeaderboardWithLboard(32U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(44U); + auto* pAchievement45 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(45U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement45, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + auto* pAchievement46 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(46U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement46, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + emulator.mockGameContext.InitializeFromAchievementRuntime(); - auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement44.SetID(44U); - auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement45.SetID(45U); - auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement46.SetID(46U); + auto* vmAchievement44 = emulator.mockGameContext.Assets().FindAchievement(44U); + auto* vmAchievement45 = emulator.mockGameContext.Assets().FindAchievement(45U); + auto* vmAchievement46 = emulator.mockGameContext.Assets().FindAchievement(46U); + auto* vmLeaderboard32 = emulator.mockGameContext.Assets().FindLeaderboard(32U); + Expects(vmAchievement44 && vmAchievement45 && vmAchievement46 && vmLeaderboard32); - auto& pLeaderboard = emulator.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(32U); - Assert::IsFalse(pLeaderboard.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsFalse(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); + Assert::IsFalse(vmLeaderboard32->IsActive()); bool bWasReset = false; emulator.SetResetFunction([&bWasReset]() { bWasReset = true; }); @@ -764,17 +773,18 @@ public: // ensure hardcore mode was enabled Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsTrue(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); // ensure prompt was shown Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); // enabling hardcore mode should enable leaderboards - Assert::IsTrue(pLeaderboard.IsActive()); + Assert::IsTrue(vmLeaderboard32->IsActive()); // enabling hardcore mode should activate achievements not unlocked in hardcore - Assert::IsTrue(pAchievement44.IsActive()); - Assert::IsTrue(pAchievement45.IsActive()); - Assert::IsFalse(pAchievement46.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsTrue(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); // enabling hardcore mode should reset the emulator Assert::IsTrue(bWasReset); @@ -785,7 +795,10 @@ public: EmulatorContextHarness emulator; emulator.MockVersions("0.57", "0.57"); emulator.mockGameContext.SetGameId(1U); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::AreEqual(std::wstring(L"Enable hardcore mode?"), vmMessageBox.GetHeader()); @@ -799,6 +812,7 @@ public: Assert::IsFalse(emulator.EnableHardcoreMode()); Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsFalse(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); Assert::IsFalse(bWasReset); } @@ -808,7 +822,10 @@ public: EmulatorContextHarness emulator; emulator.MockVersions("0.58", "0.57"); emulator.mockGameContext.SetGameId(1U); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::AreEqual(std::wstring(L"Enable hardcore mode?"), vmMessageBox.GetHeader()); @@ -821,6 +838,7 @@ public: Assert::IsTrue(emulator.EnableHardcoreMode()); Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsTrue(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); Assert::IsTrue(bWasReset); } @@ -832,8 +850,11 @@ public: emulator.mockConfiguration.SetHostName("host"); emulator.mockUserContext.Initialize("User", "Token"); emulator.mockGameContext.SetGameId(1U); + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; emulator.MockVersions("0.57.0.0", "0.58.0.0"); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::AreEqual(std::wstring(L"A newer client is required for hardcore mode."), vmMessageBox.GetHeader()); @@ -847,6 +868,7 @@ public: Assert::IsFalse(emulator.EnableHardcoreMode()); Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsFalse(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); Assert::AreEqual(std::string("http://host/download.php"), emulator.mockDesktop.LastOpenedUrl()); @@ -862,8 +884,11 @@ public: emulator.mockConfiguration.SetHostName("host"); emulator.mockUserContext.Initialize("User", "Token"); emulator.mockGameContext.SetGameId(1U); + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; emulator.MockVersions("0.57.0.0", "0.58.0.0"); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::AreEqual(std::wstring(L"A newer client is required for hardcore mode."), vmMessageBox.GetHeader()); @@ -877,6 +902,7 @@ public: Assert::IsFalse(emulator.EnableHardcoreMode()); Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsFalse(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); Assert::AreEqual(std::string(""), emulator.mockDesktop.LastOpenedUrl()); Assert::IsTrue(emulator.mockUserContext.IsLoggedIn()); @@ -889,21 +915,27 @@ public: emulator.MockVersions("0.57", "0.57"); emulator.mockGameContext.SetGameId(1U); emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true); + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; - emulator.mockRcheevosClient.MockAchievement(44U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE; - emulator.mockRcheevosClient.MockAchievement(45U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; - emulator.mockRcheevosClient.MockAchievement(46U)->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; + emulator.mockAchievementRuntime.MockLeaderboardWithLboard(32U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(44U); + auto* pAchievement45 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(45U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement45, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + auto* pAchievement46 = emulator.mockAchievementRuntime.MockAchievementWithTrigger(46U); + emulator.mockAchievementRuntime.UnlockAchievement(pAchievement46, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + emulator.mockGameContext.InitializeFromAchievementRuntime(); - auto& pAchievement44 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement44.SetID(44U); - auto& pAchievement45 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement45.SetID(45U); - auto& pAchievement46 = emulator.mockGameContext.Assets().NewAchievement(); - pAchievement46.SetID(46U); + auto* vmAchievement44 = emulator.mockGameContext.Assets().FindAchievement(44U); + auto* vmAchievement45 = emulator.mockGameContext.Assets().FindAchievement(45U); + auto* vmAchievement46 = emulator.mockGameContext.Assets().FindAchievement(46U); + auto* vmLeaderboard32 = emulator.mockGameContext.Assets().FindLeaderboard(32U); + Expects(vmAchievement44 && vmAchievement45 && vmAchievement46 && vmLeaderboard32); - auto& pLeaderboard = emulator.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(32U); - Assert::IsFalse(pLeaderboard.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsFalse(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); + Assert::IsFalse(vmLeaderboard32->IsActive()); bool bWasReset = false; emulator.SetResetFunction([&bWasReset]() { bWasReset = true; }); @@ -912,17 +944,18 @@ public: // ensure hardcore mode was enabled Assert::IsTrue(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsTrue(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); // ensure prompt was not shown Assert::IsFalse(emulator.mockDesktop.WasDialogShown()); // enabling hardcore mode should enable leaderboards - Assert::IsTrue(pLeaderboard.IsActive()); + Assert::IsTrue(vmLeaderboard32->IsActive()); // enabling hardcore mode should activate achievements not unlocked in hardcore - Assert::IsTrue(pAchievement44.IsActive()); - Assert::IsTrue(pAchievement45.IsActive()); - Assert::IsFalse(pAchievement46.IsActive()); + Assert::IsTrue(vmAchievement44->IsActive()); + Assert::IsTrue(vmAchievement45->IsActive()); + Assert::IsFalse(vmAchievement46->IsActive()); // enabling hardcore mode should reset the emulator Assert::IsTrue(bWasReset); @@ -935,8 +968,11 @@ public: emulator.mockConfiguration.SetHostName("host"); emulator.mockUserContext.Initialize("User", "Token"); emulator.mockGameContext.SetGameId(1U); + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; emulator.MockVersions("0.57.0.0", "0.58.0.0"); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) { Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); Assert::AreEqual(std::wstring(L"A newer client is required for hardcore mode."), vmMessageBox.GetHeader()); @@ -950,6 +986,7 @@ public: Assert::IsFalse(emulator.EnableHardcoreMode(false)); Assert::IsFalse(emulator.mockConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore)); + Assert::IsFalse(rc_client_get_hardcore_enabled(emulator.mockAchievementRuntime.GetClient()) != 0); Assert::IsTrue(emulator.mockDesktop.WasDialogShown()); Assert::AreEqual(std::string("http://host/download.php"), emulator.mockDesktop.LastOpenedUrl()); @@ -1317,36 +1354,6 @@ public: Assert::AreEqual(gsl::at(buffer, 1), memory.at(1)); } - TEST_METHOD(TestReadMemoryByteInvalidAddressDisablesAchievement) - { - memory.at(4) = 0xA8; - memory.at(5) = 0x00; - memory.at(6) = 0x37; - memory.at(7) = 0x2E; - - EmulatorContextHarness emulator; - emulator.mockAchievementRuntime.ActivateAchievement(1, "0xH0004=1_0xH0044=1"); - Assert::IsTrue(emulator.mockAchievementRuntime.IsAchievementSupported(1)); - - // memory has not yet been registered. achievement should not be disabled [client may register memory later] - Assert::AreEqual(0x00, static_cast(emulator.ReadMemory(4U, MemSize::EightBit))); - Assert::IsTrue(emulator.mockAchievementRuntime.IsAchievementSupported(1)); - - // valid memory registered. achievement should not be disabled - emulator.AddMemoryBlock(0, 20, &ReadMemory0, &WriteMemory0); - Assert::AreEqual(0xA8, static_cast(emulator.ReadMemory(4U, MemSize::EightBit))); - Assert::IsTrue(emulator.mockAchievementRuntime.IsAchievementSupported(1)); - - // address not in supported memory range, achievement should not be disabled [indirect memory address] - Assert::AreEqual(0x00, static_cast(emulator.ReadMemory(68U, MemSize::EightBit))); - Assert::IsTrue(emulator.mockAchievementRuntime.IsAchievementSupported(1)); - - // invalid memory region registered, address not supported. achievement should be disabled - emulator.AddMemoryBlock(1, 80, nullptr, nullptr); - Assert::AreEqual(0x00, static_cast(emulator.ReadMemory(68U, MemSize::EightBit))); - Assert::IsFalse(emulator.mockAchievementRuntime.IsAchievementSupported(1)); - } - TEST_METHOD(TestReadMemoryBuffer) { InitializeMemory(); @@ -1688,45 +1695,52 @@ public: emulator.MockVersions("0.57", "0.57"); emulator.mockGameContext.SetGameId(1U); emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Leaderboards, true); - emulator.mockDesktop.ExpectWindow([&emulator](ra::ui::viewmodels::MessageBoxViewModel&) + emulator.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + emulator.mockAchievementRuntime.GetClient()->state.hardcore = 0; + emulator.mockDesktop.ExpectWindow( + [&emulator](ra::ui::viewmodels::MessageBoxViewModel&) { return ra::ui::DialogResult::Yes; }); - auto& ach0 = emulator.MockAchievement(); // 0 = Unmodified - ach0.SetState(ra::data::models::AssetState::Active); - auto& ach1 = emulator.MockAchievement(); // 1 = Modified - ach1.SetName(L"New Name"); - ach1.SetState(ra::data::models::AssetState::Active); - auto& ach2 = emulator.MockAchievement(); // 2 = New - ach2.SetNew(); - ach2.SetState(ra::data::models::AssetState::Active); - auto& ach3 = emulator.MockAchievement(); // 3 = Unpublished - ach3.SetName(L"Newer Name"); - ach3.UpdateLocalCheckpoint(); - ach3.SetState(ra::data::models::AssetState::Active); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(44U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(45U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(46U); + emulator.mockAchievementRuntime.MockAchievementWithTrigger(47U); + emulator.mockGameContext.InitializeFromAchievementRuntime(); - Assert::AreEqual(ra::data::models::AssetChanges::None, emulator.mockGameContext.Assets().GetItemAt(0)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::Modified, emulator.mockGameContext.Assets().GetItemAt(1)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::New, emulator.mockGameContext.Assets().GetItemAt(2)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::Unpublished, emulator.mockGameContext.Assets().GetItemAt(3)->GetChanges()); + auto* vmAchievement44 = emulator.mockGameContext.Assets().FindAchievement(44U); + auto* vmAchievement45 = emulator.mockGameContext.Assets().FindAchievement(45U); + vmAchievement45->SetName(L"New Name"); // modified + auto* vmAchievement46 = emulator.mockGameContext.Assets().FindAchievement(46U); + vmAchievement46->SetNew(); // new + auto* vmAchievement47 = emulator.mockGameContext.Assets().FindAchievement(47U); + vmAchievement47->SetName(L"New Name"); + vmAchievement47->UpdateLocalCheckpoint(); // unpublished - Assert::AreEqual(ra::data::models::AssetState::Active, emulator.mockGameContext.Assets().GetItemAt(0)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Active, emulator.mockGameContext.Assets().GetItemAt(1)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Active, emulator.mockGameContext.Assets().GetItemAt(2)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Active, emulator.mockGameContext.Assets().GetItemAt(3)->GetState()); + Assert::AreEqual(ra::data::models::AssetChanges::None, vmAchievement44->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::Modified, vmAchievement45->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::New, vmAchievement46->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::Unpublished, vmAchievement47->GetChanges()); + + Assert::AreEqual(ra::data::models::AssetState::Active, vmAchievement44->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Active, vmAchievement45->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Active, vmAchievement46->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Active, vmAchievement47->GetState()); Assert::IsTrue(emulator.EnableHardcoreMode()); - Assert::AreEqual(ra::data::models::AssetChanges::None, emulator.mockGameContext.Assets().GetItemAt(0)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::Modified, emulator.mockGameContext.Assets().GetItemAt(1)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::New, emulator.mockGameContext.Assets().GetItemAt(2)->GetChanges()); - Assert::AreEqual(ra::data::models::AssetChanges::Unpublished, emulator.mockGameContext.Assets().GetItemAt(3)->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::None, vmAchievement44->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::Modified, vmAchievement45->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::New, vmAchievement46->GetChanges()); + Assert::AreEqual(ra::data::models::AssetChanges::Unpublished, vmAchievement47->GetChanges()); - Assert::AreEqual(ra::data::models::AssetState::Active, emulator.mockGameContext.Assets().GetItemAt(0)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, emulator.mockGameContext.Assets().GetItemAt(1)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, emulator.mockGameContext.Assets().GetItemAt(2)->GetState()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, emulator.mockGameContext.Assets().GetItemAt(3)->GetState()); + // enabling hardcore does not change the asset state to waiting. it issues a reset request + // and resetting the runtime changes the asset state to waiting. + Assert::AreEqual(ra::data::models::AssetState::Active, vmAchievement44->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Inactive, vmAchievement45->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Inactive, vmAchievement46->GetState()); + Assert::AreEqual(ra::data::models::AssetState::Inactive, vmAchievement47->GetState()); } TEST_METHOD(TestFormatAddress) diff --git a/tests/data/context/GameAssets_Tests.cpp b/tests/data/context/GameAssets_Tests.cpp index 8b8c59f7..74a10d1d 100644 --- a/tests/data/context/GameAssets_Tests.cpp +++ b/tests/data/context/GameAssets_Tests.cpp @@ -35,6 +35,7 @@ public: GameAssetsHarness() noexcept { GSL_SUPPRESS_F6 mockGameContext.SetGameTitle(L"GameName"); + mockRuntime.MockGame(); } void SaveAllAssets() diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp index 6b73ff37..0f1ee85b 100644 --- a/tests/data/context/GameContext_Tests.cpp +++ b/tests/data/context/GameContext_Tests.cpp @@ -13,6 +13,7 @@ #include "tests\RA_UnitTestHelpers.h" #include "tests\data\DataAsserts.hh" +#include "tests\mocks\MockAchievementRuntime.hh" #include "tests\mocks\MockAudioSystem.hh" #include "tests\mocks\MockConsoleContext.hh" #include "tests\mocks\MockEmulatorContext.hh" @@ -21,7 +22,6 @@ #include "tests\mocks\MockDesktop.hh" #include "tests\mocks\MockLocalStorage.hh" #include "tests\mocks\MockOverlayManager.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockSessionTracker.hh" #include "tests\mocks\MockThreadPool.hh" @@ -54,8 +54,7 @@ public: { public: GameContextHarness() noexcept : - m_OverrideGameContext(this), - m_OverrideRuntime(&runtime) + m_OverrideGameContext(this) { } @@ -65,13 +64,12 @@ public: ra::services::mocks::MockLocalStorage mockStorage; ra::services::mocks::MockThreadPool mockThreadPool; ra::services::mocks::MockAudioSystem mockAudioSystem; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; + ra::services::mocks::MockAchievementRuntime mockAchievementRuntime; ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager; ra::data::context::mocks::MockConsoleContext mockConsoleContext; ra::data::context::mocks::MockEmulatorContext mockEmulator; ra::data::context::mocks::MockSessionTracker mockSessionTracker; ra::data::context::mocks::MockUserContext mockUser; - ra::services::AchievementRuntime runtime; ra::ui::mocks::MockDesktop mockDesktop; ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager; @@ -102,7 +100,7 @@ public: std::wstring GetRichPresenceDisplayString() const { - return runtime.GetRichPresenceDisplayString(); + return mockAchievementRuntime.GetRichPresenceDisplayString(); } bool IsRichPresenceFromFile() const @@ -153,10 +151,10 @@ public: mockConsoleContext.SetId(nConsoleID); mockConsoleContext.SetName(ra::Widen(rc_console_name(static_cast(nConsoleID)))); - mockRcheevosClient.MockUser("Username", "ApiToken"); - mockRcheevosClient.MockResponse("r=gameid&m=" + sHash, + mockAchievementRuntime.MockUser("Username", "ApiToken"); + mockAchievementRuntime.MockResponse("r=gameid&m=" + sHash, "{\"Success\":true,\"GameID\":" + std::to_string(nGameID) + "}"); - mockRcheevosClient.MockResponse( + mockAchievementRuntime.MockResponse( "r=patch&u=Username&t=ApiToken&g=" + std::to_string(nGameID), "{\"Success\":true,\"PatchData\":{" "\"ID\":" + std::to_string(nGameID) + "," @@ -167,7 +165,7 @@ public: "\"Leaderboards\":[" + sLeaderboards + "]," "\"RichPresencePatch\":\"" + sRichPresence + "\"" "}}"); - mockRcheevosClient.MockResponse( + mockAchievementRuntime.MockResponse( "r=startsession&u=Username&t=ApiToken&g=" + std::to_string(nGameID) + "&l=" RCHEEVOS_VERSION_STRING, sUnlocks.empty() ? "{\"Success\":true}" : "{\"Success\":true," + sUnlocks + "}"); @@ -203,7 +201,6 @@ public: } ra::services::ServiceLocator::ServiceOverride m_OverrideGameContext; - ra::services::ServiceLocator::ServiceOverride m_OverrideRuntime; }; class GameContextNotifyTarget : public GameContext::NotifyTarget @@ -1065,16 +1062,16 @@ public: game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321"); bool bBeforeResponseCalled = false; - game.mockRcheevosClient.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1", + game.mockAchievementRuntime.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1", [&game, &bBeforeResponseCalled]() { bBeforeResponseCalled = true; - Assert::IsTrue(game.runtime.IsPaused()); + Assert::IsTrue(game.mockAchievementRuntime.IsPaused()); }); game.LoadGame(1U, "0123456789abcdeffedcba987654321"); - Assert::IsTrue(bBeforeResponseCalled); // paused during load - Assert::IsFalse(game.runtime.IsPaused()); // not paused after load + Assert::IsTrue(bBeforeResponseCalled); // paused during load + Assert::IsFalse(game.mockAchievementRuntime.IsPaused()); // not paused after load } TEST_METHOD(TestLoadGameWhileRuntimePaused) @@ -1083,18 +1080,18 @@ public: game.MockLoadGameAPIs(1U, "0123456789abcdeffedcba987654321"); bool bBeforeResponseCalled = false; - game.mockRcheevosClient.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1", + game.mockAchievementRuntime.OnBeforeResponse("r=patch&u=Username&t=ApiToken&g=1", [&game, &bBeforeResponseCalled]() { bBeforeResponseCalled = true; - Assert::IsTrue(game.runtime.IsPaused()); + Assert::IsTrue(game.mockAchievementRuntime.IsPaused()); }); - game.runtime.SetPaused(true); + game.mockAchievementRuntime.SetPaused(true); game.LoadGame(1U, "0123456789abcdeffedcba987654321"); - Assert::IsTrue(bBeforeResponseCalled); // paused during load - Assert::IsTrue(game.runtime.IsPaused()); // paused after load + Assert::IsTrue(bBeforeResponseCalled); // paused during load + Assert::IsTrue(game.mockAchievementRuntime.IsPaused()); // paused after load } TEST_METHOD(TestReloadRichPresenceScriptNoFile) @@ -1195,1327 +1192,6 @@ public: Assert::IsFalse(game.IsRichPresenceFromFile()); } - TEST_METHOD(TestAwardAchievementNonExistant) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - game.AwardAchievement(1U); - - Assert::IsFalse(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - Assert::IsFalse(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - Assert::IsNull(game.mockOverlayManager.GetMessage(1)); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievement) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.SetGameHash("hash"); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - Assert::AreEqual(1U, request.AchievementId); - Assert::AreEqual(false, request.Hardcore); - Assert::AreEqual(std::string("hash"), request.GameHash); - - response.NewPlayerScore = 125U; - response.Result = ra::api::ApiResult::Success; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(125U, game.mockUser.GetScore()); - } - - TEST_METHOD(TestAwardAchievementHardcore) - { - GameContextHarness game; - game.SetGameHash("hash"); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - Assert::AreEqual(1U, request.AchievementId); - Assert::AreEqual(true, request.Hardcore); - Assert::AreEqual(std::string("hash"), request.GameHash); - - response.NewPlayerScore = 125U; - response.Result = ra::api::ApiResult::Success; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(125U, game.mockUser.GetScore()); - } - - TEST_METHOD(TestAwardAchievementNoPopup) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::None); - game.SetGameHash("hash"); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - Assert::AreEqual(1U, request.AchievementId); - Assert::AreEqual(false, request.Hardcore); - Assert::AreEqual(std::string("hash"), request.GameHash); - - response.NewPlayerScore = 125U; - response.Result = ra::api::ApiResult::Success; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - // sound should still be played - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - - // popup should not be displayed - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Assert::IsNull(pPopup); - - // API call should still occur - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(125U, game.mockUser.GetScore()); - } - - TEST_METHOD(TestAwardAchievementLocal) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetCategory(ra::data::models::AssetCategory::Local); - pAch.UpdateLocalCheckpoint(); - Assert::IsFalse(pAch.IsModified()); - - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Local Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementUnofficial) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetCategory(ra::data::models::AssetCategory::Unofficial); - pAch.UpdateServerCheckpoint(); - Assert::IsFalse(pAch.IsModified()); - - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Unofficial Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementModified) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetPoints(99); - Assert::IsTrue(pAch.IsModified()); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Modified Achievement Unlocked LOCALLY"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (99)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementModifiedLocal) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetCategory(ra::data::models::AssetCategory::Local); - pAch.SetPoints(99); - Assert::IsTrue(pAch.IsModified()); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Modified Local Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (99)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementModifiedUnofficial) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetCategory(ra::data::models::AssetCategory::Unofficial); - pAch.SetPoints(99); - Assert::IsTrue(pAch.IsModified()); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Modified Unofficial Achievement Unlocked LOCALLY"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (99)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementUnpublished) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - auto& pAch = game.MockAchievement(); - pAch.SetPoints(99); - pAch.UpdateLocalCheckpoint(); - Assert::IsFalse(pAch.IsModified()); - Assert::AreEqual(ra::data::models::AssetChanges::Unpublished, pAch.GetChanges()); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Modified Achievement Unlocked LOCALLY"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (99)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementMemoryModified) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - - game.MockAchievement(); - game.mockEmulator.MockMemoryModified(true); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked LOCALLY"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Error: RAM tampered with"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementMemoryInsecureHardcore) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockServer.ExpectUncalled(); - - game.MockAchievement(); - game.mockEmulator.MockMemoryInsecure(true); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked LOCALLY"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Error: RAM insecure"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementMemoryInsecureNonHardcore) - { - GameContextHarness game; - game.SetGameHash("hash"); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request& request, ra::api::AwardAchievement::Response& response) - { - Assert::AreEqual(1U, request.AchievementId); - Assert::AreEqual(false, request.Hardcore); - Assert::AreEqual(std::string("hash"), request.GameHash); - - response.NewPlayerScore = 125U; - response.Result = ra::api::ApiResult::Success; - return true; - }); - - game.MockAchievement(); - game.mockEmulator.MockMemoryInsecure(true); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementOffline) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Offline, true); - game.mockServer.ExpectUncalled(); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Offline Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::IsFalse(pPopup->IsDetailError()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementDuplicate) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.ErrorMessage = "User already has this achievement awarded."; - response.Result = ra::api::ApiResult::Error; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - game.mockThreadPool.ExecuteNextTask(); - - // special error for "already unlocked" should not be reported - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - } - - TEST_METHOD(TestAwardAchievementDuplicateHardcore) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.ErrorMessage = "User already has hardcore and regular achievements awarded."; - response.Result = ra::api::ApiResult::Error; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - game.mockThreadPool.ExecuteNextTask(); - - // special error for "already unlocked" should not be reported - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - } - - TEST_METHOD(TestAwardAchievementError) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.ErrorMessage = "Achievement data cannot be found for 1"; - response.Result = ra::api::ApiResult::Error; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - game.mockThreadPool.ExecuteNextTask(); - - // error message should be reported - Assert::AreEqual(std::wstring(L"Achievement Unlock FAILED"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Achievement data cannot be found for 1"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - } - - TEST_METHOD(TestAwardAchievementErrorAfterPopupDismissed) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.ErrorMessage = "Achievement data cannot be found for 1"; - response.Result = ra::api::ApiResult::Error; - return true; - }); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // if error occurs after original popup is gone, a new one should be created to display the error - ra::services::ServiceLocator::ServiceOverride contextOverride(&game, false); - game.mockOverlayManager.ClearPopups(); - game.mockThreadPool.ExecuteNextTask(); - - // error message should be reported - pPopup = game.mockOverlayManager.GetMessage(2); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlock FAILED"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Achievement data cannot be found for 1"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - } - - TEST_METHOD(TestAwardAchievementCompatibilityMode) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockServer.ExpectUncalled(); - game.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest); - - game.MockAchievement(); - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Test Achievement Unlocked"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"AchievementTitle (5)"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"AchievementDescription"), pPopup->GetDetail()); - Assert::AreEqual(std::string("12345"), pPopup->GetImage().Name()); - - // AwardAchievement API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestAwardAchievementMasteryHardcore) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.SetGameId(1U); - game.SetGameHash("hash"); - game.SetGameTitle(L"GameName"); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.Result = ra::api::ApiResult::Success; - response.AchievementsRemaining = 0; - return true; - }); - - game.mockConfiguration.SetUsername("Username"); - game.mockSessionTracker.MockSession(1, 123456789, std::chrono::seconds(78 * 60)); - - game.MockAchievement(); - auto& pAch2 = game.MockAchievement(); - pAch2.SetID(2U); - - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(2); - Assert::IsNull(pPopup); - - game.AwardAchievement(2U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - pPopup = game.mockOverlayManager.GetMessage(2); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(3); - Assert::IsNull(pPopup); - - game.mockThreadPool.ExecuteNextTask(); // award achievement 1 - game.mockThreadPool.ExecuteNextTask(); // award achievement 2 - game.mockThreadPool.ExecuteNextTask(); // check unlocks - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - pPopup = game.mockOverlayManager.GetMessage(3); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Mastered GameName"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"2 achievements, 10 points"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Username | Play time: 1h18m"), pPopup->GetDetail()); - - pPopup = game.mockOverlayManager.GetMessage(4); // mastery should only have been shown once - Expects(pPopup == nullptr); - } - - TEST_METHOD(TestAwardAchievementMasteryNonHardcore) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); - game.SetGameId(1U); - game.SetGameHash("hash"); - game.SetGameTitle(L"GameName"); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.Result = ra::api::ApiResult::Success; - response.AchievementsRemaining = 0; - return true; - }); - - game.mockConfiguration.SetUsername("Player"); - game.mockSessionTracker.MockSession(1, 123456789, std::chrono::seconds((102*60 + 3) * 60)); - - game.MockAchievement(); - auto& pAch2 = game.MockAchievement(); - pAch2.SetID(2U); - - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(2); - Assert::IsNull(pPopup); - - game.AwardAchievement(2U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - pPopup = game.mockOverlayManager.GetMessage(2); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(3); - Assert::IsNull(pPopup); - - game.mockThreadPool.ExecuteNextTask(); // award achievement 1 - game.mockThreadPool.ExecuteNextTask(); // award achievement 2 - game.mockThreadPool.ExecuteNextTask(); // check unlocks - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - pPopup = game.mockOverlayManager.GetMessage(3); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Completed GameName"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"2 achievements, 10 points"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Player | Play time: 102h03m"), pPopup->GetDetail()); - - pPopup = game.mockOverlayManager.GetMessage(4); // mastery should only have been shown once - Expects(pPopup == nullptr); - } - - TEST_METHOD(TestAwardAchievementMasteryIncomplete) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, ra::ui::viewmodels::PopupLocation::BottomLeft); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, ra::ui::viewmodels::PopupLocation::TopMiddle); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.SetGameHash("hash"); - game.SetGameTitle(L"GameName"); - game.mockServer.HandleRequest([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response) - { - response.Result = ra::api::ApiResult::Success; - response.AchievementsRemaining = 1; - return true; - }); - - game.MockAchievement(); - auto& pAch2 = game.MockAchievement(); - pAch2.SetID(2U); - - game.AwardAchievement(1U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(2); - Assert::IsNull(pPopup); - - game.AwardAchievement(2U); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); - pPopup = game.mockOverlayManager.GetMessage(2); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); - - pPopup = game.mockOverlayManager.GetMessage(3); - Assert::IsNull(pPopup); - - game.mockThreadPool.ExecuteNextTask(); // award achievement 1 - game.mockThreadPool.ExecuteNextTask(); // award achievement 2 - game.mockThreadPool.ExecuteNextTask(); // check unlocks - - // server indicates at least one achievement is not unlocked - reset by user? - pPopup = game.mockOverlayManager.GetMessage(3); - Assert::IsNull(pPopup); - } - - TEST_METHOD(TestSubmitLeaderboardEntryNonExistant) - { - GameContextHarness game; - game.mockServer.ExpectUncalled(); - - game.SubmitLeaderboardEntry(1U, 1234U); - - Assert::IsFalse(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - Assert::IsNull(game.mockOverlayManager.GetMessage(1)); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - } - - TEST_METHOD(TestSubmitLeaderboardEntry) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - unsigned int nNewScore = 0U; - game.mockServer.HandleRequest([&nNewScore] - (const ra::api::SubmitLeaderboardEntry::Request& request, ra::api::SubmitLeaderboardEntry::Response& response) - { - Assert::AreEqual(1U, request.LeaderboardId); - Assert::AreEqual(1234, request.Score); - Assert::AreEqual(std::string("hash"), request.GameHash); - nNewScore = request.Score; - - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Player", 1234U }); - response.TopEntries.push_back({ 3, "Steve", 1500U }); - response.TopEntries.push_back({ 4, "Bill", 1700U }); - - response.Score = 1234U; - response.BestScore = 1234U; - response.NewRank = 2; - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(1234U, nNewScore); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 4U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry1 = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry1); - Ensures(vmEntry1 != nullptr); - Assert::AreEqual(1, vmEntry1->GetRank()); - Assert::AreEqual(std::wstring(L"George"), vmEntry1->GetUserName()); - Assert::AreEqual(std::wstring(L"1000"), vmEntry1->GetScore()); - Assert::IsFalse(vmEntry1->IsHighlighted()); - - const auto* vmEntry2 = vmScoreboard->Entries().GetItemAt(1); - Assert::IsNotNull(vmEntry2); - Ensures(vmEntry2 != nullptr); - Assert::AreEqual(2, vmEntry2->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry2->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry2->GetScore()); - Assert::IsTrue(vmEntry2->IsHighlighted()); - - const auto* vmEntry3 = vmScoreboard->Entries().GetItemAt(2); - Assert::IsNotNull(vmEntry3); - Ensures(vmEntry3 != nullptr); - Assert::AreEqual(3, vmEntry3->GetRank()); - Assert::AreEqual(std::wstring(L"Steve"), vmEntry3->GetUserName()); - Assert::AreEqual(std::wstring(L"1500"), vmEntry3->GetScore()); - Assert::IsFalse(vmEntry3->IsHighlighted()); - - const auto* vmEntry4 = vmScoreboard->Entries().GetItemAt(3); - Assert::IsNotNull(vmEntry4); - Ensures(vmEntry4 != nullptr); - Assert::AreEqual(4, vmEntry4->GetRank()); - Assert::AreEqual(std::wstring(L"Bill"), vmEntry4->GetUserName()); - Assert::AreEqual(std::wstring(L"1700"), vmEntry4->GetScore()); - Assert::IsFalse(vmEntry4->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryScoreboardDisabled) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::None); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - unsigned int nNewScore = 0U; - game.mockServer.HandleRequest([&nNewScore] - (const ra::api::SubmitLeaderboardEntry::Request& request, ra::api::SubmitLeaderboardEntry::Response& response) - { - nNewScore = request.Score; - - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Player", 1234U }); - response.TopEntries.push_back({ 3, "Steve", 1500U }); - response.TopEntries.push_back({ 4, "Bill", 1700U }); - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(1234U, nNewScore); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNull(vmScoreboard); - } - - TEST_METHOD(TestSubmitLeaderboardEntryNonHardcore) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Submission requires Hardcore mode"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryModified) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest); - game.SetGameHash("hash"); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.Assets().FindLeaderboard(1U)->SetName(L"Name2"); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Modified Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"Name2"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Leaderboards are not submitted in test mode."), pPopup->GetDetail()); - Assert::IsFalse(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"Name2"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryCompatibilityMode) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest); - game.SetGameHash("hash"); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Leaderboards are not submitted in test mode."), pPopup->GetDetail()); - Assert::IsFalse(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryLowRank) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - game.mockServer.HandleRequest([] - (const ra::api::SubmitLeaderboardEntry::Request&, ra::api::SubmitLeaderboardEntry::Response& response) - { - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Philip", 1100U }); - response.TopEntries.push_back({ 3, "Steve", 1200U }); - response.TopEntries.push_back({ 4, "Bill", 1300U }); - response.TopEntries.push_back({ 5, "Roger", 1400U }); - response.TopEntries.push_back({ 6, "Andy", 1500U }); - response.TopEntries.push_back({ 7, "Jason", 1600U }); - response.TopEntries.push_back({ 8, "Jeff", 1700U }); - - response.Score = 2000U; - response.BestScore = 1900U; - response.NewRank = 11; - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 2000U); - - game.mockThreadPool.ExecuteNextTask(); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 7U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry1 = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry1); - Ensures(vmEntry1 != nullptr); - Assert::AreEqual(1, vmEntry1->GetRank()); - Assert::AreEqual(std::wstring(L"George"), vmEntry1->GetUserName()); - Assert::AreEqual(std::wstring(L"1000"), vmEntry1->GetScore()); - Assert::IsFalse(vmEntry1->IsHighlighted()); - - const auto* vmEntry6 = vmScoreboard->Entries().GetItemAt(5); - Assert::IsNotNull(vmEntry6); - Ensures(vmEntry6 != nullptr); - Assert::AreEqual(6, vmEntry6->GetRank()); - Assert::AreEqual(std::wstring(L"Andy"), vmEntry6->GetUserName()); - Assert::AreEqual(std::wstring(L"1500"), vmEntry6->GetScore()); - Assert::IsFalse(vmEntry6->IsHighlighted()); - - const auto* vmEntry7 = vmScoreboard->Entries().GetItemAt(6); - Assert::IsNotNull(vmEntry7); - Ensures(vmEntry7 != nullptr); - Assert::AreEqual(11, vmEntry7->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry7->GetUserName()); - Assert::AreEqual(std::wstring(L"(2000) 1900"), vmEntry7->GetScore()); - Assert::IsTrue(vmEntry7->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryScoreNotImproved) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - unsigned int nNewScore = 0U; - game.mockServer.HandleRequest([&nNewScore] - (const ra::api::SubmitLeaderboardEntry::Request & request, ra::api::SubmitLeaderboardEntry::Response & response) - { - Assert::AreEqual(1U, request.LeaderboardId); - Assert::AreEqual(1234, request.Score); - Assert::AreEqual(std::string("hash"), request.GameHash); - nNewScore = request.Score; - - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Player", 1200U }); - response.TopEntries.push_back({ 3, "Steve", 1500U }); - response.TopEntries.push_back({ 4, "Bill", 1700U }); - - response.Score = 1234U; - response.BestScore = 1200U; - response.NewRank = 2; - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(1234U, nNewScore); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 4U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry2 = vmScoreboard->Entries().GetItemAt(1); - Assert::IsNotNull(vmEntry2); - Ensures(vmEntry2 != nullptr); - Assert::AreEqual(2, vmEntry2->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry2->GetUserName()); - Assert::AreEqual(std::wstring(L"(1234) 1200"), vmEntry2->GetScore()); - Assert::IsTrue(vmEntry2->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryScoreNotImprovedSharedRank) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - unsigned int nNewScore = 0U; - game.mockServer.HandleRequest([&nNewScore] - (const ra::api::SubmitLeaderboardEntry::Request& request, ra::api::SubmitLeaderboardEntry::Response& response) - { - Assert::AreEqual(1U, request.LeaderboardId); - Assert::AreEqual(1234, request.Score); - Assert::AreEqual(std::string("hash"), request.GameHash); - nNewScore = request.Score; - - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Steve", 1200U }); - response.TopEntries.push_back({ 2, "Player", 1200U }); - response.TopEntries.push_back({ 4, "Bill", 1700U }); - - response.Score = 1234U; - response.BestScore = 1200U; - response.NewRank = 2; - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(1234U, nNewScore); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 4U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry2 = vmScoreboard->Entries().GetItemAt(1); - Assert::IsNotNull(vmEntry2); - Ensures(vmEntry2 != nullptr); - Assert::AreEqual(2, vmEntry2->GetRank()); - Assert::AreEqual(std::wstring(L"Steve"), vmEntry2->GetUserName()); - Assert::AreEqual(std::wstring(L"1200"), vmEntry2->GetScore()); - Assert::IsFalse(vmEntry2->IsHighlighted()); - - const auto* vmEntry3 = vmScoreboard->Entries().GetItemAt(2); - Assert::IsNotNull(vmEntry3); - Ensures(vmEntry3 != nullptr); - Assert::AreEqual(2, vmEntry3->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry3->GetUserName()); - Assert::AreEqual(std::wstring(L"(1234) 1200"), vmEntry3->GetScore()); - Assert::IsTrue(vmEntry3->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryScoreNotImprovedSharedRankLastEntry) - { - GameContextHarness game; - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - unsigned int nNewScore = 0U; - game.mockServer.HandleRequest([&nNewScore] - (const ra::api::SubmitLeaderboardEntry::Request& request, ra::api::SubmitLeaderboardEntry::Response& response) - { - Assert::AreEqual(1U, request.LeaderboardId); - Assert::AreEqual(1234, request.Score); - Assert::AreEqual(std::string("hash"), request.GameHash); - nNewScore = request.Score; - - response.Result = ra::api::ApiResult::Success; - response.TopEntries.push_back({ 1, "George", 1000U }); - response.TopEntries.push_back({ 2, "Harold", 1100U }); - response.TopEntries.push_back({ 2, "Roger", 1100U }); - response.TopEntries.push_back({ 4, "Phil", 1150U }); - response.TopEntries.push_back({ 5, "Steve", 1175U }); - response.TopEntries.push_back({ 6, "Terry", 1200U }); - response.TopEntries.push_back({ 6, "Edward", 1200U }); - response.TopEntries.push_back({ 6, "Player", 1200U }); - response.TopEntries.push_back({ 9, "Bill", 1300U }); - - response.Score = 1234U; - response.BestScore = 1200U; - response.NewRank = 6; - return true; - }); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - game.mockThreadPool.ExecuteNextTask(); - Assert::AreEqual(1234U, nNewScore); - - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 7U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry6 = vmScoreboard->Entries().GetItemAt(5); - Assert::IsNotNull(vmEntry6); - Ensures(vmEntry6 != nullptr); - Assert::AreEqual(6, vmEntry6->GetRank()); - Assert::AreEqual(std::wstring(L"Terry"), vmEntry6->GetUserName()); - Assert::AreEqual(std::wstring(L"1200"), vmEntry6->GetScore()); - Assert::IsFalse(vmEntry6->IsHighlighted()); - - // player is entry 8, but rank 6. 7 items will be shown, 7th item should be replaced with player entry - const auto* vmEntry7 = vmScoreboard->Entries().GetItemAt(6); - Assert::IsNotNull(vmEntry7); - Ensures(vmEntry7 != nullptr); - Assert::AreEqual(6, vmEntry7->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry7->GetUserName()); - Assert::AreEqual(std::wstring(L"(1234) 1200"), vmEntry7->GetScore()); - Assert::IsTrue(vmEntry7->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryMemoryModified) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.mockEmulator.MockMemoryModified(true); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Error: RAM tampered with"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryMemoryInsecure) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", "ApiToken"); - game.SetGameHash("hash"); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.mockEmulator.MockMemoryInsecure(true); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Error: RAM insecure"), pPopup->GetDetail()); - Assert::IsTrue(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - - TEST_METHOD(TestSubmitLeaderboardEntryOffline) - { - GameContextHarness game; - game.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, ra::ui::viewmodels::PopupLocation::BottomRight); - game.mockUser.Initialize("player", "Player", ""); - game.SetGameHash("hash"); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); - game.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Offline, true); - - game.mockServer.ExpectUncalled(); - - game.MockLeaderboard(); - game.SubmitLeaderboardEntry(1U, 1234U); - - // SubmitLeaderboardEntry API call is async, try to execute it - expect no tasks queued - game.mockThreadPool.ExecuteNextTask(); - - Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); - - // error message should be reported - const auto* pPopup = game.mockOverlayManager.GetMessage(1); - Expects(pPopup != nullptr); - Assert::IsNotNull(pPopup); - Assert::AreEqual(std::wstring(L"Leaderboard NOT Submitted"), pPopup->GetTitle()); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), pPopup->GetDescription()); - Assert::AreEqual(std::wstring(L"Leaderboards are not submitted in offline mode."), pPopup->GetDetail()); - Assert::IsFalse(pPopup->IsDetailError()); - - // empty leaderboard should be displayed with the non-submitted score - const auto* vmScoreboard = game.mockOverlayManager.GetScoreboard(1U); - Assert::IsNotNull(vmScoreboard); - Ensures(vmScoreboard != nullptr); - Assert::AreEqual(std::wstring(L"LeaderboardTitle"), vmScoreboard->GetHeaderText()); - Assert::AreEqual({ 1U }, vmScoreboard->Entries().Count()); - - const auto* vmEntry = vmScoreboard->Entries().GetItemAt(0); - Assert::IsNotNull(vmEntry); - Ensures(vmEntry != nullptr); - Assert::AreEqual(0, vmEntry->GetRank()); - Assert::AreEqual(std::wstring(L"Player"), vmEntry->GetUserName()); - Assert::AreEqual(std::wstring(L"1234"), vmEntry->GetScore()); - Assert::IsTrue(vmEntry->IsHighlighted()); - } - TEST_METHOD(TestSetModeNotify) { class NotifyHarness : public GameContext::NotifyTarget diff --git a/tests/data/context/SessionTracker_Tests.cpp b/tests/data/context/SessionTracker_Tests.cpp index 95946d00..9e2ad39b 100644 --- a/tests/data/context/SessionTracker_Tests.cpp +++ b/tests/data/context/SessionTracker_Tests.cpp @@ -32,9 +32,9 @@ public: GSL_SUPPRESS_F6 SessionTrackerHarness(const char* sUsername = "User") noexcept : m_sUsername(sUsername), m_sUsernameWide(ra::Widen(sUsername)) { + mockRuntime.MockGame(); } - ra::api::mocks::MockServer mockServer; ra::data::context::mocks::MockGameContext mockGameContext; ra::services::mocks::MockAchievementRuntime mockRuntime; @@ -245,96 +245,6 @@ public: Assert::AreEqual(std::string("1234:1534000000:1732:f5\n1234:1534889323:173:bb\n9999:1534889496:169:14\n"), tracker.GetStoredData()); } - TEST_METHOD(TestPing) - { - SessionTrackerHarness tracker; - - tracker.Initialize("User"); - - int nPings = 0; - tracker.mockServer.HandleRequest([&nPings](const ra::api::Ping::Request& request, _UNUSED ra::api::Ping::Response& /*response*/) - { - Assert::AreEqual(1234U, request.GameId); - Assert::AreEqual(std::wstring(L"Playing Banana"), request.CurrentActivity); - ++nPings; - return false; - }); - - // BeginSession should begin the session, but not write to the file - tracker.mockGameContext.SetGameId(1234U); - tracker.mockGameContext.SetGameTitle(L"Banana"); - tracker.BeginSession(1234U); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call StartSession - Assert::AreEqual(0, nPings); - - // after 30 seconds, the callback will be called, and the first ping should occur - tracker.mockClock.AdvanceTime(std::chrono::seconds(30)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(30)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual({ 1U }, tracker.mockThreadPool.PendingTasks()); - Assert::AreEqual(1, nPings); - - // after two minutes, the callback will be called again, and the second ping should occur - tracker.mockClock.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual(2, nPings); - - // after two more minutes, the callback will be called again, and the third ping should occur - tracker.mockClock.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual(3, nPings); - - // once the session is ended, pinging should stop - tracker.EndSession(); - tracker.mockClock.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual(3, nPings); - } - - TEST_METHOD(TestPingUnknownGame) - { - SessionTrackerHarness tracker; - - tracker.Initialize("User"); - tracker.mockGameContext.SetGameId(0U); - tracker.mockGameContext.SetGameTitle(L"FileUnknown"); - - int nPings = 0; - tracker.mockServer.HandleRequest([&nPings](const ra::api::Ping::Request& request, _UNUSED ra::api::Ping::Response& /*response*/) - { - Assert::AreEqual(0U, request.GameId); - Assert::AreEqual(std::wstring(L"Playing FileUnknown"), request.CurrentActivity); - ++nPings; - return false; - }); - tracker.mockServer.ExpectUncalled(); - - // BeginSession should begin the session, but not send a start session - tracker.BeginSession(0U); - tracker.mockThreadPool.ExecuteNextTask(); // StartSession should not be queued - Assert::AreEqual(0, nPings); - - // after 30 seconds, the callback will be called, and the first ping should occur - tracker.mockClock.AdvanceTime(std::chrono::seconds(30)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(30)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual({ 1U }, tracker.mockThreadPool.PendingTasks()); - Assert::AreEqual(1, nPings); - - // after two minutes, the callback will be called again, and the second ping should occur - tracker.mockClock.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.AdvanceTime(std::chrono::seconds(120)); - tracker.mockThreadPool.ExecuteNextTask(); // execute async server call - Assert::AreEqual(2, nPings); - - // total playtime should still be tallied, but not written to the file - Assert::AreEqual(150, gsl::narrow(tracker.GetTotalPlaytime(0U).count())); - Assert::AreEqual(std::string(""), tracker.GetStoredData()); - } - TEST_METHOD(TestCurrentActivityNoRichPresence) { SessionTrackerHarness tracker; diff --git a/tests/data/models/AchievementModel_Tests.cpp b/tests/data/models/AchievementModel_Tests.cpp index 995428a3..16c98834 100644 --- a/tests/data/models/AchievementModel_Tests.cpp +++ b/tests/data/models/AchievementModel_Tests.cpp @@ -25,6 +25,11 @@ private: class AchievementModelHarness : public AchievementModel { public: + AchievementModelHarness() + { + mockRuntime.MockGame(); + } + ra::data::context::mocks::MockGameContext mockGameContext; ra::services::impl::StringTextWriter textWriter; ra::services::mocks::MockAchievementRuntime mockRuntime; @@ -63,141 +68,6 @@ public: Assert::AreEqual(std::string(""), achievement.GetTrigger()); } - TEST_METHOD(TestActivateDeactivate) - { - AchievementModelHarness achievement; - achievement.SetID(1U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - Assert::IsNull(achievement.mockRuntime.GetAchievementTrigger(1U)); - - achievement.Activate(); - - Assert::AreEqual(AssetState::Waiting, achievement.GetState()); - const auto* pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual((int)RC_TRIGGER_STATE_WAITING, (int)pTrigger->state); - - achievement.Deactivate(); - - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNull(pTrigger); - } - - TEST_METHOD(TestActivateDeactivateActive) - { - AchievementModelHarness achievement; - achievement.SetID(1U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - - auto* pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Expects(pTrigger != nullptr); - pTrigger->state = RC_TRIGGER_STATE_ACTIVE; - achievement.DoFrame(); - Assert::AreEqual(AssetState::Active, achievement.GetState()); - - achievement.Activate(); - Assert::AreEqual(AssetState::Active, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pTrigger->state); - - achievement.Deactivate(); - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNull(pTrigger); - } - - TEST_METHOD(TestActivateDeactivatePaused) - { - AchievementModelHarness achievement; - achievement.SetID(1U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - - auto* pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Expects(pTrigger != nullptr); - pTrigger->state = RC_TRIGGER_STATE_PAUSED; - achievement.DoFrame(); - Assert::AreEqual(AssetState::Paused, achievement.GetState()); - - achievement.Activate(); - Assert::AreEqual(AssetState::Paused, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual((int)RC_TRIGGER_STATE_PAUSED, (int)pTrigger->state); - - achievement.Deactivate(); - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNull(pTrigger); - Assert::AreEqual(std::chrono::system_clock::duration::zero().count(), achievement.GetUnlockTime().time_since_epoch().count()); - } - - TEST_METHOD(TestActivateDeactivateTriggered) - { - AchievementModelHarness achievement; - achievement.SetID(1U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - - auto* pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Expects(pTrigger != nullptr); - pTrigger->state = RC_TRIGGER_STATE_TRIGGERED; - - // first DoFrame will set State to Triggered, which deactivates the achievement - achievement.DoFrame(); - Assert::AreEqual(AssetState::Triggered, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNull(pTrigger); - Assert::AreNotEqual(std::chrono::system_clock::duration::zero().count(), achievement.GetUnlockTime().time_since_epoch().count()); - - // second DoFrame won't be able to find the trigger, but should not modify the State - achievement.DoFrame(); - Assert::AreEqual(AssetState::Triggered, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNull(pTrigger); - - // reactivating the achievement will set it back to waiting, but not reset the unlock time - achievement.Activate(); - Assert::AreEqual(AssetState::Waiting, achievement.GetState()); - pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual((int)RC_TRIGGER_STATE_WAITING, (int)pTrigger->state); - Assert::AreNotEqual(std::chrono::system_clock::duration::zero().count(), achievement.GetUnlockTime().time_since_epoch().count()); - } - - TEST_METHOD(TestStatePrimed) - { - AchievementModelHarness achievement; - achievement.SetID(1U); - achievement.SetTrigger("0xH1234=1_T:0xH1234=2"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - - auto* pTrigger = achievement.mockRuntime.GetAchievementTrigger(1U); - Assert::IsNotNull(pTrigger); - Expects(pTrigger != nullptr); - pTrigger->state = RC_TRIGGER_STATE_PRIMED; - achievement.DoFrame(); - Assert::AreEqual(AssetState::Primed, achievement.GetState()); - } - TEST_METHOD(TestSetBadgeNameLocal) { AchievementModelHarness achievement; diff --git a/tests/data/models/LeaderboardModel_Tests.cpp b/tests/data/models/LeaderboardModel_Tests.cpp index bedc77b8..748976e0 100644 --- a/tests/data/models/LeaderboardModel_Tests.cpp +++ b/tests/data/models/LeaderboardModel_Tests.cpp @@ -46,152 +46,6 @@ public: Assert::AreEqual(std::string(""), leaderboard.GetValueDefinition()); } - TEST_METHOD(TestActivateDeactivate) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - - Assert::AreEqual(std::string("0xH1234=1"), leaderboard.GetStartTrigger()); - Assert::AreEqual(std::string("0xH1234=2"), leaderboard.GetSubmitTrigger()); - Assert::AreEqual(std::string("0xH1234=3"), leaderboard.GetCancelTrigger()); - Assert::AreEqual(std::string("0xH1234"), leaderboard.GetValueDefinition()); - - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - Assert::IsNull(leaderboard.mockRuntime.GetLeaderboardDefinition(1U)); - - leaderboard.Activate(); - - Assert::AreEqual(AssetState::Waiting, leaderboard.GetState()); - const auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Assert::AreEqual((int)RC_LBOARD_STATE_WAITING, (int)pLeaderboard->state); - Assert::AreEqual(1U, pLeaderboard->start.requirement->conditions->operand2.value.num); - Assert::AreEqual(2U, pLeaderboard->submit.requirement->conditions->operand2.value.num); - Assert::AreEqual(3U, pLeaderboard->cancel.requirement->conditions->operand2.value.num); - - leaderboard.Deactivate(); - - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNull(pLeaderboard); - } - - TEST_METHOD(TestActivateDeactivateActive) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - - auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Expects(pLeaderboard != nullptr); - pLeaderboard->state = RC_LBOARD_STATE_ACTIVE; - leaderboard.DoFrame(); - Assert::AreEqual(AssetState::Active, leaderboard.GetState()); - - leaderboard.Activate(); - Assert::AreEqual(AssetState::Active, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Assert::AreEqual((int)RC_LBOARD_STATE_ACTIVE, (int)pLeaderboard->state); - - leaderboard.Deactivate(); - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNull(pLeaderboard); - } - - TEST_METHOD(TestActivateDeactivateCanceled) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - - // canceled means the leaderboard was primed, but was canceled before submitting. - // it should still be active, waiting for the start condition to become false. - auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Expects(pLeaderboard != nullptr); - pLeaderboard->state = RC_LBOARD_STATE_CANCELED; - leaderboard.DoFrame(); - Assert::AreEqual(AssetState::Waiting, leaderboard.GetState()); - - leaderboard.Activate(); - Assert::AreEqual(AssetState::Waiting, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Assert::AreEqual((int)RC_LBOARD_STATE_CANCELED, (int)pLeaderboard->state); - - leaderboard.Deactivate(); - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNull(pLeaderboard); - } - - TEST_METHOD(TestActivateDeactivateTriggered) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - - // triggered means the leaderboard was submitted. - // it should still be active, waiting for the start condition to become false - auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Expects(pLeaderboard != nullptr); - pLeaderboard->state = RC_LBOARD_STATE_TRIGGERED; - leaderboard.DoFrame(); - Assert::AreEqual(AssetState::Waiting, leaderboard.GetState()); - - leaderboard.Activate(); - Assert::AreEqual(AssetState::Waiting, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Assert::AreEqual((int)RC_LBOARD_STATE_TRIGGERED, (int)pLeaderboard->state); - - leaderboard.Deactivate(); - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNull(pLeaderboard); - } - - TEST_METHOD(TestActivateDeactivatePrimed) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - - // started means the leaderboard is engaged (showing on screen) - auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Expects(pLeaderboard != nullptr); - pLeaderboard->state = RC_LBOARD_STATE_STARTED; - leaderboard.DoFrame(); - Assert::AreEqual(AssetState::Primed, leaderboard.GetState()); - - leaderboard.Activate(); - Assert::AreEqual(AssetState::Primed, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Assert::AreEqual((int)RC_LBOARD_STATE_STARTED, (int)pLeaderboard->state); - - leaderboard.Deactivate(); - Assert::AreEqual(AssetState::Inactive, leaderboard.GetState()); - pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNull(pLeaderboard); - } - TEST_METHOD(TestDeserialize) { LeaderboardModelHarness leaderboard; @@ -336,106 +190,6 @@ public: Assert::AreEqual(AssetChanges::None, leaderboard.GetChanges()); } - TEST_METHOD(TestSetStartTrigger) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetName(L"Title"); - leaderboard.SetDescription(L"Desc"); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.SetValueFormat(ValueFormat::Value); - leaderboard.SetLowerIsBetter(true); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - Assert::AreEqual(AssetChanges::None, leaderboard.GetChanges()); - - // modify - leaderboard.SetStartTrigger("0xH5555=55"); - Assert::AreEqual(AssetChanges::Modified, leaderboard.GetChanges()); - - // ensure runtime is updated - const auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Ensures(pLeaderboard != nullptr); - Assert::AreEqual(0x5555U, pLeaderboard->start.requirement->conditions->operand1.value.memref->address); - } - - TEST_METHOD(TestSetSubmitTrigger) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetName(L"Title"); - leaderboard.SetDescription(L"Desc"); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.SetValueFormat(ValueFormat::Value); - leaderboard.SetLowerIsBetter(true); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - Assert::AreEqual(AssetChanges::None, leaderboard.GetChanges()); - - // modify - leaderboard.SetSubmitTrigger("0xH5555=55"); - Assert::AreEqual(AssetChanges::Modified, leaderboard.GetChanges()); - - // ensure runtime is updated - const auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Ensures(pLeaderboard != nullptr); - Assert::AreEqual(0x5555U, pLeaderboard->submit.requirement->conditions->operand1.value.memref->address); - } - - TEST_METHOD(TestSetCancelTrigger) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetName(L"Title"); - leaderboard.SetDescription(L"Desc"); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.SetValueFormat(ValueFormat::Value); - leaderboard.SetLowerIsBetter(true); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - Assert::AreEqual(AssetChanges::None, leaderboard.GetChanges()); - - // modify - leaderboard.SetCancelTrigger("0xH5555=55"); - Assert::AreEqual(AssetChanges::Modified, leaderboard.GetChanges()); - - // ensure runtime is updated - const auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Ensures(pLeaderboard != nullptr); - Assert::AreEqual(0x5555U, pLeaderboard->cancel.requirement->conditions->operand1.value.memref->address); - } - - TEST_METHOD(TestSetValueDefinition) - { - LeaderboardModelHarness leaderboard; - leaderboard.SetID(1U); - leaderboard.SetName(L"Title"); - leaderboard.SetDescription(L"Desc"); - leaderboard.SetDefinition("STA:0xH1234=1::SUB:0xH1234=2::CAN:0xH1234=3::VAL:0xH1234"); - leaderboard.SetValueFormat(ValueFormat::Value); - leaderboard.SetLowerIsBetter(true); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); - Assert::AreEqual(AssetChanges::None, leaderboard.GetChanges()); - - // modify - leaderboard.SetValueDefinition("M:0xH5555"); - Assert::AreEqual(AssetChanges::Modified, leaderboard.GetChanges()); - - // ensure runtime is updated - const auto* pLeaderboard = leaderboard.mockRuntime.GetLeaderboardDefinition(1U); - Assert::IsNotNull(pLeaderboard); - Ensures(pLeaderboard != nullptr); - Assert::AreEqual(0x5555U, pLeaderboard->value.conditions->conditions->operand1.value.memref->address); - } - TEST_METHOD(TestValidateMissingField) { LeaderboardModelHarness leaderboard; diff --git a/tests/data/models/RichPresenceModel_Tests.cpp b/tests/data/models/RichPresenceModel_Tests.cpp index ddfb6bf1..4ed7ab33 100644 --- a/tests/data/models/RichPresenceModel_Tests.cpp +++ b/tests/data/models/RichPresenceModel_Tests.cpp @@ -24,6 +24,11 @@ private: class RichPresenceModelHarness : public RichPresenceModel { public: + RichPresenceModelHarness() + { + mockRuntime.MockGame(); + } + ra::data::context::mocks::MockGameContext mockGameContext; ra::services::mocks::MockLocalStorage mockLocalStorage; ra::services::impl::StringTextWriter textWriter; @@ -45,69 +50,6 @@ public: Assert::AreEqual(std::string(""), richPresence.GetScript()); } - TEST_METHOD(TestActivateDeactivate) - { - RichPresenceModelHarness richPresence; - richPresence.SetScript("Display:\nTest\n"); - richPresence.CreateServerCheckpoint(); - richPresence.CreateLocalCheckpoint(); - - Assert::AreEqual(std::string("Display:\nTest\n"), richPresence.GetScript()); - - Assert::AreEqual(AssetState::Inactive, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"No Rich Presence defined."), richPresence.mockRuntime.GetRichPresenceDisplayString()); - - richPresence.Activate(); - - Assert::AreEqual(AssetState::Active, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"Test"), richPresence.mockRuntime.GetRichPresenceDisplayString()); - - richPresence.Deactivate(); - - Assert::AreEqual(AssetState::Inactive, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"No Rich Presence defined."), richPresence.mockRuntime.GetRichPresenceDisplayString()); - } - - TEST_METHOD(TestChangeWhileActive) - { - RichPresenceModelHarness richPresence; - richPresence.SetScript("Display:\nTest\n"); - richPresence.CreateServerCheckpoint(); - richPresence.CreateLocalCheckpoint(); - richPresence.Activate(); - - Assert::AreEqual(AssetState::Active, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"Test"), richPresence.mockRuntime.GetRichPresenceDisplayString()); - - richPresence.SetScript("Display:\nNew test\n"); - - Assert::AreEqual(AssetState::Active, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"New test"), richPresence.mockRuntime.GetRichPresenceDisplayString()); - } - - TEST_METHOD(TestActivateInvalid) - { - RichPresenceModelHarness richPresence; - richPresence.SetScript("Test\n"); - richPresence.CreateServerCheckpoint(); - richPresence.CreateLocalCheckpoint(); - - Assert::AreEqual(std::string("Test\n"), richPresence.GetScript()); - - Assert::AreEqual(AssetState::Inactive, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"No Rich Presence defined."), richPresence.mockRuntime.GetRichPresenceDisplayString()); - - richPresence.Activate(); - - Assert::AreEqual(AssetState::Disabled, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"Parse error -18 (line 1): Missing display string"), richPresence.mockRuntime.GetRichPresenceDisplayString()); - - richPresence.SetScript("Display:\nTest\n"); - - Assert::AreEqual(AssetState::Active, richPresence.GetState()); - Assert::AreEqual(std::wstring(L"Test"), richPresence.mockRuntime.GetRichPresenceDisplayString()); - } - TEST_METHOD(TestNormalizeScript) { RichPresenceModelHarness richPresence; diff --git a/tests/mocks/MockRcheevosClient.cpp b/tests/mocks/MockAchievementRuntime.cpp similarity index 58% rename from tests/mocks/MockRcheevosClient.cpp rename to tests/mocks/MockAchievementRuntime.cpp index d6349ff8..5b892943 100644 --- a/tests/mocks/MockRcheevosClient.cpp +++ b/tests/mocks/MockAchievementRuntime.cpp @@ -1,4 +1,7 @@ -#include "MockRcheevosClient.hh" +#include "MockAchievementRuntime.hh" + +#include "MockEmulatorContext.hh" +#include "MockGameContext.hh" #include "services\ServiceLocator.hh" #include "CppUnitTest.h" @@ -13,7 +16,7 @@ namespace ra { namespace services { namespace mocks { -void MockRcheevosClient::OnBeforeResponse(const std::string& sRequestParams, std::function&& fHandler) +void MockAchievementRuntime::OnBeforeResponse(const std::string& sRequestParams, std::function&& fHandler) { for (auto& pResponse : m_vResponses) { @@ -28,7 +31,7 @@ void MockRcheevosClient::OnBeforeResponse(const std::string& sRequestParams, std ra::StringPrintf(L"No response registered for: %s", sRequestParams).c_str()); } -void MockRcheevosClient::MockUser(const std::string& sUsername, const std::string& sApiToken) +void MockAchievementRuntime::MockUser(const std::string& sUsername, const std::string& sApiToken) { m_sUsername = sUsername; m_sApiToken = sApiToken; @@ -38,7 +41,7 @@ void MockRcheevosClient::MockUser(const std::string& sUsername, const std::strin GetClient()->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; } -void MockRcheevosClient::MockGame() +void MockAchievementRuntime::MockGame() { rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(rc_client_game_info_t)); Expects(game != nullptr); @@ -126,28 +129,72 @@ static rc_client_achievement_info_t* AddAchievement(rc_client_game_info_t* game, achievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; achievement->public_.points = 5; + achievement->author = "Author"; return achievement; } -rc_client_achievement_info_t* MockRcheevosClient::MockAchievement(uint32_t nId, const char* sTitle) +rc_client_achievement_info_t* MockAchievementRuntime::MockAchievement(uint32_t nId, const char* sTitle) { rc_client_game_info_t* game = GetClient()->game; return AddAchievement(game, GetCoreSubset(game), nId, sTitle); } -rc_client_achievement_info_t* MockRcheevosClient::MockAchievementWithTrigger(uint32_t nId, const char* sTitle) +rc_client_achievement_info_t* MockAchievementRuntime::MockAchievementWithTrigger(uint32_t nId, const char* sTitle) { rc_client_game_info_t* game = GetClient()->game; rc_client_achievement_info_t* achievement = AddAchievement(game, GetCoreSubset(game), nId, sTitle); achievement->trigger = (rc_trigger_t*)rc_buf_alloc(&game->buffer, sizeof(rc_trigger_t)); memset(achievement->trigger, 0, sizeof(*achievement->trigger)); + achievement->trigger->state = RC_TRIGGER_STATE_ACTIVE; return achievement; } -rc_client_achievement_info_t* MockRcheevosClient::MockUnofficialAchievement(uint32_t nId, const char* sTitle) +rc_client_achievement_info_t* MockAchievementRuntime::ActivateAchievement(uint32_t nId, const std::string& sTrigger) +{ + rc_client_game_info_t* game = GetClient()->game; + rc_client_achievement_info_t* achievement = AddAchievement(game, GetCoreSubset(game), nId, "Achievement"); + + m_mAchievementDefinitions[nId] = sTrigger; + + auto nSize = rc_trigger_size(sTrigger.c_str()); + void* trigger_buffer = rc_buf_alloc(&game->buffer, nSize); + achievement->trigger = rc_parse_trigger(trigger_buffer, sTrigger.c_str(), nullptr, 0); + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + + return achievement; +} + +void MockAchievementRuntime::UnlockAchievement(rc_client_achievement_info_t* pAchievement, int nMode) +{ + pAchievement->public_.unlocked |= nMode; + + if (pAchievement->unlock_time_softcore == 0) + pAchievement->unlock_time_softcore = time(nullptr); + + if (nMode == RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH) + { + if (pAchievement->unlock_time_hardcore == 0) + pAchievement->unlock_time_hardcore = time(nullptr); + + pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + + if (pAchievement->trigger) + pAchievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } + else if (nMode == RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE && + !rc_client_get_hardcore_enabled(GetClient())) + { + pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + + if (pAchievement->trigger) + pAchievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } +} + +rc_client_achievement_info_t* MockAchievementRuntime::MockUnofficialAchievement(uint32_t nId, const char* sTitle) { rc_client_game_info_t* game = GetClient()->game; rc_client_achievement_info_t* achievement = AddAchievement(game, GetCoreSubset(game), nId, sTitle); @@ -155,14 +202,14 @@ rc_client_achievement_info_t* MockRcheevosClient::MockUnofficialAchievement(uint return achievement; } -rc_client_achievement_info_t* MockRcheevosClient::MockLocalAchievement(uint32_t nId, const char* sTitle) +rc_client_achievement_info_t* MockAchievementRuntime::MockLocalAchievement(uint32_t nId, const char* sTitle) { rc_client_game_info_t* game = GetClient()->game; return AddAchievement(game, GetLocalSubset(game), nId, sTitle); } -static rc_client_leaderboard_info_t* AddLeaderboard(rc_client_game_info_t* game, rc_client_subset_info_t* subset, - uint32_t nId, const char* sTitle) +static rc_client_leaderboard_info_t* AddLeaderboard(rc_client_t* client, rc_client_game_info_t* game, + rc_client_subset_info_t* subset, uint32_t nId, const char* sTitle) { if (subset->public_.num_leaderboards % 8 == 0) { @@ -196,24 +243,53 @@ static rc_client_leaderboard_info_t* AddLeaderboard(rc_client_game_info_t* game, const std::string sGeneratedDescripton = ra::StringPrintf("Description %u", nId); leaderboard->public_.description = rc_buf_strcpy(&game->buffer, sGeneratedDescripton.c_str()); + leaderboard->public_.state = static_cast(rc_client_get_hardcore_enabled(client) ? + RC_CLIENT_LEADERBOARD_STATE_ACTIVE : RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + + return leaderboard; +} + +rc_client_leaderboard_info_t* MockAchievementRuntime::MockLeaderboard(uint32_t nId, const char* sTitle) +{ + rc_client_game_info_t* game = GetClient()->game; + return AddLeaderboard(GetClient(), game, GetCoreSubset(game), nId, sTitle); +} + +rc_client_leaderboard_info_t* MockAchievementRuntime::MockLeaderboardWithLboard(uint32_t nId, const char* sTitle) +{ + rc_client_game_info_t* game = GetClient()->game; + rc_client_leaderboard_info_t* leaderboard = AddLeaderboard(GetClient(), game, GetCoreSubset(game), nId, sTitle); + + leaderboard->lboard = (rc_lboard_t*)rc_buf_alloc(&game->buffer, sizeof(rc_lboard_t)); + memset(leaderboard->lboard, 0, sizeof(*leaderboard->lboard)); + leaderboard->lboard->state = static_cast( + rc_client_get_hardcore_enabled(GetClient()) ? RC_LBOARD_STATE_ACTIVE : RC_LBOARD_STATE_INACTIVE); + + return leaderboard; +} + +rc_client_leaderboard_info_t* MockAchievementRuntime::MockLocalLeaderboard(uint32_t nId, const char* sTitle) +{ + rc_client_game_info_t* game = GetClient()->game; + return AddLeaderboard(GetClient(), game, GetLocalSubset(game), nId, sTitle); +} + +rc_client_leaderboard_info_t* MockAchievementRuntime::ActivateLeaderboard(uint32_t nId, const std::string& sDefinition) +{ + rc_client_game_info_t* game = GetClient()->game; + rc_client_leaderboard_info_t* leaderboard = AddLeaderboard(GetClient(), game, GetCoreSubset(game), nId, "Leaderboard"); + + m_mLeaderboardDefinitions[nId] = sDefinition; + + auto nSize = rc_lboard_size(sDefinition.c_str()); + void* trigger_buffer = rc_buf_alloc(&game->buffer, nSize); + leaderboard->lboard = rc_parse_lboard(trigger_buffer, sDefinition.c_str(), nullptr, 0); leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; return leaderboard; } -rc_client_leaderboard_info_t* MockRcheevosClient::MockLeaderboard(uint32_t nId, const char* sTitle) -{ - rc_client_game_info_t* game = GetClient()->game; - return AddLeaderboard(game, GetCoreSubset(game), nId, sTitle); -} - -rc_client_leaderboard_info_t* MockRcheevosClient::MockLocalLeaderboard(uint32_t nId, const char* sTitle) -{ - rc_client_game_info_t* game = GetClient()->game; - return AddLeaderboard(game, GetLocalSubset(game), nId, sTitle); -} - -void MockRcheevosClient::AssertCalled(const std::string& sRequestParams) const +void MockAchievementRuntime::AssertCalled(const std::string& sRequestParams) const { for (auto& pResponse : m_vResponses) { @@ -228,7 +304,7 @@ void MockRcheevosClient::AssertCalled(const std::string& sRequestParams) const ra::StringPrintf(L"Could not find mock response for %s", sRequestParams).c_str()); } -void MockRcheevosClient::AssertNoPendingRequests() const +void MockAchievementRuntime::AssertNoPendingRequests() const { for (auto& pResponse : m_vResponses) { @@ -241,10 +317,10 @@ void MockRcheevosClient::AssertNoPendingRequests() const } } -void MockRcheevosClient::MockServerCall(const rc_api_request_t* pRequest, +void MockAchievementRuntime::MockServerCall(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, void* pCallbackData, rc_client_t*) { - auto* pClient = dynamic_cast(&ra::services::ServiceLocator::GetMutable()); + auto* pClient = dynamic_cast(&ra::services::ServiceLocator::GetMutable()); std::string sRequestParams = pRequest->post_data; for (auto& pResponse : pClient->m_vResponses) @@ -275,4 +351,41 @@ void MockRcheevosClient::MockServerCall(const rc_api_request_t* pRequest, } // namespace mocks } // namespace services + +namespace data { +namespace context { +namespace mocks { + +void MockEmulatorContext::SetRuntimeMemorySize(size_t nBytes) +{ + if (ra::services::ServiceLocator::Exists()) + { + auto* pGame = ra::services::ServiceLocator::Get().GetClient()->game; + if (pGame && pGame->max_valid_address == 0U) + pGame->max_valid_address = gsl::narrow_cast(nBytes); + } +} + +void MockGameContext::InitializeFromAchievementRuntime() +{ + auto* pMockRuntime = dynamic_cast( + &ra::services::ServiceLocator::GetMutable()); + + if (pMockRuntime != nullptr) + { + GameContext::InitializeFromAchievementRuntime(pMockRuntime->GetAchievementDefinitions(), + pMockRuntime->GetLeaderboardDefinitions()); + } + else + { + std::map mAchievementDefinitions; + std::map mLeaderboardDefinitions; + GameContext::InitializeFromAchievementRuntime(mAchievementDefinitions, mLeaderboardDefinitions); + } +} + +} // namespace mocks +} // namespace context +} // namespace data } // namespace ra + diff --git a/tests/mocks/MockAchievementRuntime.hh b/tests/mocks/MockAchievementRuntime.hh index 36dec0ff..4eaf12ec 100644 --- a/tests/mocks/MockAchievementRuntime.hh +++ b/tests/mocks/MockAchievementRuntime.hh @@ -5,6 +5,8 @@ #include "services\AchievementRuntime.hh" #include "services\ServiceLocator.hh" +#include + namespace ra { namespace services { namespace mocks { @@ -14,25 +16,78 @@ class MockAchievementRuntime : public AchievementRuntime public: MockAchievementRuntime() noexcept : m_Override(this) { + /* intercept server requests */ + GetClient()->callbacks.server_call = MockServerCall; + + /* disable logging */ + rc_client_enable_logging(GetClient(), RC_CLIENT_LOG_LEVEL_NONE, nullptr); } bool IsAchievementSupported(ra::AchievementID nAchievement) noexcept { - for (unsigned i = 0; i < m_pRuntime.trigger_count; ++i) - { - if (m_pRuntime.triggers[i].id == nAchievement) - { - const auto* pTrigger = m_pRuntime.triggers[i].trigger; - if (pTrigger) - return pTrigger->state != RC_TRIGGER_STATE_DISABLED && !m_pRuntime.triggers[i].invalid_memref; - } - } - - return false; + const auto* pAchievement = GetAchievementTrigger(nAchievement); + return (pAchievement != nullptr && pAchievement->state != RC_TRIGGER_STATE_DISABLED); } + void MockResponse(const std::string& sRequestParams, const std::string& sResponseBody) + { + auto& pResponse = m_vResponses.emplace_back(); + pResponse.sRequestParams = sRequestParams; + pResponse.sResponseBody = sResponseBody; + pResponse.nHttpStatus = 200; + } + + void OnBeforeResponse(const std::string& sRequestParams, std::function&& fHandler); + + void MockUser(const std::string& sUsername, const std::string& sApiToken); + + void MockGame(); + + rc_client_achievement_info_t* MockAchievement(uint32_t nId, const char* sTitle = nullptr); + rc_client_achievement_info_t* MockAchievementWithTrigger(uint32_t nId, const char* sTitle = nullptr); + rc_client_achievement_info_t* MockUnofficialAchievement(uint32_t nId, const char* sTitle = nullptr); + rc_client_achievement_info_t* MockLocalAchievement(uint32_t nId, const char* sTitle = nullptr); + + rc_client_achievement_info_t* ActivateAchievement(uint32_t nId, const std::string& sTrigger); + void UnlockAchievement(rc_client_achievement_info_t* pAchievement, int nMode); + + rc_client_leaderboard_info_t* MockLeaderboard(uint32_t nId, const char* sTitle = nullptr); + rc_client_leaderboard_info_t* MockLeaderboardWithLboard(uint32_t nId, const char* sTitle = nullptr); + rc_client_leaderboard_info_t* MockLocalLeaderboard(uint32_t nId, const char* sTitle = nullptr); + + rc_client_leaderboard_info_t* ActivateLeaderboard(uint32_t nId, const std::string& sDefinition); + + void AssertCalled(const std::string& sRequestParams) const; + + void AssertNoPendingRequests() const; + + std::map& GetAchievementDefinitions() { return m_mAchievementDefinitions; } + std::map& GetLeaderboardDefinitions() { return m_mLeaderboardDefinitions; } + private: + static void MockServerCall(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, + void* pCallbackData, rc_client_t*); + ra::services::ServiceLocator::ServiceOverride m_Override; + + typedef struct MockApiResponse + { + std::string sRequestParams; + std::string sResponseBody; + int nHttpStatus = 0; + bool bSeen = 0; + rc_client_server_callback_t fAsyncCallback = nullptr; + void* pAsyncCallbackData = nullptr; + std::function fBeforeResponse = nullptr; + } MockApiResponse; + + std::vector m_vResponses; + + std::map m_mAchievementDefinitions; + std::map m_mLeaderboardDefinitions; + + std::string m_sUsername; + std::string m_sApiToken; }; } // namespace mocks diff --git a/tests/mocks/MockEmulatorContext.hh b/tests/mocks/MockEmulatorContext.hh index b3bb57c1..b89a3947 100644 --- a/tests/mocks/MockEmulatorContext.hh +++ b/tests/mocks/MockEmulatorContext.hh @@ -87,6 +87,8 @@ public: ClearMemoryBlocks(); AddMemoryBlock(0, s_pMemory.size_bytes(), ReadMemoryHelper, WriteMemoryHelper); + + SetRuntimeMemorySize(s_pMemory.size_bytes()); } void MockMemory(unsigned char pMemory[], size_t nBytes) @@ -95,6 +97,8 @@ public: ClearMemoryBlocks(); AddMemoryBlock(0, nBytes, ReadMemoryHelper, WriteMemoryHelper); + + SetRuntimeMemorySize(nBytes); } void MockMemoryModified(bool bModified) noexcept @@ -122,6 +126,8 @@ private: s_pMemory.at(nAddress) = nValue; } + static void SetRuntimeMemorySize(size_t nBytes); + bool m_bInvalid = false; std::string m_sWarnDisableHardcoreActivity; diff --git a/tests/mocks/MockGameContext.hh b/tests/mocks/MockGameContext.hh index d89e9c49..a346263c 100644 --- a/tests/mocks/MockGameContext.hh +++ b/tests/mocks/MockGameContext.hh @@ -115,6 +115,8 @@ public: return true; } + void InitializeFromAchievementRuntime(); + private: class MockCodeNotesModel : public ra::data::models::CodeNotesModel { diff --git a/tests/mocks/MockRcheevosClient.hh b/tests/mocks/MockRcheevosClient.hh deleted file mode 100644 index 3f16f89c..00000000 --- a/tests/mocks/MockRcheevosClient.hh +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef RA_SERVICES_MOCK_RCHEEVOS_CLIENT_HH -#define RA_SERVICES_MOCK_RCHEEVOS_CLIENT_HH -#pragma once - -#include "services\RcheevosClient.hh" -#include "services\ServiceLocator.hh" - -#include - -namespace ra { -namespace services { -namespace mocks { - -class MockRcheevosClient : public RcheevosClient -{ -public: - MockRcheevosClient() noexcept : m_Override(this) - { - /* intercept server requests */ - GetClient()->callbacks.server_call = MockRcheevosClient::MockServerCall; - - /* disable logging */ - rc_client_enable_logging(GetClient(), RC_CLIENT_LOG_LEVEL_NONE, nullptr); - } - - void MockResponse(const std::string& sRequestParams, const std::string& sResponseBody) - { - auto& pResponse = m_vResponses.emplace_back(); - pResponse.sRequestParams = sRequestParams; - pResponse.sResponseBody = sResponseBody; - pResponse.nHttpStatus = 200; - } - - void OnBeforeResponse(const std::string& sRequestParams, std::function&& fHandler); - - void MockUser(const std::string& sUsername, const std::string& sApiToken); - - void MockGame(); - rc_client_achievement_info_t* MockAchievement(uint32_t nId, const char* sTitle = nullptr); - rc_client_achievement_info_t* MockAchievementWithTrigger(uint32_t nId, const char* sTitle = nullptr); - rc_client_achievement_info_t* MockUnofficialAchievement(uint32_t nId, const char* sTitle = nullptr); - rc_client_achievement_info_t* MockLocalAchievement(uint32_t nId, const char* sTitle = nullptr); - - rc_client_leaderboard_info_t* MockLeaderboard(uint32_t nId, const char* sTitle = nullptr); - rc_client_leaderboard_info_t* MockLocalLeaderboard(uint32_t nId, const char* sTitle = nullptr); - - void AssertCalled(const std::string& sRequestParams) const; - - void AssertNoPendingRequests() const; - -private: - static void MockServerCall(const rc_api_request_t* pRequest, rc_client_server_callback_t fCallback, - void* pCallbackData, rc_client_t*); - - ra::services::ServiceLocator::ServiceOverride m_Override; - - typedef struct MockApiResponse - { - std::string sRequestParams; - std::string sResponseBody; - int nHttpStatus = 0; - bool bSeen = 0; - rc_client_server_callback_t fAsyncCallback = nullptr; - void* pAsyncCallbackData = nullptr; - std::function fBeforeResponse = nullptr; - } MockApiResponse; - - std::vector m_vResponses; - - std::string m_sUsername; - std::string m_sApiToken; -}; - -} // namespace mocks -} // namespace services -} // namespace ra - -#endif // !RA_SERVICES_MOCK_RCHEEVOS_CLIENT_HH diff --git a/tests/mocks/MockServer.hh b/tests/mocks/MockServer.hh index 3731d857..05adab86 100644 --- a/tests/mocks/MockServer.hh +++ b/tests/mocks/MockServer.hh @@ -63,21 +63,6 @@ public: return HandleRequest(request); } - Ping::Response Ping(const Ping::Request& request) override - { - return HandleRequest(request); - } - - AwardAchievement::Response AwardAchievement(const AwardAchievement::Request& request) override - { - return HandleRequest(request); - } - - SubmitLeaderboardEntry::Response SubmitLeaderboardEntry(const SubmitLeaderboardEntry::Request& request) override - { - return HandleRequest(request); - } - FetchUserFriends::Response FetchUserFriends(const FetchUserFriends::Request& request) override { return HandleRequest(request); diff --git a/tests/services/AchievementRuntime_Tests.cpp b/tests/services/AchievementRuntime_Tests.cpp index 266fbd4d..06f235d1 100644 --- a/tests/services/AchievementRuntime_Tests.cpp +++ b/tests/services/AchievementRuntime_Tests.cpp @@ -2,53 +2,30 @@ #include "tests\RA_UnitTestHelpers.h" #include "tests\data\DataAsserts.hh" +#include "tests\mocks\MockAudioSystem.hh" +#include "tests\mocks\MockClock.hh" +#include "tests\mocks\MockConfiguration.hh" #include "tests\mocks\MockConsoleContext.hh" #include "tests\mocks\MockEmulatorContext.hh" #include "tests\mocks\MockFileSystem.hh" +#include "tests\mocks\MockFrameEventQueue.hh" #include "tests\mocks\MockGameContext.hh" +#include "tests\mocks\MockImageRepository.hh" +#include "tests\mocks\MockOverlayManager.hh" +#include "tests\mocks\MockOverlayTheme.hh" +#include "tests\mocks\MockSessionTracker.hh" +#include "tests\mocks\MockSurface.hh" #include "tests\mocks\MockThreadPool.hh" #include "tests\mocks\MockUserContext.hh" #include "tests\mocks\MockWindowManager.hh" +#include "tests\ui\UIAsserts.hh" + +#include +#include + using namespace Microsoft::VisualStudio::CppUnitTestFramework; -namespace Microsoft { -namespace VisualStudio { -namespace CppUnitTestFramework { - -template<> -std::wstring ToString( - const ra::services::AchievementRuntime::ChangeType& nChangeType) -{ - switch (nChangeType) - { - case ra::services::AchievementRuntime::ChangeType::None: - return L"None"; - case ra::services::AchievementRuntime::ChangeType::AchievementTriggered: - return L"AchievementTriggered"; - case ra::services::AchievementRuntime::ChangeType::AchievementReset: - return L"AchievementReset"; - case ra::services::AchievementRuntime::ChangeType::AchievementDisabled: - return L"AchievementDisabled"; - case ra::services::AchievementRuntime::ChangeType::LeaderboardStarted: - return L"LeaderboardStarted"; - case ra::services::AchievementRuntime::ChangeType::LeaderboardCanceled: - return L"LeaderboardCanceled"; - case ra::services::AchievementRuntime::ChangeType::LeaderboardUpdated: - return L"LeaderboardUpdated"; - case ra::services::AchievementRuntime::ChangeType::LeaderboardTriggered: - return L"LeaderboardTriggered"; - case ra::services::AchievementRuntime::ChangeType::LeaderboardDisabled: - return L"LeaderboardDisabled"; - default: - return std::to_wstring(static_cast(nChangeType)); - } -} - -} // namespace CppUnitTestFramework -} // namespace VisualStudio -} // namespace Microsoft - namespace ra { namespace services { namespace tests { @@ -59,346 +36,664 @@ public: GSL_SUPPRESS_F6 AchievementRuntimeHarness() : m_Override(this) { mockUserContext.Initialize("User", "ApiToken"); + GetClient()->user.display_name = "UserDisplay"; + GetClient()->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + MockGame(); + + m_fRealEventHandler = GetClient()->callbacks.event_handler; + rc_client_set_event_handler(GetClient(), CaptureEventHandler); + + rc_client_set_userdata(GetClient(), this); } ra::data::context::mocks::MockEmulatorContext mockEmulatorContext; ra::data::context::mocks::MockGameContext mockGameContext; + ra::data::context::mocks::MockSessionTracker mockSessionTracker; ra::data::context::mocks::MockUserContext mockUserContext; + ra::services::mocks::MockAudioSystem mockAudioSystem; + ra::services::mocks::MockClock mockClock; + ra::services::mocks::MockConfiguration mockConfiguration; ra::services::mocks::MockFileSystem mockFileSystem; + ra::services::mocks::MockFrameEventQueue mockFrameEventQueue; ra::services::mocks::MockThreadPool mockThreadPool; + ra::ui::drawing::mocks::MockSurfaceFactory mockSurfaceFactory; + ra::ui::mocks::MockImageRepository mockImageRepository; + ra::ui::mocks::MockOverlayTheme mockOverlayTheme; + ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager; ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager; - rc_memref_t* GetMemRefs() noexcept + void MockGame() { - return m_pRuntime.memrefs; + rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(rc_client_game_info_t)); + Expects(game != nullptr); + rc_buf_init(&game->buffer); + rc_runtime_init(&game->runtime); + + game->public_.id = 1; + game->public_.console_id = RC_CONSOLE_NINTENDO; + game->public_.badge_name = "012345"; + game->public_.title = "Game Title"; + + GetClient()->game = game; } - size_t GetTriggerCount() const noexcept + void ResetEvents() { m_vEvents.clear(); } + + size_t GetEventCount() const { return m_vEvents.size(); } + + void AssertEvent(uint32_t nType, uint32_t nRecordId = 0) { - return m_pRuntime.trigger_count; + for (const auto& pEvent : m_vEvents) + { + if (pEvent.type != nType) + continue; + + switch (nType) + { + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + if (pEvent.achievement->id == nRecordId) + return; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD: + if (pEvent.leaderboard->id == nRecordId) + return; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + if (pEvent.leaderboard_tracker->id == nRecordId) + return; + break; + + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + case RC_CLIENT_EVENT_GAME_COMPLETED: + return; + } + } + + if (nRecordId != 0) + Assert::Fail(ra::StringPrintf(L"Event %u not found for record %u.", nType, nRecordId).c_str()); + else + Assert::Fail(ra::StringPrintf(L"Event %u not found.", nType).c_str()); + } + + void RaiseEvent(const rc_client_event_t& event) + { + m_fRealEventHandler(&event, GetClient()); + } + + rc_client_achievement_info_t* MockAchievement(uint32_t nId, const std::string& sTrigger) + { + auto* game = GetClient()->game; + auto* ach = AddAchievement(game, GetCoreSubset(game), nId, ra::StringPrintf("Ach%u", nId).c_str()); + + auto nSize = rc_trigger_size(sTrigger.c_str()); + void* trigger_buffer = rc_buf_alloc(&game->buffer, nSize); + ach->trigger = rc_parse_trigger(trigger_buffer, sTrigger.c_str(), nullptr, 0); + ActivateAchievement(ach); + + return ach; + } + + rc_client_achievement_info_t* MockLocalAchievement(uint32_t nId, const std::string& sTrigger) + { + auto* game = GetClient()->game; + auto* ach = AddAchievement(game, GetLocalSubset(game), nId, ra::StringPrintf("Ach%u", nId).c_str()); + + auto nSize = rc_trigger_size(sTrigger.c_str()); + void* trigger_buffer = rc_buf_alloc(&game->buffer, nSize); + ach->trigger = rc_parse_trigger(trigger_buffer, sTrigger.c_str(), nullptr, 0); + ActivateAchievement(ach); + + return ach; + } + + void ActivateAchievement(rc_client_achievement_info_t* pAchievement) + { + pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + pAchievement->trigger->state = RC_TRIGGER_STATE_WAITING; + } + + void DeactivateAchievement(rc_client_achievement_info_t* pAchievement) + { + pAchievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; + pAchievement->trigger->state = RC_TRIGGER_STATE_INACTIVE; + } + + ra::data::models::AchievementModel* WrapAchievement(rc_client_achievement_info_t* pAchievement) + { + auto nCategory = ra::data::models::AssetCategory::Core; + if (pAchievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) + { + nCategory = ra::data::models::AssetCategory::Unofficial; + } + else + { + const auto* pSubset = GetClient()->game->subsets; + for (; pSubset; pSubset = pSubset->next) + { + for (uint32_t i = 0; i < pSubset->public_.num_achievements; ++i) + { + if (&pSubset->achievements[i] == pAchievement) + { + if (pSubset->public_.id == ra::data::context::GameAssets::LocalSubsetId) + nCategory = ra::data::models::AssetCategory::Local; + break; + } + } + } + } + + auto vmAchievement = std::make_unique(); + vmAchievement->Attach(*pAchievement, nCategory, ""); + + mockGameContext.Assets().Append(std::move(vmAchievement)); + return mockGameContext.Assets().FindAchievement(pAchievement->public_.id); + } + + rc_client_leaderboard_info_t* MockLeaderboard(uint32_t nId, const std::string& sDefinition) + { + rc_client_game_info_t* game = GetClient()->game; + auto* lboard = AddLeaderboard(game, GetCoreSubset(game), nId, ra::StringPrintf("Leaderboard%u", nId).c_str()); + + auto nSize = rc_lboard_size(sDefinition.c_str()); + void* lboard_buffer = rc_buf_alloc(&game->buffer, nSize); + lboard->lboard = rc_parse_lboard(lboard_buffer, sDefinition.c_str(), nullptr, 0); + ActivateLeaderboard(lboard); + + return lboard; + } + + rc_client_leaderboard_info_t* MockLocalLeaderboard(uint32_t nId, const std::string& sDefinition) + { + rc_client_game_info_t* game = GetClient()->game; + auto* lboard = AddLeaderboard(game, GetLocalSubset(game), nId, ra::StringPrintf("Leaderboard%u", nId).c_str()); + + auto nSize = rc_lboard_size(sDefinition.c_str()); + void* lboard_buffer = rc_buf_alloc(&game->buffer, nSize); + lboard->lboard = rc_parse_lboard(lboard_buffer, sDefinition.c_str(), nullptr, 0); + ActivateLeaderboard(lboard); + + return lboard; + } + + void ActivateLeaderboard(rc_client_leaderboard_info_t* pLeaderboard) + { + pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + pLeaderboard->lboard->state = RC_LBOARD_STATE_ACTIVE; + } + + void DeactivateLeaderboard(rc_client_leaderboard_info_t* pLeaderboard) + { + pLeaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + pLeaderboard->lboard->state = RC_LBOARD_STATE_INACTIVE; + } + + ra::data::models::LeaderboardModel* WrapLeaderboard(rc_client_leaderboard_info_t* pLeaderboard) + { + auto nCategory = ra::data::models::AssetCategory::Core; + + const auto* pSubset = GetClient()->game->subsets; + for (; pSubset; pSubset = pSubset->next) + { + for (uint32_t i = 0; i < pSubset->public_.num_leaderboards; ++i) + { + if (&pSubset->leaderboards[i] == pLeaderboard) + { + if (pSubset->public_.id == ra::data::context::GameAssets::LocalSubsetId) + nCategory = ra::data::models::AssetCategory::Local; + break; + } + } + } + + auto vmLeaderboard = std::make_unique(); + vmLeaderboard->Attach(*pLeaderboard, nCategory, ""); + pLeaderboard->lboard->state = RC_LBOARD_STATE_ACTIVE; // syncing State sets this back to waiting + + mockGameContext.Assets().Append(std::move(vmLeaderboard)); + return mockGameContext.Assets().FindLeaderboard(pLeaderboard->public_.id); + } + + void SyncToRuntime() + { + // toggling hardcore will reset the runtime with appropriately active achievements + bool bHardcoreEnabled = rc_client_get_hardcore_enabled(GetClient()); + rc_client_set_hardcore_enabled(GetClient(), !bHardcoreEnabled); + rc_client_set_hardcore_enabled(GetClient(), bHardcoreEnabled); + + // one of the toggles will be to hardcore mode, which expects a reset - clear the flag + GetClient()->game->waiting_for_reset = false; + + // MockAchievement generates triggers with their own memrefs - promote to common memref pool + auto& runtime = GetClient()->game->runtime; + Assert::IsNull(runtime.memrefs); + auto** next = &runtime.memrefs; + for (unsigned i = 0; i < runtime.trigger_count; ++i) + { + *next = runtime.triggers[i].trigger->memrefs; + while (*next) + next = &(*next)->next; + + runtime.triggers[i].trigger->memrefs = nullptr; + } + } + + void SaveProgressToString(std::string& sBuffer) + { + const size_t nSize = SaveProgressToBuffer(reinterpret_cast(sBuffer.data()), gsl::narrow_cast(sBuffer.size())); + if (nSize > sBuffer.size()) + { + sBuffer.resize(nSize); + SaveProgressToBuffer(reinterpret_cast(sBuffer.data()), gsl::narrow_cast(nSize)); + } + } + + bool LoadProgressFromString(const std::string& sBuffer) + { + return LoadProgressFromBuffer(reinterpret_cast(sBuffer.data())); } private: + static void CaptureEventHandler(const rc_client_event_t* pEvent, rc_client_t* pClient) + { + auto* harness = static_cast(rc_client_get_userdata(pClient)); + Expects(harness != nullptr); + harness->m_vEvents.push_back(*pEvent); + } + + std::vector m_vEvents; + + static rc_client_subset_info_t* GetSubset(rc_client_game_info_t* game, uint32_t subset_id, const char* name) + { + rc_client_subset_info_t *subset = game->subsets, **next = &game->subsets; + for (; subset; subset = subset->next) + { + if (subset->public_.id == subset_id) + return subset; + + next = &subset->next; + } + + subset = (rc_client_subset_info_t*)rc_buf_alloc(&game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = subset_id; + strcpy_s(subset->public_.badge_name, sizeof(subset->public_.badge_name), game->public_.badge_name); + subset->public_.title = name; + subset->active = 1; + + *next = subset; + + return subset; + } + + static rc_client_subset_info_t* GetCoreSubset(rc_client_game_info_t* game) + { + return GetSubset(game, game->public_.id, game->public_.title); + } + + static rc_client_subset_info_t* GetLocalSubset(rc_client_game_info_t* game) + { + return GetSubset(game, ra::data::context::GameAssets::LocalSubsetId, "Local"); + } + + static rc_client_achievement_info_t* AddAchievement(rc_client_game_info_t* game, rc_client_subset_info_t* subset, + uint32_t nId, const char* sTitle) + { + if (subset->public_.num_achievements % 8 == 0) + { + const uint32_t new_count = subset->public_.num_achievements + 8; + rc_client_achievement_info_t* new_achievements = (rc_client_achievement_info_t*)rc_buf_alloc( + &game->buffer, sizeof(rc_client_achievement_info_t) * new_count); + + if (subset->public_.num_achievements > 0) + { + memcpy(new_achievements, subset->achievements, + sizeof(rc_client_achievement_info_t) * subset->public_.num_achievements); + } + + subset->achievements = new_achievements; + } + + rc_client_achievement_info_t* achievement = &subset->achievements[subset->public_.num_achievements++]; + memset(achievement, 0, sizeof(*achievement)); + achievement->public_.id = nId; + + if (sTitle) + { + achievement->public_.title = rc_buf_strcpy(&game->buffer, sTitle); + } + else + { + const std::string sGeneratedTitle = ra::StringPrintf("Achievement %u", nId); + achievement->public_.title = rc_buf_strcpy(&game->buffer, sGeneratedTitle.c_str()); + } + + const std::string sGeneratedDescripton = ra::StringPrintf("Description %u", nId); + achievement->public_.description = rc_buf_strcpy(&game->buffer, sGeneratedDescripton.c_str()); + + achievement->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + achievement->public_.points = 5; + + return achievement; + } + + static rc_client_leaderboard_info_t* AddLeaderboard(rc_client_game_info_t* game, rc_client_subset_info_t* subset, + uint32_t nId, const char* sTitle) + { + if (subset->public_.num_leaderboards % 8 == 0) + { + const uint32_t new_count = subset->public_.num_leaderboards + 8; + rc_client_leaderboard_info_t* new_leaderboards = (rc_client_leaderboard_info_t*)rc_buf_alloc( + &game->buffer, sizeof(rc_client_leaderboard_info_t) * new_count); + + if (subset->public_.num_leaderboards > 0) + { + memcpy(new_leaderboards, subset->leaderboards, + sizeof(rc_client_leaderboard_info_t) * subset->public_.num_leaderboards); + } + + subset->leaderboards = new_leaderboards; + } + + rc_client_leaderboard_info_t* leaderboard = &subset->leaderboards[subset->public_.num_leaderboards++]; + memset(leaderboard, 0, sizeof(*leaderboard)); + leaderboard->public_.id = nId; + + if (sTitle) + { + leaderboard->public_.title = rc_buf_strcpy(&game->buffer, sTitle); + } + else + { + const std::string sGeneratedTitle = ra::StringPrintf("Leaderboard %u", nId); + leaderboard->public_.title = rc_buf_strcpy(&game->buffer, sGeneratedTitle.c_str()); + } + + const std::string sGeneratedDescripton = ra::StringPrintf("Description %u", nId); + leaderboard->public_.description = rc_buf_strcpy(&game->buffer, sGeneratedDescripton.c_str()); + + leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + + return leaderboard; + } + ra::services::ServiceLocator::ServiceOverride m_Override; + rc_client_event_handler_t m_fRealEventHandler; }; TEST_CLASS(AchievementRuntime_Tests) { -private: - std::array sTriggerBuffer{}; - - rc_trigger_t* ParseTrigger(const char* sTrigger, unsigned char* sBuffer = nullptr, size_t nBufferSize = 0U) - { - Expects(sTrigger != nullptr); - if (sBuffer == nullptr) - { - sBuffer = sTriggerBuffer.data(); - nBufferSize = sizeof(sTriggerBuffer); - } - - const auto nSize = rc_trigger_size(sTrigger); - Expects(to_unsigned(nSize) < nBufferSize); - - return rc_parse_trigger(sBuffer, sTrigger, nullptr, 0); - } - - rc_lboard_t* ParseLeaderboard(const char* sTrigger, unsigned char* sBuffer = nullptr, size_t nBufferSize = 0U) - { - Expects(sTrigger != nullptr); - if (sBuffer == nullptr) - { - sBuffer = sTriggerBuffer.data(); - nBufferSize = sizeof(sTriggerBuffer); - } - - const auto nSize = rc_lboard_size(sTrigger); - Expects(to_unsigned(nSize) < nBufferSize); - - return rc_parse_lboard(sBuffer, sTrigger, nullptr, 0); - } - - rc_richpresence_t* ParseRichPresence(const char* sScript, unsigned char* sBuffer = nullptr, size_t nBufferSize = 0U) - { - Expects(sScript != nullptr); - if (sBuffer == nullptr) - { - sBuffer = sTriggerBuffer.data(); - nBufferSize = sizeof(sTriggerBuffer); - } - - const auto nSize = rc_richpresence_size(sScript); - Expects(to_unsigned(nSize) < nBufferSize); - - return rc_parse_richpresence(sBuffer, sScript, nullptr, 0); - } - - static void AssertChange(const std::vector& vChanges, AchievementRuntime::ChangeType nType, unsigned nId) - { - for (const auto& pChange : vChanges) - { - if (pChange.nId == nId && pChange.nType == nType) - return; - } - - const auto pString = ra::StringPrintf(L"Could not find change notification for %s:%d", - Microsoft::VisualStudio::CppUnitTestFramework::ToString(nType), nId); - Assert::Fail(pString.c_str()); - } - - static void AssertChange(const std::vector& vChanges, AchievementRuntime::ChangeType nType, unsigned nId, int nValue) - { - for (const auto& pChange : vChanges) - { - if (pChange.nId == nId && pChange.nType == nType) - { - Assert::AreEqual(nValue, pChange.nValue); - return; - } - } - - const auto pString = ra::StringPrintf(L"Could not find change notification for %s:%d", - Microsoft::VisualStudio::CppUnitTestFramework::ToString(nType), nId); - Assert::Fail(pString.c_str()); - } - public: - TEST_METHOD(TestActivateAchievement) + TEST_METHOD(TestSyncAssetsAchievement) + { + AchievementRuntimeHarness runtime; + runtime.MockGame(); + + auto vmAchievement = std::make_unique(); + vmAchievement->CreateServerCheckpoint(); + vmAchievement->SetCategory(ra::data::models::AssetCategory::Local); + vmAchievement->SetName(L"Achievement Name"); + vmAchievement->SetDescription(L"Do something cool"); + vmAchievement->SetPoints(25); + vmAchievement->SetTrigger("0xH0000=1"); + vmAchievement->SetID(ra::data::context::GameAssets::FirstLocalId); + vmAchievement->CreateLocalCheckpoint(); + vmAchievement->SetState(ra::data::models::AssetState::Active); + runtime.mockGameContext.Assets().Append(std::move(vmAchievement)); + + // SyncAssets should generate an empty core subset and a local subset containing the achievement + runtime.SyncAssets(); + + auto* pSubset = runtime.GetClient()->game->subsets; + Expects(pSubset != nullptr); + Assert::AreEqual("Game Title", pSubset->public_.title); + Assert::AreEqual(1U, pSubset->public_.id); + Assert::AreEqual("012345", pSubset->public_.badge_name); + Assert::AreEqual(0U, pSubset->public_.num_achievements); + Assert::IsTrue(pSubset->active); + + pSubset = pSubset->next; + Expects(pSubset != nullptr); + Assert::AreEqual("Local", pSubset->public_.title); + Assert::AreEqual(ra::data::context::GameAssets::LocalSubsetId, pSubset->public_.id); + Assert::AreEqual("012345", pSubset->public_.badge_name); + Assert::AreEqual(1U, pSubset->public_.num_achievements); + Assert::IsTrue(pSubset->active); + Assert::IsNull(pSubset->next); + + const auto* pAchievement = pSubset->achievements; + Assert::AreEqual("Achievement Name", pAchievement->public_.title); + Assert::AreEqual("Do something cool", pAchievement->public_.description); + Assert::AreEqual(25U, pAchievement->public_.points); + Assert::AreEqual(ra::data::context::GameAssets::FirstLocalId, pAchievement->public_.id); + Assert::IsNotNull(pAchievement->trigger); + const auto* pOldTrigger = pAchievement->trigger; + + // directly modifying the achievement trigger should rebuild the underlying trigger + runtime.mockGameContext.Assets().FindAchievement(ra::data::context::GameAssets::FirstLocalId) + ->SetTrigger("0xH0000=2"); + Assert::IsNotNull(pAchievement->trigger); + const auto* pNewTrigger = pAchievement->trigger; + Assert::AreNotEqual(static_cast(pOldTrigger), static_cast(pNewTrigger)); + + // refreshing a local asset will reconstruct the model. since the hash is the same, the sync + // should reuse the trigger. + runtime.mockGameContext.Assets().Clear(); + + auto vmAchievement2 = std::make_unique(); + vmAchievement2->CreateServerCheckpoint(); + vmAchievement2->SetCategory(ra::data::models::AssetCategory::Local); + vmAchievement2->SetName(L"Achievement Name"); + vmAchievement2->SetDescription(L"Do something cool"); + vmAchievement2->SetPoints(25); + vmAchievement2->SetTrigger("0xH0000=2"); + vmAchievement2->SetID(ra::data::context::GameAssets::FirstLocalId); + vmAchievement2->CreateLocalCheckpoint(); + vmAchievement2->SetState(ra::data::models::AssetState::Active); + runtime.mockGameContext.Assets().Append(std::move(vmAchievement2)); + + runtime.SyncAssets(); + + pAchievement = runtime.GetClient()->game->subsets->next->achievements; + Expects(pAchievement != nullptr); + const auto* pNewerTrigger = pAchievement->trigger; + Assert::AreEqual(static_cast(pNewTrigger), static_cast(pNewerTrigger)); + + // refreshing a local asset will reconstruct the model. if the hash changes, a new trigger + // will be constructed. + runtime.mockGameContext.Assets().Clear(); + + auto vmAchievement3 = std::make_unique(); + vmAchievement3->CreateServerCheckpoint(); + vmAchievement3->SetCategory(ra::data::models::AssetCategory::Local); + vmAchievement3->SetName(L"Achievement Name"); + vmAchievement3->SetDescription(L"Do something cool"); + vmAchievement3->SetPoints(25); + vmAchievement3->SetTrigger("0xH0000=1"); + vmAchievement3->SetID(ra::data::context::GameAssets::FirstLocalId); + vmAchievement3->CreateLocalCheckpoint(); + vmAchievement3->SetState(ra::data::models::AssetState::Active); + runtime.mockGameContext.Assets().Append(std::move(vmAchievement3)); + + runtime.SyncAssets(); + + pAchievement = runtime.GetClient()->game->subsets->next->achievements; + Expects(pAchievement != nullptr); + const auto* pNewestTrigger = pAchievement->trigger; + Assert::AreNotEqual(static_cast(pNewerTrigger), static_cast(pNewestTrigger)); + } + + TEST_METHOD(TestDoFrameTriggerAchievement) { std::array memory{ 0x00 }; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - auto* pTrigger = "0xH0000=1"; + runtime.MockAchievement(6U, "0xH0000=1"); - // achievement not active, should not raise events - std::vector vChanges; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + // expect no events for untriggered achievement + runtime.DoFrame(); + Assert::AreEqual({ 0U }, runtime.GetEventCount()); - // first Process call will switch the state from Waiting to Active - runtime.ActivateAchievement(6U, pTrigger); - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 6U); - vChanges.clear(); - - // now that it's active, we can trigger it + // now trigger it memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 6U); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({ 1U }, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6U); + runtime.ResetEvents(); // triggered achievement should not continue to trigger - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); } - TEST_METHOD(TestActivateAchievementPaused) + TEST_METHOD(TestDoFrameTriggerAchievementWhilePaused) { std::array memory{ 0x00 }; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - auto* pTrigger = "0xH0000=0"; + runtime.MockAchievement(6U, "0xH0000=1"); runtime.SetPaused(true); Assert::IsTrue(runtime.IsPaused()); - runtime.ActivateAchievement(6U, pTrigger); // runtime is paused and achievement will be ignored as long as its true - std::vector vChanges; for (int i = 0; i < 10; i++) { - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); } runtime.SetPaused(false); Assert::IsFalse(runtime.IsPaused()); - // achievement will initially be "waiting" and must be false before it can activate - runtime.Process(vChanges); + // expect no events for untriggered achievement + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // pause runtime and trigger achievement + runtime.SetPaused(true); + Assert::IsTrue(runtime.IsPaused()); + memory.at(0) = 1; + + // runtime is paused and achievement will be ignored as long as its paused for (int i = 0; i < 10; i++) { - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); } - // achievement is no longer true, so it can activate - memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 6U); - vChanges.clear(); - - // achievement is true again, but should not trigger when paused - memory.at(0) = 0; - runtime.SetPaused(true); - Assert::IsTrue(runtime.IsPaused()); - vChanges.clear(); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // processing re-enabled, achievement should trigger + // unpausing the runtime does not explicitly process a frame runtime.SetPaused(false); - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 6U); + Assert::IsFalse(runtime.IsPaused()); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // now achievement should trigger + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6U); + runtime.ResetEvents(); } - TEST_METHOD(TestReactivateAchievementPaused) + TEST_METHOD(TestDoFrameReactivateAchievementWhilePaused) { - std::array memory{ 0x00 }; + std::array memory{0x00}; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - const char* pTriggerDefinition = "0xH0000=1"; - rc_trigger_t* pTrigger; + auto* pAchievement = runtime.MockAchievement(6U, "0xH0000=1"); - std::vector vChanges; - runtime.ActivateAchievement(6U, pTriggerDefinition); - pTrigger = runtime.GetAchievementTrigger(6U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual(static_cast(RC_TRIGGER_STATE_WAITING), static_cast(pTrigger->state)); + // expect no events for untriggered achievement + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); - runtime.Process(vChanges); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 6U); - Assert::AreEqual(static_cast(RC_TRIGGER_STATE_ACTIVE), static_cast(pTrigger->state)); - vChanges.clear(); + // trigger achievement + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6U); + runtime.ResetEvents(); + // pause runtime and reactivate the achievement - expect no events runtime.SetPaused(true); - Assert::IsTrue(runtime.IsPaused()); + runtime.ActivateAchievement(pAchievement); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); - runtime.DeactivateAchievement(6U); - Assert::IsNull(runtime.GetAchievementTrigger(6U)); - - // activating a previously active achievement should resurrect it, even if paused - runtime.ActivateAchievement(6U, pTriggerDefinition); - pTrigger = runtime.GetAchievementTrigger(6U); - Assert::IsNotNull(pTrigger); - Assert::AreEqual(static_cast(RC_TRIGGER_STATE_WAITING), static_cast(pTrigger->state)); - - // achievement should not become active when paused - runtime.SetPaused(true); - Assert::IsTrue(runtime.IsPaused()); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // processing re-enabled, achievement should become active + // unpause runtime - expect no events - achievement is waiting runtime.SetPaused(false); - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 6U); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual((int)RC_TRIGGER_STATE_WAITING, (int)pAchievement->trigger->state); + + // adjust memory so achievement will transition to active + memory.at(0) = 0; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pAchievement->trigger->state); + + // pause runtime and update memory so achievement would trigger - expect no events + runtime.SetPaused(true); + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // unpause runtime - expect event + runtime.SetPaused(false); + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6U); } - TEST_METHOD(TestMonitorAchievementReset) + TEST_METHOD(TestDoFrameMeasuredAchievement) { std::array memory{0x00, 0x12, 0x34, 0xAB, 0x56}; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - auto* pTrigger = "1=1.3._R:0xH0000=1"; - runtime.ActivateAchievement(4U, pTrigger); - runtime.GetAchievementTrigger(4U)->state = RC_TRIGGER_STATE_ACTIVE; - - // HitCount should increase, but no trigger - std::vector vChanges; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // trigger reset, expect notification - memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementReset, 4U); - vChanges.clear(); - - // watch for notification, already reset, no notification - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // disable reset, hitcount should increase, no notification - memory.at(0) = 0; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // enable reset, expect notification - memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementReset, 4U); - vChanges.clear(); - - // already reset, shouldn't get repeated notification - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // disable reset, advance to trigger - memory.at(0) = 0; - runtime.Process(vChanges); - runtime.Process(vChanges); - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 4U); - vChanges.clear(); - } - - TEST_METHOD(TestMonitorAchievementMeasured) - { - std::array memory{0x00, 0x12, 0x34, 0xAB, 0x56}; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - auto* pTrigger = "M:0xH0002=100"; - runtime.ActivateAchievement(4U, pTrigger); - runtime.GetAchievementTrigger(4U)->state = RC_TRIGGER_STATE_ACTIVE; + runtime.MockAchievement(4U, "M:0xH0002=100"); // value did not change, expect no event from initialization - std::vector vChanges; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // change value, expect notification memory.at(2) = 75; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementProgressChanged, 4U, 75); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 4U); + runtime.ResetEvents(); // watch for notification, no change, no notification - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // disable achievement, no notification - runtime.DeactivateAchievement(4U); - memory.at(2) = 76; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // enable achievement, expect activation event, but not progress notification - runtime.ActivateAchievement(4U, pTrigger); - memory.at(2) = 77; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 4U); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // change value, expect notification memory.at(2) = 78; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementProgressChanged, 4U, 78); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE, 4U); + runtime.ResetEvents(); // trigger achievement, expect trigger notification, but not progress notification memory.at(2) = 100; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 4U); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 4U); } - + /* TEST_METHOD(TestUpdateAchievementId) { std::array memory{ 0x00 }; @@ -437,67 +732,7 @@ public: Assert::AreEqual({ 1U }, vChanges.size()); AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 99001U); } - - TEST_METHOD(TestResetActiveAchievements) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - runtime.ActivateAchievement(6U, "0xH0000=0_0xH0001=1"); - auto* pTrigger = runtime.GetAchievementTrigger(6U); - pTrigger->state = RC_TRIGGER_STATE_ACTIVE; - - runtime.ActivateAchievement(7U, "0xH0000=0.30._R:0xH0001=1"); - auto* pTrigger2 = runtime.GetAchievementTrigger(7U); - pTrigger2->state = RC_TRIGGER_STATE_ACTIVE; - - // rack up some hits - std::vector vChanges; - for (int i = 0; i < 10; i++) - { - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - } - Assert::AreEqual(10U, pTrigger->requirement->conditions->current_hits); - Assert::AreEqual(10U, pTrigger2->requirement->conditions->current_hits); - - // set state to trigger achievement, then call reset. HitCount should go to 0 and trigger should be delayed - // until inactive. - memory.at(1) = 1; - runtime.ResetActiveAchievements(); - Assert::AreEqual(0U, pTrigger->requirement->conditions->current_hits); - Assert::AreEqual(0U, pTrigger2->requirement->conditions->current_hits); - - // first call will register an activation change for 7. ignore it - runtime.Process(vChanges); - vChanges.clear(); - - for (int i = 0; i < 10; i++) - { - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - } - - // achievement is no longer true, so it will transition from waiting to active - memory.at(1) = 0; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, 6U); - vChanges.clear(); - - // second achievement should get a hitcount since the reset condition is no longer true - Assert::AreEqual(1U, pTrigger2->requirement->conditions->current_hits); - - // achievement is true again and should trigger - // second achievement is reset - expect to be notified - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 2U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementTriggered, 6U); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementReset, 7U); - Assert::AreEqual(0U, pTrigger2->requirement->conditions->current_hits); - } + */ static rc_condition_t* GetCondition(AchievementRuntimeHarness& harness, ra::AchievementID nId, int nGroup, int nCond) noexcept { @@ -533,35 +768,35 @@ public: Assert::IsNotNull(pCond); Assert::AreEqual(pCond->current_hits, ra::to_unsigned(nHits)); } - /* + TEST_METHOD(TestPersistProgressFile) { AchievementRuntimeHarness runtime; - runtime.ActivateAchievement(3U, "1=1.10."); - runtime.ActivateAchievement(5U, "1=1.2."); + auto* pAch3 = runtime.MockAchievement(3U, "1=1.10."); + auto* pAch5 = runtime.MockAchievement(5U, "1=1.2."); + runtime.SyncToRuntime(); SetConditionHitCount(runtime, 3U, 0, 0, 2); SetConditionHitCount(runtime, 5U, 0, 0, 2); runtime.SaveProgressToFile("test.sav"); - // inactive achievements aren't persisted - runtime.GetAchievementTrigger(3U)->state = RC_TRIGGER_STATE_INACTIVE; - runtime.GetAchievementTrigger(5U)->state = RC_TRIGGER_STATE_INACTIVE; + // inactive achievements aren't updated from the persisted data + runtime.DeactivateAchievement(pAch3); + runtime.DeactivateAchievement(pAch5); SetConditionHitCount(runtime, 3U, 0, 0, 1); SetConditionHitCount(runtime, 5U, 0, 0, 1); runtime.LoadProgressFromFile("test.sav"); - AssertConditionHitCount(runtime, 3U, 0, 0, 0); - AssertConditionHitCount(runtime, 5U, 0, 0, 0); + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 1); // activate one achievement, save, activate other, restore - only first should be restored, // second should be reset because it wasn't captured. - runtime.GetAchievementTrigger(3U)->state = RC_TRIGGER_STATE_ACTIVE; + runtime.ActivateAchievement(pAch3); SetConditionHitCount(runtime, 3U, 0, 0, 1); SetConditionHitCount(runtime, 5U, 0, 0, 1); runtime.SaveProgressToFile("test.sav"); - runtime.GetAchievementTrigger(5U)->state = RC_TRIGGER_STATE_ACTIVE; - + runtime.ActivateAchievement(pAch5); SetConditionHitCount(runtime, 3U, 0, 0, 2); SetConditionHitCount(runtime, 5U, 0, 0, 2); runtime.LoadProgressFromFile("test.sav"); @@ -569,7 +804,6 @@ public: AssertConditionHitCount(runtime, 5U, 0, 0, 0); // both active, save and restore should reset both - runtime.GetAchievementTrigger(5U)->state = RC_TRIGGER_STATE_ACTIVE; SetConditionHitCount(runtime, 3U, 0, 0, 1); SetConditionHitCount(runtime, 5U, 0, 0, 1); runtime.SaveProgressToFile("test.sav"); @@ -583,118 +817,91 @@ public: // second no longer active, file contains data that should be ignored SetConditionHitCount(runtime, 3U, 0, 0, 6); SetConditionHitCount(runtime, 5U, 0, 0, 6); - runtime.DeactivateAchievement(5U); + runtime.DeactivateAchievement(pAch5); runtime.LoadProgressFromFile("test.sav"); AssertConditionHitCount(runtime, 3U, 0, 0, 1); AssertConditionHitCount(runtime, 5U, 0, 0, 6); - // if the definition changes, the hits should be reset instead of remembered - pAchievement3->GenerateConditions(); - pAchievement3->GetCondition(0, 0).CompSource().SetValue(2); - pAchievement3->RebuildTrigger(); - pAchievement3->SetConditionHitCount(0, 0, 2); - Assert::IsTrue(pAchievement3->Active()); + // if the definition changes (different md5), the hits should be reset instead of remembered + runtime.GetClient()->game->runtime.triggers[0].md5[0]++; + SetConditionHitCount(runtime, 3U, 0, 0, 6); + runtime.ActivateAchievement(pAch3); runtime.LoadProgressFromFile("test.sav"); - Assert::AreEqual(0U, pAchievement3->GetConditionHitCount(0, 0)); + AssertConditionHitCount(runtime, 3U, 0, 0, 0); } TEST_METHOD(TestPersistProgressBuffer) { AchievementRuntimeHarness runtime; - auto& ach1 = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach1.SetID(3U); - ach1.SetTrigger("1=1.10."); - auto& ach2 = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach2.SetID(5U); - ach2.SetTrigger("1=1.2."); + auto* pAch3 = runtime.MockAchievement(3U, "1=1.10."); + auto* pAch5 = runtime.MockAchievement(5U, "1=1.2."); + runtime.SyncToRuntime(); - const gsl::not_null pAchievement3{ - gsl::make_not_null(runtime.mockGameContext.FindAchievement(3U))}; - const gsl::not_null pAchievement5{ - gsl::make_not_null(runtime.mockGameContext.FindAchievement(5U))}; - - pAchievement3->SetConditionHitCount(0, 0, 2); - pAchievement5->SetConditionHitCount(0, 0, 2); + SetConditionHitCount(runtime, 3U, 0, 0, 2); + SetConditionHitCount(runtime, 5U, 0, 0, 2); std::string sBuffer; - int nSize = runtime.SaveProgressToBuffer(nullptr, 0); - sBuffer.resize(nSize); - runtime.SaveProgressToBuffer(sBuffer.data(), nSize); + runtime.SaveProgressToString(sBuffer); - // achievements weren't active, so they weren't persisted - pAchievement3->SetConditionHitCount(0, 0, 1); - pAchievement5->SetConditionHitCount(0, 0, 1); - runtime.LoadProgressFromBuffer(sBuffer.data()); - Assert::AreEqual(0U, pAchievement3->GetConditionHitCount(0, 0)); - Assert::AreEqual(0U, pAchievement5->GetConditionHitCount(0, 0)); + // inactive achievements aren't updated from the persisted data + runtime.DeactivateAchievement(pAch3); + runtime.DeactivateAchievement(pAch5); + SetConditionHitCount(runtime, 3U, 0, 0, 1); + SetConditionHitCount(runtime, 5U, 0, 0, 1); + runtime.LoadProgressFromString(sBuffer); + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 1); // activate one achievement, save, activate other, restore - only first should be restored, // second should be reset because it wasn't captured. - pAchievement3->SetActive(true); - runtime.GetAchievementTrigger(3U)->state = RC_TRIGGER_STATE_ACTIVE; - pAchievement3->SetConditionHitCount(0, 0, 1); - pAchievement5->SetConditionHitCount(0, 0, 1); - nSize = runtime.SaveProgressToBuffer(sBuffer.data(), nSize); - if (nSize > gsl::narrow_cast(sBuffer.size())) - { - sBuffer.resize(nSize); - runtime.SaveProgressToBuffer(sBuffer.data(), nSize); - } - pAchievement5->SetActive(true); - runtime.GetAchievementTrigger(5U)->state = RC_TRIGGER_STATE_ACTIVE; - - pAchievement3->SetConditionHitCount(0, 0, 2); - pAchievement5->SetConditionHitCount(0, 0, 2); - runtime.LoadProgressFromBuffer(sBuffer.data()); - Assert::AreEqual(1U, pAchievement3->GetConditionHitCount(0, 0)); - Assert::AreEqual(0U, pAchievement5->GetConditionHitCount(0, 0)); + runtime.ActivateAchievement(pAch3); + SetConditionHitCount(runtime, 3U, 0, 0, 1); + SetConditionHitCount(runtime, 5U, 0, 0, 1); + runtime.SaveProgressToString(sBuffer); + runtime.ActivateAchievement(pAch5); + SetConditionHitCount(runtime, 3U, 0, 0, 2); + SetConditionHitCount(runtime, 5U, 0, 0, 2); + runtime.LoadProgressFromString(sBuffer); + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 0); // both active, save and restore should reset both - runtime.GetAchievementTrigger(5U)->state = RC_TRIGGER_STATE_ACTIVE; - pAchievement5->SetConditionHitCount(0, 0, 1); - nSize = runtime.SaveProgressToBuffer(sBuffer.data(), nSize); - if (nSize > gsl::narrow_cast(sBuffer.size())) - { - sBuffer.resize(nSize); - runtime.SaveProgressToBuffer(sBuffer.data(), nSize); - } + SetConditionHitCount(runtime, 3U, 0, 0, 1); + SetConditionHitCount(runtime, 5U, 0, 0, 1); + runtime.SaveProgressToString(sBuffer); - pAchievement3->SetConditionHitCount(0, 0, 2); - pAchievement5->SetConditionHitCount(0, 0, 2); - runtime.LoadProgressFromBuffer(sBuffer.data()); - Assert::AreEqual(1U, pAchievement3->GetConditionHitCount(0, 0)); - Assert::AreEqual(1U, pAchievement5->GetConditionHitCount(0, 0)); + SetConditionHitCount(runtime, 3U, 0, 0, 2); + SetConditionHitCount(runtime, 5U, 0, 0, 2); + runtime.LoadProgressFromString(sBuffer); + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 1); // second no longer active, file contains data that should be ignored - pAchievement3->SetConditionHitCount(0, 0, 6); - pAchievement5->SetConditionHitCount(0, 0, 6); - pAchievement5->SetActive(false); - runtime.LoadProgressFromBuffer(sBuffer.data()); - Assert::AreEqual(1U, pAchievement3->GetConditionHitCount(0, 0)); - Assert::AreEqual(6U, pAchievement5->GetConditionHitCount(0, 0)); + SetConditionHitCount(runtime, 3U, 0, 0, 6); + SetConditionHitCount(runtime, 5U, 0, 0, 6); + runtime.DeactivateAchievement(pAch5); + runtime.LoadProgressFromString(sBuffer); + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 6); - // if the definition changes, the hits should be reset instead of remembered - pAchievement3->GenerateConditions(); - pAchievement3->GetCondition(0, 0).CompSource().SetValue(2); - pAchievement3->RebuildTrigger(); - pAchievement3->SetConditionHitCount(0, 0, 2); - Assert::IsTrue(pAchievement3->Active()); - runtime.LoadProgressFromBuffer(sBuffer.data()); - Assert::AreEqual(0U, pAchievement3->GetConditionHitCount(0, 0)); + // if the definition changes (different md5), the hits should be reset instead of remembered + runtime.GetClient()->game->runtime.triggers[0].md5[0]++; + SetConditionHitCount(runtime, 3U, 0, 0, 6); + runtime.ActivateAchievement(pAch3); + runtime.LoadProgressFromString(sBuffer); + AssertConditionHitCount(runtime, 3U, 0, 0, 0); } TEST_METHOD(TestPersistProgressMemory) { AchievementRuntimeHarness runtime; - auto& ach = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach.SetID(9U); - ach.SetTrigger("0xH1234=1_0xX1234>d0xX1234"); - ach.SetActive(true); - runtime.GetAchievementTrigger(9U)->state = RC_TRIGGER_STATE_ACTIVE; + runtime.MockAchievement(9U, "0xH1234=1_0xX1234>d0xX1234"); + runtime.SyncToRuntime(); + auto pMemRefs = runtime.GetClient()->game->runtime.memrefs; - ach.SetConditionHitCount(0, 0, 2); - ach.SetConditionHitCount(0, 1, 0); - auto pMemRef = runtime.GetMemRefs(); // 0xH1234 + SetConditionHitCount(runtime, 9U, 0, 0, 2); + SetConditionHitCount(runtime, 9U, 0, 1, 0); + auto pMemRef = pMemRefs; // 0xH1234 pMemRef->value.value = 0x02; pMemRef->value.changed = 0; pMemRef->value.prior = 0x03; @@ -706,7 +913,7 @@ public: runtime.SaveProgressToFile("test.sav"); // modify data so we can see if the persisted data is restored - pMemRef = runtime.GetMemRefs(); + pMemRef = pMemRefs; pMemRef->value.value = 0; pMemRef->value.changed = 0; pMemRef->value.prior = 0; @@ -714,16 +921,16 @@ public: pMemRef->value.value = 0; pMemRef->value.changed = 0; pMemRef->value.prior = 0; - ach.SetConditionHitCount(0, 0, 1); - ach.SetConditionHitCount(0, 1, 1); + SetConditionHitCount(runtime, 9U, 0, 0, 1); + SetConditionHitCount(runtime, 9U, 0, 1, 1); // restore persisted data runtime.LoadProgressFromFile("test.sav"); - Assert::AreEqual(2U, ach.GetConditionHitCount(0, 0)); - Assert::AreEqual(0U, ach.GetConditionHitCount(0, 1)); + AssertConditionHitCount(runtime, 9U, 0, 0, 2); + AssertConditionHitCount(runtime, 9U, 0, 1, 0); - pMemRef = runtime.GetMemRefs(); + pMemRef = pMemRefs; Assert::AreEqual(0x02U, pMemRef->value.value); Assert::AreEqual(0, (int)pMemRef->value.changed); Assert::AreEqual(0x03U, pMemRef->value.prior); @@ -736,266 +943,111 @@ public: TEST_METHOD(TestLoadProgressV1) { AchievementRuntimeHarness runtime; - auto& ach1 = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach1.SetID(3U); - ach1.SetTrigger("1=1.10."); - auto& ach2 = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach2.SetID(5U); - ach2.SetTrigger("1=1.2."); + runtime.WrapAchievement(runtime.MockAchievement(3U, "1=1.10."))->SetTrigger("1=1.10."); + runtime.WrapAchievement(runtime.MockAchievement(5U, "1=1.2."))->SetTrigger("1=1.2."); + runtime.SyncToRuntime(); - const gsl::not_null pAchievement3{ - gsl::make_not_null(runtime.mockGameContext.FindAchievement(3U)) }; - const gsl::not_null pAchievement5{ - gsl::make_not_null(runtime.mockGameContext.FindAchievement(5U)) }; - - pAchievement3->SetConditionHitCount(0, 0, 2); - pAchievement5->SetConditionHitCount(0, 0, 2); - pAchievement3->SetActive(true); - pAchievement5->SetActive(true); + SetConditionHitCount(runtime, 3U, 0, 0, 2); + SetConditionHitCount(runtime, 5U, 0, 0, 2); runtime.mockFileSystem.MockFile(L"test.sav.rap", "3:1:1:0:0:0:0:6e2301982f40d1a3f311cdb063f57e2f:4f52856e145d7cb05822e8a9675b086b:5:1:1:0:0:0:0:0e9aec1797ad6ba861a4b1e0c7f6d2ab:dd9e5fc6020e728b8c9231d5a5c904d5:"); runtime.LoadProgressFromFile("test.sav"); - Assert::AreEqual(1U, pAchievement3->GetConditionHitCount(0, 0)); - Assert::AreEqual(1U, pAchievement5->GetConditionHitCount(0, 0)); + + AssertConditionHitCount(runtime, 3U, 0, 0, 1); + AssertConditionHitCount(runtime, 5U, 0, 0, 1); } - TEST_METHOD(TestPersistProgressNoCoreGroup) - { - AchievementRuntimeHarness runtime; - auto& ach = runtime.mockGameContext.NewAchievement(Achievement::Category::Core); - ach.SetID(9U); - ach.SetTrigger("S0xH1234=1_0xX1234>d0xX1234"); - ach.SetActive(true); - runtime.GetAchievementTrigger(9U)->state = RC_TRIGGER_STATE_ACTIVE; - - ach.SetConditionHitCount(1, 0, 2); - ach.SetConditionHitCount(1, 1, 0); - auto pMemRef = runtime.GetMemRefs(); // 0xH1234 - pMemRef->value.value = 0x02; - pMemRef->value.changed = 1; - pMemRef->value.prior = 0x03; - pMemRef = pMemRef->next; // 0xX1234 - pMemRef->value.value = 0x020000; - pMemRef->value.changed = 0; - pMemRef->value.prior = 0x040000; - - runtime.SaveProgressToFile("test.sav"); - - // modify data so we can see if the persisted data is restored - pMemRef = runtime.GetMemRefs(); - pMemRef->value.value = 0; - pMemRef->value.changed = 0; - pMemRef->value.prior = 0; - pMemRef = pMemRef->next; - pMemRef->value.value = 0; - pMemRef->value.changed = 0; - pMemRef->value.prior = 0; - ach.SetConditionHitCount(1, 0, 1); - ach.SetConditionHitCount(1, 1, 1); - - // restore persisted data - runtime.LoadProgressFromFile("test.sav"); - - Assert::AreEqual(2U, ach.GetConditionHitCount(1, 0)); - Assert::AreEqual(0U, ach.GetConditionHitCount(1, 1)); - - pMemRef = runtime.GetMemRefs(); - Assert::AreEqual(0x02U, pMemRef->value.value); - Assert::AreEqual(1, (int)pMemRef->value.changed); - Assert::AreEqual(0x03U, pMemRef->value.prior); - pMemRef = pMemRef->next; - Assert::AreEqual(0x020000U, pMemRef->value.value); - Assert::AreEqual(0, (int)pMemRef->value.changed); - Assert::AreEqual(0x040000U, pMemRef->value.prior); - } - */ - - TEST_METHOD(TestReactivateAchievement) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - - auto* sTrigger = "0xH0001=0_0xH0000=1"; - runtime.ActivateAchievement(6U, sTrigger); - auto* pTrigger = runtime.GetAchievementTrigger(6U); - pTrigger->state = RC_TRIGGER_STATE_ACTIVE; - - std::vector vChanges; - runtime.Process(vChanges); - runtime.Process(vChanges); - runtime.Process(vChanges); - Assert::AreEqual(3U, pTrigger->requirement->conditions->current_hits); - - // deactivating achievement should not reset hit count - // so user can examine hit counts after achievement has triggered - runtime.DeactivateAchievement(6U); - Assert::AreEqual(3U, pTrigger->requirement->conditions->current_hits); - - runtime.Process(vChanges); - Assert::AreEqual(3U, pTrigger->requirement->conditions->current_hits); - - // reactivating achievement will reset hit count and allow it to increment again - runtime.ActivateAchievement(6U, sTrigger); - Assert::AreEqual(0U, pTrigger->requirement->conditions->current_hits); - - runtime.Process(vChanges); - Assert::AreEqual(1U, pTrigger->requirement->conditions->current_hits); - } - - TEST_METHOD(TestReleaseAchievementTrigger) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - - const auto* sTrigger = "0xH0001=0_0xH0000=1"; - runtime.ActivateAchievement(6U, sTrigger); - auto* pTrigger = runtime.GetAchievementTrigger(6U); - - // deactivated achievement shouldn't be found without passing in the trigger - runtime.DeactivateAchievement(6U); - auto* pTrigger2 = runtime.GetAchievementTrigger(6U); - Assert::IsNull(pTrigger2); - pTrigger2 = runtime.GetAchievementTrigger(6U, sTrigger); - Assert::IsNotNull(pTrigger2); - Assert::IsTrue(pTrigger == pTrigger2); - - // cannot release trigger that has memrefs - runtime.ReleaseAchievementTrigger(6U, pTrigger); - pTrigger2 = runtime.GetAchievementTrigger(6U); - Assert::IsNull(pTrigger2); - pTrigger2 = runtime.GetAchievementTrigger(6U, sTrigger); - Assert::IsNotNull(pTrigger2); - Assert::IsTrue(pTrigger == pTrigger2); - - // register second trigger that will not have memrefs - runtime.ActivateAchievement(7U, sTrigger); - pTrigger = runtime.GetAchievementTrigger(7U); - runtime.DeactivateAchievement(7U); - pTrigger2 = runtime.GetAchievementTrigger(7U); - Assert::IsNull(pTrigger2); - pTrigger2 = runtime.GetAchievementTrigger(7U, sTrigger); - Assert::IsNotNull(pTrigger2); - Assert::IsTrue(pTrigger == pTrigger2); - - // can release trigger that does not have memrefs - Assert::AreEqual({ 2U }, runtime.GetTriggerCount()); - runtime.ReleaseAchievementTrigger(7U, pTrigger); - Assert::AreEqual({ 1U }, runtime.GetTriggerCount()); - pTrigger2 = runtime.GetAchievementTrigger(7U); - Assert::IsNull(pTrigger2); - pTrigger2 = runtime.GetAchievementTrigger(7U, sTrigger); - Assert::IsNull(pTrigger2); - - // first trigger should still be findable - pTrigger2 = runtime.GetAchievementTrigger(6U, sTrigger); - Assert::IsNotNull(pTrigger2); - } - - TEST_METHOD(TestActivateLeaderboard) + TEST_METHOD(TestDoFrameActivateLeaderboard) { std::array memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 }; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - auto* pLeaderboard = "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02"; + auto* pLeaderboard = runtime.MockLeaderboard(6U, "STA:0xH00=1::CAN:0xH00=2::SUB:0xH00=3::VAL:0xH02"); - // leaderboard not active, should not trigger - std::vector vChanges; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); - - // leaderboard active, but not started - runtime.ActivateLeaderboard(6U, pLeaderboard); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + // expect no events for inactive leaderboard + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // leaderboard cancel condition should not be captured until it's been started memory.at(0) = 2; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // leaderboard start condition should notify the start with initial value memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStarted, 6U); - Assert::AreEqual(52, vChanges.front().nValue); + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 6U); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1U); + runtime.ResetEvents(); // still active leaderboard should not notify unless the value changes - vChanges.clear(); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); memory.at(2) = 33; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardUpdated, 6U); - Assert::AreEqual(33, vChanges.front().nValue); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1U); + runtime.ResetEvents(); // leaderboard cancel condition should notify memory.at(0) = 2; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardCanceled, 6U); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 6U); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1U); + runtime.ResetEvents(); // leaderboard submit condition should not trigger if leaderboard not started memory.at(0) = 3; - vChanges.clear(); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // restart the leaderboard memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStarted, 6U); - Assert::AreEqual(33, vChanges.front().nValue); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 6U); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1U); + runtime.ResetEvents(); // leaderboard submit should trigger if leaderboard started memory.at(0) = 3; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardTriggered, 6U); - Assert::AreEqual(33, vChanges.front().nValue); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 6U); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1U); + runtime.ResetEvents(); // leaderboard cancel condition should not trigger after submission memory.at(0) = 2; - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); // restart the leaderboard memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStarted, 6U); - Assert::AreEqual(33, vChanges.front().nValue); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 6U); + runtime.AssertEvent(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1U); + runtime.ResetEvents(); // deactivated leaderboard should not trigger - runtime.DeactivateLeaderboard(6U); - vChanges.clear(); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DeactivateLeaderboard(pLeaderboard); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); memory.at(0) = 2; - vChanges.clear(); - runtime.Process(vChanges); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); } TEST_METHOD(TestActivateRichPresence) { std::array memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 }; - std::vector changes; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); @@ -1009,32 +1061,31 @@ public: Assert::AreEqual(std::wstring(L"0 0"), runtime.GetRichPresenceDisplayString()); // do_frame should immediately process the rich presence - runtime.Process(changes); + runtime.DoFrame(); Assert::AreEqual(std::wstring(L"18 0"), runtime.GetRichPresenceDisplayString()); // first update - updates value and delta - runtime.Process(changes); + runtime.DoFrame(); Assert::AreEqual(std::wstring(L"18 18"), runtime.GetRichPresenceDisplayString()); // second update - updates delta - runtime.Process(changes); + runtime.DoFrame(); Assert::AreEqual(std::wstring(L"18 18"), runtime.GetRichPresenceDisplayString()); // third update - updates value after change memory.at(1) = 11; - runtime.Process(changes); + runtime.DoFrame(); Assert::AreEqual(std::wstring(L"11 18"), runtime.GetRichPresenceDisplayString()); // fourth update - updates delta and value after change memory.at(1) = 13; - runtime.Process(changes); + runtime.DoFrame(); Assert::AreEqual(std::wstring(L"13 11"), runtime.GetRichPresenceDisplayString()); } TEST_METHOD(TestActivateRichPresenceChange) { std::array memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 }; - std::vector changes; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); @@ -1056,7 +1107,6 @@ public: TEST_METHOD(TestActivateRichPresenceWithError) { std::array memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 }; - std::vector changes; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); @@ -1068,347 +1118,1794 @@ public: Assert::AreEqual(std::wstring(L"Parse error -6 (line 5): Invalid operator"), runtime.GetRichPresenceDisplayString()); } - TEST_METHOD(TestProcessLeaderboardStartPauseOnReset) + TEST_METHOD(TestMonitorAchievementPauseOnReset) { - std::array memory{ 0x00, 0x00 }; + std::array memory{0x00, 0x12, 0x34, 0xAB, 0x56}; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=0_0xH0000=9_R:0xH0001=1::SUB:0xH0000=1::CAN:0xH0000=2::VAL:0xH0000"; + auto* pAchievement = runtime.MockAchievement(4U, "1=1.3._R:0xH0000=1"); + auto* vmAchievement = runtime.WrapAchievement(pAchievement); - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); - - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::IsTrue(pLboard->start.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->start.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - pLeaderboard.SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Start); - - memory.at(1) = 0; - runtime.Process(vChanges); - Assert::IsTrue(pLboard->start.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->start.has_hits); - - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStartReset, 6U); - } - - TEST_METHOD(TestProcessLeaderboardSubmitPauseOnReset) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=1::SUB:0xH0000=0_0xH0000=9_R:0xH0001=1::CAN:0xH0000=2::VAL:0xH0000"; - - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); - - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::IsTrue(pLboard->submit.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->submit.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - pLeaderboard.SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Submit); - - memory.at(1) = 0; - runtime.Process(vChanges); - Assert::IsTrue(pLboard->submit.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->submit.has_hits); - - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardSubmitReset, 6U); - } - - TEST_METHOD(TestProcessLeaderboardCancelPauseOnReset) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=0_0xH0000=9_R:0xH0001=1::VAL:0xH0000"; - - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); - - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::IsTrue(pLboard->cancel.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->cancel.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - pLeaderboard.SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Cancel); - - memory.at(1) = 0; - runtime.Process(vChanges); - Assert::IsTrue(pLboard->cancel.has_hits); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::IsFalse(pLboard->cancel.has_hits); - - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardCancelReset, 6U); - } - - TEST_METHOD(TestProcessLeaderboardValuePauseOnReset) - { - std::array memory{ 0x05, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=0::SUB:0xH0000=2::CAN:0xH0000=3::VAL:Q:0xH0001=0_M:0xH0000"; - - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); - - runtime.ActivateLeaderboard(6U, pDefinition); - auto* pLboard = runtime.GetLeaderboardDefinition(6U); - pLboard->state = RC_LBOARD_STATE_STARTED; // value not calculated if not started - runtime.Process(vChanges); - Assert::AreEqual(5U, pLboard->value.value.value); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardUpdated, 6U); - vChanges.clear(); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::AreEqual(0U, pLboard->value.value.value); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardUpdated, 6U); - vChanges.clear(); - - pLeaderboard.SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Value); - - memory.at(1) = 0; - runtime.Process(vChanges); - Assert::AreEqual(5U, pLboard->value.value.value); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardUpdated, 6U); - vChanges.clear(); - - memory.at(1) = 1; - runtime.Process(vChanges); - Assert::AreEqual(0U, pLboard->value.value.value); - - runtime.Process(vChanges); - Assert::AreEqual({ 2U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardUpdated, 6U); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardValueReset, 6U); - } - - TEST_METHOD(TestProcessLeaderboardStartPauseOnTrigger) - { - std::array memory{ 0x00, 0x00 }; - - AchievementRuntimeHarness runtime; - runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"; - - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); - - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->start.state); - Assert::AreEqual({ 0U }, vChanges.size()); + // HitCount should increase, but no trigger + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(true, pAchievement->trigger->has_hits != 0); + // trigger reset, expect no notification because not watching for one memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->start.state); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStarted, 6U); - vChanges.clear(); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); - pLeaderboard.SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Start); + // no notification if already reset + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + // disable reset, hitcount should increase, no notification memory.at(0) = 0; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->start.state); - Assert::AreEqual({ 0U }, vChanges.size()); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + // trigger reset, watch for notification + vmAchievement->SetPauseOnReset(true); memory.at(0) = 1; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->start.state); + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.mockFrameEventQueue.Reset(); - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardStartTriggered, 6U); + // already reset, shouldn't get repeated notification + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); } - TEST_METHOD(TestProcessLeaderboardSubmitPauseOnTrigger) + TEST_METHOD(TestMonitorLeaderboardStartPauseOnReset) { std::array memory{ 0x00, 0x00 }; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"; + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=0_0xH0000=9_R:0xH0001=1::SUB:0xH0000=1::CAN:0xH0000=2::VAL:0xH0000"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); + // impossible start condition - but it should accumulate hits so we can detect a reset. + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->start.has_hits); - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->submit.state); - Assert::AreEqual({ 0U }, vChanges.size()); + // reset should clear hits, but we're not watching for event yet + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->start.has_hits); - memory.at(0) = 2; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->submit.state); - Assert::AreEqual({ 0U }, vChanges.size()); + // start watching for event and allow hits to accumulate again + vmLeaderboard->SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Start); + memory.at(1) = 0; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->start.has_hits); - pLeaderboard.SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Submit); + // reset should clear hits and raise event + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->start.has_hits); + runtime.mockFrameEventQueue.Reset(); - memory.at(0) = 0; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->submit.state); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(0) = 2; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->submit.state); - - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardSubmitTriggered, 6U); + // persistent reset should not raise event again + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->start.has_hits); } - TEST_METHOD(TestProcessLeaderboardCancelPauseOnTrigger) + TEST_METHOD(TestMonitorLeaderboardSubmitPauseOnReset) { - std::array memory{ 0x00, 0x00 }; + std::array memory{0x00, 0x00}; AchievementRuntimeHarness runtime; runtime.mockEmulatorContext.MockMemory(memory); - std::vector vChanges; - auto* pDefinition = "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"; + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=1::SUB:0xH0000=0_0xH0000=9_R:0xH0001=1::CAN:0xH0000=2::VAL:0xH0000"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); - auto& pLeaderboard = runtime.mockGameContext.Assets().NewLeaderboard(); - pLeaderboard.SetID(6U); + // accumulate hits on submit + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->submit.has_hits); - runtime.ActivateLeaderboard(6U, pDefinition); - const auto* pLboard = runtime.GetLeaderboardDefinition(6U); - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->cancel.state); - Assert::AreEqual({ 0U }, vChanges.size()); + // reset should clear hits, but we're not watching for event yet + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->submit.has_hits); - memory.at(0) = 3; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->cancel.state); - Assert::AreEqual({ 0U }, vChanges.size()); + // start watching for event and allow hits to accumulate again + vmLeaderboard->SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Submit); + memory.at(1) = 0; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->submit.has_hits); - pLeaderboard.SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Cancel); + // reset should clear hits and raise event + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->submit.has_hits); + runtime.mockFrameEventQueue.Reset(); - memory.at(0) = 0; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_ACTIVE, (int)pLboard->cancel.state); - Assert::AreEqual({ 0U }, vChanges.size()); - - memory.at(0) = 3; - runtime.Process(vChanges); - Assert::AreEqual((int)RC_TRIGGER_STATE_TRIGGERED, (int)pLboard->cancel.state); - - runtime.Process(vChanges); - Assert::AreEqual({ 1U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::LeaderboardCancelTriggered, 6U); + // persistent reset should not raise event again + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->submit.has_hits); } - - TEST_METHOD(TestDetectUnsupportedAchievements) + + TEST_METHOD(TestMonitorLeaderboardCancelPauseOnReset) { - ra::data::context::mocks::MockConsoleContext mockConsoleContext(Atari2600, L"Atari 2600"); - Assert::AreEqual(0x7FU, mockConsoleContext.MaxAddress()); - std::array memory{}; + std::array memory{0x00, 0x00}; AchievementRuntimeHarness runtime; - auto& pAch1 = runtime.mockGameContext.Assets().NewAchievement(); - pAch1.SetTrigger("0xH0000=1"); - pAch1.UpdateLocalCheckpoint(); - pAch1.Activate(); - auto& pAch2 = runtime.mockGameContext.Assets().NewAchievement(); - pAch2.SetTrigger("0xH0100=1"); - pAch1.UpdateLocalCheckpoint(); - pAch2.Activate(); - auto& pAch3 = runtime.mockGameContext.Assets().NewAchievement(); - pAch3.SetTrigger("0xH0100=1"); - pAch1.UpdateLocalCheckpoint(); - - Assert::AreEqual(ra::data::models::AssetState::Waiting, pAch1.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Waiting, pAch2.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, pAch3.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - - // no memory registered, can't detect yet - runtime.DetectUnsupportedAchievements(); - Assert::AreEqual(ra::data::models::AssetState::Waiting, pAch1.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Waiting, pAch2.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, pAch3.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - - // unsupported achievements will be detected 100ms after the memory is registered runtime.mockEmulatorContext.MockMemory(memory); - runtime.mockThreadPool.AdvanceTime(std::chrono::milliseconds(100)); + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=0_0xH0000=9_R:0xH0001=1::VAL:0xH0000"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); - // actually disable the achievements that were flagged - std::vector vChanges; - runtime.Process(vChanges); - Assert::AreEqual({ 2U }, vChanges.size()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementActivated, pAch1.GetID()); - AssertChange(vChanges, AchievementRuntime::ChangeType::AchievementDisabled, pAch2.GetID()); + // accumulate hits on cancel + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->cancel.has_hits); - // sync the state - pAch1.DoFrame(); - pAch2.DoFrame(); - pAch3.DoFrame(); + // reset should clear hits, but we're not watching for event yet + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->cancel.has_hits); - Assert::AreEqual(ra::data::models::AssetState::Active, pAch1.GetState()); - Assert::AreEqual(std::wstring(), pAch1.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Disabled, pAch2.GetState()); - Assert::AreEqual(std::wstring(L"Condition 1: Address 0100 out of range (max 007F)"), pAch2.GetValidationError()); - Assert::AreEqual(ra::data::models::AssetState::Inactive, pAch3.GetState()); - Assert::AreEqual(std::wstring(L"Condition 1: Address 0100 out of range (max 007F)"), pAch3.GetValidationError()); + // start watching for event and allow hits to accumulate again + vmLeaderboard->SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Cancel); + memory.at(1) = 0; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsTrue(pLeaderboard->lboard->cancel.has_hits); + + // reset should clear hits and raise event + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->cancel.has_hits); + runtime.mockFrameEventQueue.Reset(); + + // persistent reset should not raise event again + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::IsFalse(pLeaderboard->lboard->cancel.has_hits); } + + TEST_METHOD(TestMonitorLeaderboardValuePauseOnReset) + { + std::array memory{0x00, 0x00, 0x00}; + + AchievementRuntimeHarness runtime; + runtime.mockEmulatorContext.MockMemory(memory); + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=0::SUB:0xH0000=2::CAN:0xH0000=3::VAL:R:0xH0001=1_M:0xH0002=0"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); + + // start the leaderboard so we can start counting hits on the value + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(1U, pLeaderboard->lboard->value.value.value); + runtime.ResetEvents(); + + // reset should clear hits, but we're not watching for event yet + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); // tracker updated + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(0U, pLeaderboard->lboard->value.value.value); + runtime.ResetEvents(); + + // start watching for event and allow hits to accumulate again + vmLeaderboard->SetPauseOnReset(ra::data::models::LeaderboardModel::LeaderboardParts::Value); + memory.at(1) = 0; + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); // tracker updated + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(1U, pLeaderboard->lboard->value.value.value); + runtime.ResetEvents(); + + // reset should clear hits and raise event + memory.at(1) = 1; + runtime.DoFrame(); + Assert::AreEqual({1U}, runtime.GetEventCount()); // tracker updated + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(0U, pLeaderboard->lboard->value.value.value); + runtime.mockFrameEventQueue.Reset(); + runtime.ResetEvents(); + + // persistent reset should not raise event again + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + Assert::AreEqual(0U, pLeaderboard->lboard->value.value.value); + } + + TEST_METHOD(TestMonitorLeaderboardStartPauseOnTrigger) + { + std::array memory{0x00, 0x00}; + + AchievementRuntimeHarness runtime; + runtime.mockEmulatorContext.MockMemory(memory); + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); + + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // start the leaderboard, but we're not watching for event yet + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // cancel the leaderboard + memory.at(0) = 3; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // canceled + tracker hide + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // let leaderboard transition from waiting to active + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + + // start watching for event and restart the leaderboard + vmLeaderboard->SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Start); + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + runtime.mockFrameEventQueue.Reset(); + runtime.ResetEvents(); + } + + TEST_METHOD(TestMonitorLeaderboardSubmitPauseOnTrigger) + { + std::array memory{0x00, 0x00}; + + AchievementRuntimeHarness runtime; + runtime.mockEmulatorContext.MockMemory(memory); + auto* pLeaderboard = runtime.MockLeaderboard(6U, + "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); + + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // start the leaderboard + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // submit the leaderboard, but we're not watching for the event yet + memory.at(0) = 2; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // submitted + tracker hide + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // let leaderboard transition from waiting to active + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + + // start watching for event and restart the leaderboard + vmLeaderboard->SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Submit); + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + runtime.ResetEvents(); + + // submit the leaderboard, expect the event + memory.at(0) = 2; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // submitted + tracker hide + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + } + + TEST_METHOD(TestMonitorLeaderboardCancelPauseOnTrigger) + { + std::array memory{0x00, 0x00}; + + AchievementRuntimeHarness runtime; + runtime.mockEmulatorContext.MockMemory(memory); + auto* pLeaderboard = runtime.MockLeaderboard(6U, "STA:0xH0000=1::SUB:0xH0000=2::CAN:0xH0000=3::VAL:0xH0001"); + auto* vmLeaderboard = runtime.WrapLeaderboard(pLeaderboard); + + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + + // start the leaderboard + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // cancel the leaderboard, but we're not watching for the event yet + memory.at(0) = 3; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // canceled + tracker hide + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + runtime.ResetEvents(); + + // let leaderboard transition from waiting to active + runtime.DoFrame(); + Assert::AreEqual({0U}, runtime.GetEventCount()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumResetTriggers()); + + // start watching for event and restart the leaderboard + vmLeaderboard->SetPauseOnTrigger(ra::data::models::LeaderboardModel::LeaderboardParts::Cancel); + memory.at(0) = 1; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // started + tracker show + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + runtime.ResetEvents(); + + // cancel the leaderboard, expect the event + memory.at(0) = 3; + runtime.DoFrame(); + Assert::AreEqual({2U}, runtime.GetEventCount()); // submitted + tracker hide + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + } + + TEST_METHOD(TestDoFrameInvalidAddressDisablesAchievement) + { + std::array memory{0x00, 0x00}; + + AchievementRuntimeHarness runtime; + runtime.mockEmulatorContext.MockMemory(memory); + auto* pAch6 = runtime.MockAchievement(6U, "0xH0004=1"); + runtime.SyncToRuntime(); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + + Assert::AreEqual(static_cast(RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE), pAch6->public_.state); + Assert::AreEqual(ra::data::models::AssetState::Waiting, vmAch6->GetState()); + + runtime.DoFrame(); + + Assert::AreEqual(static_cast(RC_CLIENT_ACHIEVEMENT_STATE_DISABLED), pAch6->public_.state); + + // model state isn't synced until DoFrame is called. + vmAch6->DoFrame(); + Assert::AreEqual(ra::data::models::AssetState::Disabled, vmAch6->GetState()); + } + + TEST_METHOD(TestHandleAchievementTriggeredEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventNoRichPresence) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L""), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventLocal) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockLocalAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Local Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventUnofficial) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + pAch6->public_.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL; + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Unofficial Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventCompatibilityMode) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Test Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventModified) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + vmAch6->SetTrigger("0xH0000=2"); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Modified Achievement Unlocked LOCALLY"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventLocalModified) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockLocalAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + vmAch6->SetTrigger("0xH0000=2"); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Modified Local Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventMemoryModified) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockEmulatorContext.SetMemoryModified(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked LOCALLY"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Error: RAM tampered with"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventMemoryInsecureHardcore) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.GetClient()->state.hardcore = 1; + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockEmulatorContext.MockMemoryInsecure(true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked LOCALLY"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Error: RAM insecure"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\acherror.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventMemoryInsecureSoftcore) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + runtime.GetClient()->state.hardcore = 0; + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockEmulatorContext.MockMemoryInsecure(true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventOffline) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Offline, true); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Offline Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Assert::IsNull(pPopup); + Assert::AreEqual({0U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleAchievementTriggeredEventPauseOnTrigger) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + vmAch6->SetPauseOnTrigger(true); + runtime.mockGameContext.SetRichPresenceDisplayString(L"Titles"); + runtime.mockGameContext.Assets().FindRichPresence()->Activate(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::AchievementTriggered, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + Assert::AreEqual(ra::data::models::AssetState::Triggered, vmAch6->GetState()); + Assert::AreEqual(std::wstring(L"Titles"), vmAch6->GetUnlockRichPresence()); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::AchievementTriggered, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Achievement Unlocked"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Ach6 (5)"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 6"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::AreEqual({1U}, runtime.mockFrameEventQueue.NumTriggeredTriggers()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleChallengeIndicatorShowEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Challenge, pPopup->GetPopupType()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + } + + TEST_METHOD(TestHandleChallengeIndicatorShowEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleChallengeIndicatorHideEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, + ra::ui::viewmodels::PopupLocation::BottomRight); + runtime.mockOverlayManager.AddChallengeIndicator(6U, ra::ui::ImageType::Badge, "55223"); + auto* pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Assert::IsTrue(pPopup != nullptr && !pPopup->IsDestroyPending()); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Assert::IsTrue(pPopup == nullptr || pPopup->IsDestroyPending()); + } + + TEST_METHOD(TestHandleChallengeIndicatorHideEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Challenge, + ra::ui::viewmodels::PopupLocation::BottomRight); + auto* pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Assert::IsNull(pPopup); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + pPopup = runtime.mockOverlayManager.GetChallengeIndicator(6U); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleProgressIndicatorShowEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + snprintf(pAch6->public_.measured_progress, sizeof(pAch6->public_.measured_progress), "6/10"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Progress, pPopup->GetPopupType()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345_lock"), pPopup->GetImage().Name()); + Assert::AreEqual(std::wstring(L"6/10"), pPopup->GetText()); + } + + TEST_METHOD(TestHandleProgressIndicatorShowEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + snprintf(pAch6->public_.measured_progress, sizeof(pAch6->public_.measured_progress), "6/10"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleProgressIndicatorShowEventLocalBadge) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + snprintf(pAch6->public_.measured_progress, sizeof(pAch6->public_.measured_progress), "6/10"); + memcpy(pAch6->public_.badge_name, "012345", 7); + auto* vmAch6 = runtime.WrapAchievement(pAch6); + vmAch6->SetBadge(L"local\\abcdefg.png"); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Progress, pPopup->GetPopupType()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("local\\abcdefg.png"), pPopup->GetImage().Name()); + Assert::AreEqual(std::wstring(L"6/10"), pPopup->GetText()); + } + + TEST_METHOD(TestHandleProgressIndicatorUpdateEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + snprintf(pAch6->public_.measured_progress, sizeof(pAch6->public_.measured_progress), "6/10"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, + ra::ui::viewmodels::PopupLocation::BottomRight); + runtime.mockOverlayManager.UpdateProgressTracker(ra::ui::ImageType::Badge, "000001", L"4/10"); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + event.achievement = &pAch6->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Progress, pPopup->GetPopupType()); + Assert::AreEqual(ra::ui::ImageType::Badge, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345_lock"), pPopup->GetImage().Name()); + Assert::AreEqual(std::wstring(L"6/10"), pPopup->GetText()); + } + + TEST_METHOD(TestHandleProgressIndicatorHideEvent) + { + AchievementRuntimeHarness runtime; + auto* pAch6 = runtime.MockAchievement(6U, "0xH0000=1"); + snprintf(pAch6->public_.measured_progress, sizeof(pAch6->public_.measured_progress), "6/10"); + memcpy(pAch6->public_.badge_name, "012345", 7); + runtime.WrapAchievement(pAch6); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, + ra::ui::viewmodels::PopupLocation::BottomRight); + runtime.mockOverlayManager.UpdateProgressTracker(ra::ui::ImageType::Badge, "000001", L"4/10"); + auto* pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Assert::IsTrue(pPopup != nullptr && !pPopup->IsDestroyPending()); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + runtime.RaiseEvent(event); + + pPopup = runtime.mockOverlayManager.GetProgressTracker(); + Assert::IsTrue(pPopup == nullptr || pPopup->IsDestroyPending()); + } + + TEST_METHOD(TestHandleGameCompletedEventHardcore) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.MockAchievement(6U, "0xH0000=1")->public_.points = 5; + runtime.MockAchievement(7U, "0xH0000=1")->public_.points = 1; + runtime.MockAchievement(8U, "0xH0000=1")->public_.points = 1; + runtime.MockAchievement(9U, "0xH0000=1")->public_.points = 3; + runtime.MockAchievement(10U, "0xH0000=1")->public_.points = 10; + runtime.MockAchievement(11U, "0xH0000=1")->public_.points = 5; + runtime.MockAchievement(12U, "0xH0000=1")->public_.points = 10; + runtime.MockAchievement(13U, "0xH0000=1")->public_.points = 5; + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, + ra::ui::viewmodels::PopupLocation::TopMiddle); + runtime.mockSessionTracker.MockSession(1U, time(NULL), std::chrono::seconds(20000)); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Mastery, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Mastered Game Title"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"8 achievements, 40 points"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"UserDisplay | Play time: 5h33m"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Icon, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleGameCompletedEventSoftcore) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + runtime.MockAchievement(6U, "0xH0000=1")->public_.points = 5; + runtime.MockAchievement(7U, "0xH0000=1")->public_.points = 1; + runtime.MockAchievement(8U, "0xH0000=1")->public_.points = 1; + runtime.MockAchievement(9U, "0xH0000=1")->public_.points = 3; + runtime.MockAchievement(10U, "0xH0000=1")->public_.points = 10; + runtime.MockAchievement(11U, "0xH0000=1")->public_.points = 5; + runtime.MockAchievement(12U, "0xH0000=1")->public_.points = 10; + runtime.MockAchievement(13U, "0xH0000=1")->public_.points = 5; + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Mastery, + ra::ui::viewmodels::PopupLocation::TopMiddle); + runtime.mockSessionTracker.MockSession(1U, time(NULL), std::chrono::seconds(20000)); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Mastery, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Completed Game Title"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"8 achievements, 40 points"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"UserDisplay | Play time: 5h33m"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::Icon, pPopup->GetImage().Type()); + Assert::AreEqual(std::string("012345"), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav")); + } + + TEST_METHOD(TestHandleLeaderboardStartedEvent) + { + AchievementRuntimeHarness runtime; + auto *pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardStarted, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard attempt started"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 4"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\lb.wav")); + } + + TEST_METHOD(TestHandleLeaderboardStartedEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardStarted, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleLeaderboardFailedEvent) + { + AchievementRuntimeHarness runtime; + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, + ra::ui::viewmodels::PopupLocation::BottomLeft); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardCanceled, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard attempt failed"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Description 4"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\lbcancel.wav")); + } + + TEST_METHOD(TestHandleLeaderboardFailedEventNoPopup) + { + AchievementRuntimeHarness runtime; + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardCanceled, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + // Submitted event doesn't do anything unless a problem occurs - scoreboard event is used instead + Assert::IsNull(runtime.mockOverlayManager.GetMessage(1)); + Assert::IsNull(runtime.mockOverlayManager.GetScoreboard(4U)); + } + +private: + void AssertSimpleScoreboard(AchievementRuntimeHarness& runtime, ra::LeaderboardID nId) + { + auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(nId); + Assert::IsNotNull(pScoreboard); + Ensures(pScoreboard != nullptr); + + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardScoreboard, pScoreboard->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pScoreboard->GetHeaderText()); + Assert::AreEqual({1U}, pScoreboard->Entries().Count()); + + const auto* pEntry = pScoreboard->Entries().GetItemAt(0); + Expects(pEntry != nullptr); + Assert::AreEqual(0, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"1:23.45"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"User_"), pEntry->GetUserName()); + Assert::IsTrue(pEntry->IsHighlighted()); + } + +public: + TEST_METHOD(TestHandleLeaderboardSubmittedEventLocal) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLocalLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Local Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Local leaderboards are not submitted."), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventCompatibilityMode) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockGameContext.SetMode(ra::data::context::GameContext::Mode::CompatibilityTest); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Leaderboards are not submitted in compatibility test mode."), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventModified) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + auto* vmLbd4 = runtime.WrapLeaderboard(pLbd4); + vmLbd4->SetDescription(L"Modified Description"); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Modified Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Modified leaderboards are not submitted."), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventUnpublished) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + auto* vmLbd4 = runtime.WrapLeaderboard(pLbd4); + vmLbd4->SetDescription(L"Modified Description"); + vmLbd4->UpdateLocalCheckpoint(); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Modified Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Modified leaderboards are not submitted."), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventModifiedMemory) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockEmulatorContext.MockMemoryModified(true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Error: RAM tampered with"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventSoftcore) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, false); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Submission requires Hardcore mode"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventMemoryInsecure) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockEmulatorContext.MockMemoryInsecure(true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Error: RAM insecure"), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardSubmittedEventOffline) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Offline, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + pLbd4->public_.tracker_value = "1:23.45"; + runtime.WrapLeaderboard(pLbd4); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + event.leaderboard = &pLbd4->public_; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetMessage(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::Message, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard Submitted"), pPopup->GetTitle()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pPopup->GetDescription()); + Assert::AreEqual(std::wstring(L"Leaderboards are not submitted in offline mode."), pPopup->GetDetail()); + Assert::AreEqual(ra::ui::ImageType::None, pPopup->GetImage().Type()); + Assert::AreEqual(std::string(), pPopup->GetImage().Name()); + Assert::IsTrue(runtime.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\info.wav")); + + AssertSimpleScoreboard(runtime, 4U); + } + + TEST_METHOD(TestHandleLeaderboardTrackerShowEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_leaderboard_tracker_t tracker; + memset(&tracker, 0, sizeof(tracker)); + snprintf(tracker.display, sizeof(tracker.display), "1:23.45"); + tracker.id = 1; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + event.leaderboard_tracker = &tracker; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardTracker, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"1:23.45"), pPopup->GetDisplayText()); + } + + TEST_METHOD(TestHandleLeaderboardTrackerShowEventNoPopup) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_leaderboard_tracker_t tracker; + memset(&tracker, 0, sizeof(tracker)); + snprintf(tracker.display, sizeof(tracker.display), "1:23.45"); + tracker.id = 1; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + event.leaderboard_tracker = &tracker; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleLeaderboardTrackerUpdateEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::BottomRight); + runtime.mockOverlayManager.AddScoreTracker(1U).SetDisplayText(L"XXX"); + + rc_client_leaderboard_tracker_t tracker; + memset(&tracker, 0, sizeof(tracker)); + snprintf(tracker.display, sizeof(tracker.display), "1:23.45"); + tracker.id = 1; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + event.leaderboard_tracker = &tracker; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1); + Expects(pPopup != nullptr); + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardTracker, pPopup->GetPopupType()); + Assert::AreEqual(std::wstring(L"1:23.45"), pPopup->GetDisplayText()); + } + + TEST_METHOD(TestHandleLeaderboardTrackerUpdateEventNoPopup) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_leaderboard_tracker_t tracker; + memset(&tracker, 0, sizeof(tracker)); + snprintf(tracker.display, sizeof(tracker.display), "1:23.45"); + tracker.id = 1; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + event.leaderboard_tracker = &tracker; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1); + Assert::IsNull(pPopup); + } + + TEST_METHOD(TestHandleLeaderboardTrackerHideEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::BottomRight); + runtime.mockOverlayManager.AddScoreTracker(1U).SetDisplayText(L"XXX"); + + rc_client_leaderboard_tracker_t tracker; + memset(&tracker, 0, sizeof(tracker)); + snprintf(tracker.display, sizeof(tracker.display), "1:23.45"); + tracker.id = 1; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + event.leaderboard_tracker = &tracker; + runtime.RaiseEvent(event); + + auto* pPopup = runtime.mockOverlayManager.GetScoreTracker(1); + Assert::IsTrue(pPopup == nullptr || pPopup->IsDestroyPending()); + } + + TEST_METHOD(TestHandleLeaderboardScoreboardEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_leaderboard_scoreboard_entry_t entries[3]; + memset(entries, 0, sizeof(entries)); + rc_client_leaderboard_scoreboard_t scoreboard; + memset(&scoreboard, 0, sizeof(scoreboard)); + scoreboard.leaderboard_id = 4U; + scoreboard.new_rank = 2; + snprintf(scoreboard.best_score, sizeof(scoreboard.best_score), "1:20.00"); + snprintf(scoreboard.submitted_score, sizeof(scoreboard.submitted_score), "1:23.45"); + scoreboard.num_entries = 3; + scoreboard.num_top_entries = 3; + scoreboard.top_entries = entries; + entries[0].rank = 1; + snprintf(entries[0].score, sizeof(entries[0].score), "1:19.55"); + entries[0].username = "Bob"; + entries[1].rank = 2; + snprintf(entries[1].score, sizeof(entries[0].score), "1:20.00"); + entries[1].username = "User_"; + entries[2].rank = 3; + snprintf(entries[2].score, sizeof(entries[0].score), "1:20.12"); + entries[2].username = "Tara"; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + event.leaderboard = &pLbd4->public_; + event.leaderboard_scoreboard = &scoreboard; + runtime.RaiseEvent(event); + + auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(4U); + Assert::IsNotNull(pScoreboard); + Ensures(pScoreboard != nullptr); + + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardScoreboard, pScoreboard->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pScoreboard->GetHeaderText()); + Assert::AreEqual({3U}, pScoreboard->Entries().Count()); + + const auto* pEntry = pScoreboard->Entries().GetItemAt(0); + Expects(pEntry != nullptr); + Assert::AreEqual(1, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"1:19.55"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Bob"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(1); + Expects(pEntry != nullptr); + Assert::AreEqual(2, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"(1:23.45) 1:20.00"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"User_"), pEntry->GetUserName()); + Assert::IsTrue(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(2); + Expects(pEntry != nullptr); + Assert::AreEqual(3, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"1:20.12"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Tara"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + } + + TEST_METHOD(TestHandleLeaderboardScoreboardEventNoPopup) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::None); + + rc_client_leaderboard_scoreboard_entry_t entries[3]; + memset(entries, 0, sizeof(entries)); + rc_client_leaderboard_scoreboard_t scoreboard; + memset(&scoreboard, 0, sizeof(scoreboard)); + scoreboard.leaderboard_id = 4U; + scoreboard.new_rank = 2; + snprintf(scoreboard.best_score, sizeof(scoreboard.best_score), "1:20.00"); + snprintf(scoreboard.submitted_score, sizeof(scoreboard.submitted_score), "1:23.45"); + scoreboard.num_entries = 3; + scoreboard.num_top_entries = 3; + scoreboard.top_entries = entries; + entries[0].rank = 1; + snprintf(entries[0].score, sizeof(entries[0].score), "1:19.55"); + entries[0].username = "Bob"; + entries[1].rank = 2; + snprintf(entries[1].score, sizeof(entries[1].score), "1:20.00"); + entries[1].username = "User_"; + entries[2].rank = 3; + snprintf(entries[2].score, sizeof(entries[2].score), "1:20.12"); + entries[2].username = "Tara"; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + event.leaderboard = &pLbd4->public_; + event.leaderboard_scoreboard = &scoreboard; + runtime.RaiseEvent(event); + + auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(4U); + Assert::IsNull(pScoreboard); + } + + TEST_METHOD(TestHandleLeaderboardScoreboardEventPlayerNotInTopEntries) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + auto* pLbd4 = runtime.MockLeaderboard(4U, "STA:0xH0000=1::CAN:0xH0000=1::SUB:0xH0000=2::VAL:0xH0001"); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardScoreboard, + ra::ui::viewmodels::PopupLocation::BottomRight); + + rc_client_leaderboard_scoreboard_entry_t entries[7]; + memset(entries, 0, sizeof(entries)); + rc_client_leaderboard_scoreboard_t scoreboard; + memset(&scoreboard, 0, sizeof(scoreboard)); + scoreboard.leaderboard_id = 4U; + scoreboard.new_rank = 27; + snprintf(scoreboard.best_score, sizeof(scoreboard.best_score), "1:20.00"); + snprintf(scoreboard.submitted_score, sizeof(scoreboard.submitted_score), "1:23.45"); + scoreboard.num_entries = 31; + scoreboard.num_top_entries = 7; + scoreboard.top_entries = entries; + entries[0].rank = 1; + snprintf(entries[0].score, sizeof(entries[0].score), "0:56.55"); + entries[0].username = "Bob"; + entries[1].rank = 2; + snprintf(entries[1].score, sizeof(entries[1].score), "0:57.00"); + entries[1].username = "Calvin"; + entries[2].rank = 3; + snprintf(entries[2].score, sizeof(entries[2].score), "0:57.12"); + entries[2].username = "Debbie"; + entries[3].rank = 4; + snprintf(entries[3].score, sizeof(entries[3].score), "0:58.22"); + entries[3].username = "Erin"; + entries[4].rank = 4; + snprintf(entries[4].score, sizeof(entries[4].score), "0:58.22"); + entries[4].username = "Frank"; + entries[5].rank = 6; + snprintf(entries[5].score, sizeof(entries[5].score), "0:59.88"); + entries[5].username = "George"; + entries[6].rank = 7; + snprintf(entries[6].score, sizeof(entries[6].score), "1:00.41"); + entries[6].username = "Harry"; + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD; + event.leaderboard = &pLbd4->public_; + event.leaderboard_scoreboard = &scoreboard; + runtime.RaiseEvent(event); + + auto* pScoreboard = runtime.mockOverlayManager.GetScoreboard(4U); + Assert::IsNotNull(pScoreboard); + Ensures(pScoreboard != nullptr); + + Assert::AreEqual(ra::ui::viewmodels::Popup::LeaderboardScoreboard, pScoreboard->GetPopupType()); + Assert::AreEqual(std::wstring(L"Leaderboard4"), pScoreboard->GetHeaderText()); + Assert::AreEqual({7U}, pScoreboard->Entries().Count()); + + const auto* pEntry = pScoreboard->Entries().GetItemAt(0); + Expects(pEntry != nullptr); + Assert::AreEqual(1, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:56.55"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Bob"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(1); + Expects(pEntry != nullptr); + Assert::AreEqual(2, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:57.00"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Calvin"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(2); + Expects(pEntry != nullptr); + Assert::AreEqual(3, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:57.12"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Debbie"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(3); + Expects(pEntry != nullptr); + Assert::AreEqual(4, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:58.22"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Erin"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(4); + Expects(pEntry != nullptr); + Assert::AreEqual(4, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:58.22"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"Frank"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(5); + Expects(pEntry != nullptr); + Assert::AreEqual(6, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"0:59.88"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"George"), pEntry->GetUserName()); + Assert::IsFalse(pEntry->IsHighlighted()); + + pEntry = pScoreboard->Entries().GetItemAt(6); + Expects(pEntry != nullptr); + Assert::AreEqual(27, pEntry->GetRank()); + Assert::AreEqual(std::wstring(L"(1:23.45) 1:20.00"), pEntry->GetScore()); + Assert::AreEqual(std::wstring(L"User_"), pEntry->GetUserName()); + Assert::IsTrue(pEntry->IsHighlighted()); + } + + // TODO: TestHandleServerError + + TEST_METHOD(TestHandleResetEvent) + { + AchievementRuntimeHarness runtime; + runtime.mockConfiguration.SetFeatureEnabled(ra::services::Feature::Hardcore, true); + runtime.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::LeaderboardTracker, + ra::ui::viewmodels::PopupLocation::BottomRight); + + bool bWasReset = false; + runtime.mockEmulatorContext.SetResetFunction([&bWasReset]() { bWasReset = true; }); + + rc_client_event_t event; + memset(&event, 0, sizeof(event)); + event.type = RC_CLIENT_EVENT_RESET; + runtime.RaiseEvent(event); + + Assert::IsTrue(bWasReset); + } + }; } // namespace tests diff --git a/tests/ui/UIAsserts.hh b/tests/ui/UIAsserts.hh index c5803f48..85660f81 100644 --- a/tests/ui/UIAsserts.hh +++ b/tests/ui/UIAsserts.hh @@ -5,6 +5,7 @@ #include "ui\ImageReference.hh" #include "ui\WindowViewModelBase.hh" #include "ui\viewmodels\MessageBoxViewModel.hh" +#include "ui\viewmodels\PopupViewModelBase.hh" namespace Microsoft { namespace VisualStudio { @@ -90,6 +91,36 @@ template<> std::wstring ToString(const ra::ui::ImageType& typ } } +template<> +std::wstring ToString(const ra::ui::viewmodels::Popup& type) +{ + switch (type) + { + case ra::ui::viewmodels::Popup::None: + return L"None"; + case ra::ui::viewmodels::Popup::Message: + return L"Message"; + case ra::ui::viewmodels::Popup::AchievementTriggered: + return L"AchievementTriggered"; + case ra::ui::viewmodels::Popup::Mastery: + return L"Mastery"; + case ra::ui::viewmodels::Popup::LeaderboardStarted: + return L"LeaderboardStarted"; + case ra::ui::viewmodels::Popup::LeaderboardCanceled: + return L"LeaderboardCanceled"; + case ra::ui::viewmodels::Popup::LeaderboardTracker: + return L"LeaderboardTracker"; + case ra::ui::viewmodels::Popup::LeaderboardScoreboard: + return L"LeaderboardScoreboard"; + case ra::ui::viewmodels::Popup::Challenge: + return L"Challenge"; + case ra::ui::viewmodels::Popup::Progress: + return L"Progress"; + default: + return std::to_wstring(ra::etoi(type)); + } +} + #pragma warning(pop) } // namespace CppUnitTestFramework diff --git a/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp b/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp index 6cfa9966..e318a6e8 100644 --- a/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp +++ b/tests/ui/viewmodels/AssetEditorViewModel_Tests.cpp @@ -44,6 +44,8 @@ private: { // DoFrame tests expect the dialog to be visible GSL_SUPPRESS_F6 SetIsVisible(true); + + mockRuntime.MockGame(); } ~AssetEditorViewModelHarness() @@ -853,62 +855,64 @@ public: TEST_METHOD(TestSyncState) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetState(AssetState::Active); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); + vmAch->SetState(AssetState::Active); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual(AssetState::Active, editor.GetState()); - Assert::AreEqual(AssetState::Active, achievement.GetState()); + Assert::AreEqual(AssetState::Active, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Active"), editor.GetWaitingLabel()); editor.SetState(AssetState::Paused); Assert::AreEqual(AssetState::Paused, editor.GetState()); - Assert::AreEqual(AssetState::Paused, achievement.GetState()); + Assert::AreEqual(AssetState::Paused, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Active"), editor.GetWaitingLabel()); - achievement.SetState(AssetState::Primed); + vmAch->SetState(AssetState::Primed); Assert::AreEqual(AssetState::Primed, editor.GetState()); - Assert::AreEqual(AssetState::Primed, achievement.GetState()); + Assert::AreEqual(AssetState::Primed, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Active"), editor.GetWaitingLabel()); editor.SetState(AssetState::Triggered); Assert::AreEqual(AssetState::Triggered, editor.GetState()); - Assert::AreEqual(AssetState::Triggered, achievement.GetState()); + Assert::AreEqual(AssetState::Triggered, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Active"), editor.GetWaitingLabel()); - achievement.SetState(AssetState::Waiting); + vmAch->SetState(AssetState::Waiting); Assert::AreEqual(AssetState::Waiting, editor.GetState()); - Assert::AreEqual(AssetState::Waiting, achievement.GetState()); + Assert::AreEqual(AssetState::Waiting, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Waiting"), editor.GetWaitingLabel()); - achievement.SetState(AssetState::Active); + vmAch->SetState(AssetState::Active); Assert::AreEqual(AssetState::Active, editor.GetState()); - Assert::AreEqual(AssetState::Active, achievement.GetState()); + Assert::AreEqual(AssetState::Active, vmAch->GetState()); Assert::AreEqual(std::wstring(L"Active"), editor.GetWaitingLabel()); } TEST_METHOD(TestSyncStateMeasured) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetTrigger("M:0xH1234=6.11."); - achievement.SetState(AssetState::Inactive); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - editor.LoadAsset(&achievement); + editor.mockRuntime.ActivateAchievement(1234U, "M:0xH1234=6.11."); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); + vmAch->SetState(AssetState::Inactive); + + editor.LoadAsset(vmAch); Assert::IsTrue(editor.HasMeasured()); Assert::AreEqual(std::wstring(L"[Not Active]"), editor.GetMeasuredValue()); - achievement.SetState(AssetState::Waiting); + vmAch->SetState(AssetState::Waiting); Assert::AreEqual(std::wstring(L"0/11"), editor.GetMeasuredValue()); - achievement.SetState(AssetState::Active); + vmAch->SetState(AssetState::Active); Assert::AreEqual(std::wstring(L"0/11"), editor.GetMeasuredValue()); - achievement.SetState(AssetState::Triggered); + vmAch->SetState(AssetState::Triggered); Assert::AreEqual(std::wstring(L"[Not Active]"), editor.GetMeasuredValue()); } @@ -1225,14 +1229,12 @@ public: { AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); // make sure the record got loaded into the runtime const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1253,7 +1255,7 @@ public: pCondition->SetTargetValue(2U); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0xH1234=2"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1234=2"), vmAch->GetTrigger()); // make sure the runtime record got updated pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1277,18 +1279,13 @@ public: { AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Deactivate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); + vmAch->Deactivate(); - editor.LoadAsset(&achievement); - - // make sure the record did not get loaded into the runtime - const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1304,11 +1301,7 @@ public: pCondition->SetTargetValue(2U); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0xH1234=2"), achievement.GetTrigger()); - - // make sure the record did not get loaded into the runtime - pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + Assert::AreEqual(std::string("0xH1234=2"), vmAch->GetTrigger()); // make sure the UI kept the updated value Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); @@ -1327,14 +1320,12 @@ public: { AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); // make sure the record got loaded into the runtime const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1352,10 +1343,10 @@ public: Assert::AreEqual((int)TriggerOperandType::Value, (int)pCondition->GetTargetType()); Assert::AreEqual(std::wstring(L"1"), pCondition->GetTargetValue()); - achievement.SetTrigger("0xH1234=2"); + vmAch->SetTrigger("0xH1234=2"); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0xH1234=2"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1234=2"), vmAch->GetTrigger()); // make sure the runtime record got updated pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1379,14 +1370,12 @@ public: { AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("M:0xH1234=1.1._0xH2345=1.2."); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "M:0xH1234=1.1._0xH2345=1.2."); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); // make sure the record got loaded into the runtime const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1396,16 +1385,14 @@ public: editor.Trigger().Conditions().GetItemAt(1)->SetType(ra::ui::viewmodels::TriggerConditionType::Measured); // make sure the trigger definition got updated - Assert::AreEqual(std::string("M:0xH1234=1.1._M:0xH2345=1.2."), achievement.GetTrigger()); + Assert::AreEqual(std::string("M:0xH1234=1.1._M:0xH2345=1.2."), vmAch->GetTrigger()); // make sure the error got set Assert::AreEqual(std::wstring(L"Multiple measured targets"), editor.GetAssetValidationError()); Assert::IsTrue(editor.HasAssetValidationError()); // make sure the achievement was disabled - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + Assert::AreEqual(AssetState::Inactive, vmAch->GetState()); // make sure the UI kept the change Assert::AreEqual({ 2U }, editor.Trigger().Conditions().Count()); @@ -1417,16 +1404,14 @@ public: editor.Trigger().Conditions().GetItemAt(0)->SetType(ra::ui::viewmodels::TriggerConditionType::Standard); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0xH1234=1.1._M:0xH2345=1.2."), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1234=1.1._M:0xH2345=1.2."), vmAch->GetTrigger()); // make sure the error got cleared Assert::AreEqual(std::wstring(L""), editor.GetAssetValidationError()); Assert::IsFalse(editor.HasAssetValidationError()); // the achievement should have remained disabled - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + Assert::AreEqual(AssetState::Inactive, vmAch->GetState()); // make sure the UI kept the change Assert::AreEqual({ 2U }, editor.Trigger().Conditions().Count()); @@ -1441,14 +1426,12 @@ public: TEST_METHOD(TestTriggerUpdatedInvalidAlts) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0=0SM:0xH1234=1.1.S0xH2345=1.2."); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0=0SM:0xH1234=1.1.S0xH2345=1.2."); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); // make sure the record got loaded into the runtime const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1459,16 +1442,14 @@ public: editor.Trigger().Conditions().GetItemAt(0)->SetType(ra::ui::viewmodels::TriggerConditionType::Measured); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0=0SM:0xH1234=1.1.SM:0xH2345=1.2."), achievement.GetTrigger()); + Assert::AreEqual(std::string("0=0SM:0xH1234=1.1.SM:0xH2345=1.2."), vmAch->GetTrigger()); // make sure the error got set Assert::AreEqual(std::wstring(L"Multiple measured targets"), editor.GetAssetValidationError()); Assert::IsTrue(editor.HasAssetValidationError()); // make sure the achievement was disabled - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + Assert::AreEqual(AssetState::Inactive, vmAch->GetState()); // make sure the UI kept the change Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); @@ -1504,16 +1485,14 @@ public: pCondition->SetRequiredHits(2); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0=0SM:0xH1234=1.2.SM:0xH2345=1.2."), achievement.GetTrigger()); + Assert::AreEqual(std::string("0=0SM:0xH1234=1.2.SM:0xH2345=1.2."), vmAch->GetTrigger()); // make sure the error got cleared Assert::AreEqual(std::wstring(L""), editor.GetAssetValidationError()); Assert::IsFalse(editor.HasAssetValidationError()); // the achievement should have remained disabled - Assert::AreEqual(AssetState::Inactive, achievement.GetState()); - pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); - Assert::IsNull(pTrigger); + Assert::AreEqual(AssetState::Inactive, vmAch->GetState()); // make sure the UI kept the change pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1535,14 +1514,12 @@ public: AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); editor.Trigger().SetSelectedGroupIndex(0); auto& condition = *editor.Trigger().Conditions().GetItemAt(0); @@ -1552,13 +1529,13 @@ public: // change to address switches size to match source condition.SetTargetType(TriggerOperandType::Delta); - Assert::AreEqual(std::string("0xH1234=d0xH0001"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1234=d0xH0001"), vmAch->GetTrigger()); Assert::AreEqual(initialVersion + 1, editor.GetTriggerVersion()); Assert::IsNotNull(group.m_pConditionSet); // change to value sets size back to 32-bit condition.SetTargetType(TriggerOperandType::Value); - Assert::AreEqual(std::string("0xH1234=1"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1234=1"), vmAch->GetTrigger()); Assert::AreEqual(initialVersion + 2, editor.GetTriggerVersion()); Assert::IsNotNull(group.m_pConditionSet); } @@ -1567,14 +1544,12 @@ public: { AssetEditorViewModelHarness editor; editor.mockConfiguration.SetFeatureEnabled(ra::services::Feature::PreferDecimal, true); - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1_0xH1235=2"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1_0xH1235=2"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); // make sure the record got loaded into the runtime const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); @@ -1585,7 +1560,7 @@ public: editor.Trigger().MoveSelectedConditionsUp(); // make sure the trigger definition got updated - Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), vmAch->GetTrigger()); Assert::IsNotNull(editor.Trigger().Groups().GetItemAt(0)->m_pConditionSet); // make sure the UI picked up the updated value @@ -1605,7 +1580,7 @@ public: editor.Trigger().MoveSelectedConditionsUp(); // make sure the trigger definition did not update - Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), achievement.GetTrigger()); + Assert::AreEqual(std::string("0xH1235=2_0xH1234=1"), vmAch->GetTrigger()); Assert::IsNotNull(editor.Trigger().Groups().GetItemAt(0)->m_pConditionSet); // make sure the UI did not update @@ -1623,17 +1598,14 @@ public: TEST_METHOD(TestValueUpdated) { AssetEditorViewModelHarness editor; - LeaderboardModel leaderboard; - leaderboard.SetID(1234U); - leaderboard.SetStartTrigger("0x8000=0"); - leaderboard.SetSubmitTrigger("0x8000=1"); - leaderboard.SetCancelTrigger("0x8000=2"); - leaderboard.SetValueDefinition("M:0xH1234"); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); + const std::string sDefinition = "STA:0x8000=0::SUB:0x8000=1::CAN:0x8000=2::VAL:M:0xH1234"; + editor.mockRuntime.ActivateLeaderboard(1234U, sDefinition); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmLbd = editor.mockGameContext.Assets().FindLeaderboard(1234U); + vmLbd->SetDefinition(sDefinition); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&leaderboard); + editor.LoadAsset(vmLbd); editor.SetSelectedLeaderboardPart(AssetEditorViewModel::LeaderboardPart::Value); // make sure the record got loaded into the runtime @@ -1648,10 +1620,10 @@ public: Assert::AreEqual(std::wstring(L"0x1234"), pCondition->GetSourceValue()); Assert::AreEqual((int)TriggerOperatorType::None, (int)pCondition->GetOperator()); - leaderboard.SetValueDefinition("M:0xH2222"); + vmLbd->SetValueDefinition("M:0xH2222"); // make sure the value definition got updated - Assert::AreEqual(std::string("M:0xH2222"), leaderboard.GetValueDefinition()); + Assert::AreEqual(std::string("M:0xH2222"), vmLbd->GetValueDefinition()); // make sure the runtime record got updated pLeaderboard = editor.mockRuntime.GetLeaderboardDefinition(1234U); @@ -1671,7 +1643,7 @@ public: pCondition->SetSourceValue(0x3333U); // make sure the value definition got updated - Assert::AreEqual(std::string("M:0xH3333"), leaderboard.GetValueDefinition()); + Assert::AreEqual(std::string("M:0xH3333"), vmLbd->GetValueDefinition()); // make sure the runtime record got updated pLeaderboard = editor.mockRuntime.GetLeaderboardDefinition(1234U); @@ -1682,14 +1654,12 @@ public: TEST_METHOD(TestDoFrameUpdatesHitsFromActiveAchievement) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1708,15 +1678,13 @@ public: TEST_METHOD(TestDoFrameUpdatesHitsFromActiveAchievementWhenHidden) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); editor.SetIsVisible(false); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1742,14 +1710,12 @@ public: TEST_METHOD(TestDoFrameUpdatesMeasuredFromActiveAchievement) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("M:0xH1234=1.99."); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "M:0xH1234=1.99."); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); Expects(pTrigger != nullptr); @@ -1763,18 +1729,12 @@ public: TEST_METHOD(TestDoFrameUpdatesMeasuredFromActiveLeaderboard) { AssetEditorViewModelHarness editor; - LeaderboardModel leaderboard; - leaderboard.SetID(1234U); - leaderboard.SetStartTrigger("1=1"); - leaderboard.SetSubmitTrigger("1=0"); - leaderboard.SetCancelTrigger("0=1"); - leaderboard.SetValueDefinition("M:0xH1234"); - leaderboard.SetValueFormat(ra::data::ValueFormat::Centiseconds); - leaderboard.CreateServerCheckpoint(); - leaderboard.CreateLocalCheckpoint(); - leaderboard.Activate(); + editor.mockRuntime.ActivateLeaderboard(1234U, "STA:1=1::SUB:1=0::CAN:0=1::VAL:M:0xH1234")->format = RC_FORMAT_CENTISECS; + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmLbd = editor.mockGameContext.Assets().FindLeaderboard(1234U); + editor.mockRuntime.SyncAssets(); - editor.LoadAsset(&leaderboard); + editor.LoadAsset(vmLbd); auto* pDefinition = editor.mockRuntime.GetLeaderboardDefinition(1234U); Expects(pDefinition != nullptr); @@ -1831,18 +1791,16 @@ public: TEST_METHOD(TestDecimalPreferredUpdatesConditions) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1_fF2222=f2.3"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1_fF2222=f2.3"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); Expects(pTrigger != nullptr); pTrigger->requirement->conditions->current_hits = 6U; - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); const auto* pCondition1 = editor.Trigger().Conditions().GetItemAt(0); Expects(pCondition1 != nullptr); const auto* pCondition2 = editor.Trigger().Conditions().GetItemAt(1); @@ -1872,18 +1830,16 @@ public: TEST_METHOD(TestCaptureRestoreHits) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); const auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); Expects(pTrigger != nullptr); pTrigger->requirement->conditions->current_hits = 6U; - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1897,7 +1853,7 @@ public: editor.LoadAsset(nullptr); Assert::AreEqual({ 0U }, editor.Trigger().Conditions().Count()); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); pCondition = editor.Trigger().Conditions().GetItemAt(0); Expects(pCondition != nullptr); @@ -1907,20 +1863,17 @@ public: TEST_METHOD(TestCaptureRestoreHitsTriggered) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - Assert::AreEqual(ra::data::models::AssetState::Waiting, achievement.GetState()); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); Expects(pTrigger != nullptr); pTrigger->requirement->conditions->current_hits = 6U; pTrigger->has_hits = 1; - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1929,17 +1882,15 @@ public: pTrigger->requirement->conditions->current_hits = 7U; pTrigger->state = RC_TRIGGER_STATE_TRIGGERED; - achievement.SetState(ra::data::models::AssetState::Triggered); - // triggered achievement is removed from runtime - Assert::IsNull(editor.mockRuntime.GetAchievementTrigger(1234U)); + vmAch->SetState(ra::data::models::AssetState::Triggered); - // but the updated state should still be reflected in the UI + // the updated state should still be reflected in the UI Assert::AreEqual(7U, pCondition->GetCurrentHits()); editor.LoadAsset(nullptr); Assert::AreEqual({ 0U }, editor.Trigger().Conditions().Count()); - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); pCondition = editor.Trigger().Conditions().GetItemAt(0); Expects(pCondition != nullptr); @@ -1950,20 +1901,17 @@ public: TEST_METHOD(TestCaptureRestoreHitsTriggeredChanged) { AssetEditorViewModelHarness editor; - AchievementModel achievement; - achievement.SetID(1234U); - achievement.SetTrigger("0xH1234=1"); - achievement.CreateServerCheckpoint(); - achievement.CreateLocalCheckpoint(); - achievement.Activate(); - Assert::AreEqual(ra::data::models::AssetState::Waiting, achievement.GetState()); + editor.mockRuntime.ActivateAchievement(1234U, "0xH1234=1"); + editor.mockGameContext.InitializeFromAchievementRuntime(); + auto* vmAch = editor.mockGameContext.Assets().FindAchievement(1234U); + editor.mockRuntime.SyncAssets(); auto* pTrigger = editor.mockRuntime.GetAchievementTrigger(1234U); Expects(pTrigger != nullptr); pTrigger->requirement->conditions->current_hits = 6U; pTrigger->has_hits = 1; - editor.LoadAsset(&achievement); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); auto* pCondition = editor.Trigger().Conditions().GetItemAt(0); @@ -1972,9 +1920,7 @@ public: pTrigger->requirement->conditions->current_hits = 7U; pTrigger->state = RC_TRIGGER_STATE_TRIGGERED; - achievement.SetState(ra::data::models::AssetState::Triggered); - // triggered achievement is removed from runtime - Assert::IsNull(editor.mockRuntime.GetAchievementTrigger(1234U)); + vmAch->SetState(ra::data::models::AssetState::Triggered); // but the updated state should still be reflected in the UI Assert::AreEqual(7U, pCondition->GetCurrentHits()); @@ -1982,8 +1928,8 @@ public: editor.LoadAsset(nullptr); Assert::AreEqual({ 0U }, editor.Trigger().Conditions().Count()); - achievement.SetTrigger("0xH1234=2"); - editor.LoadAsset(&achievement); + vmAch->SetTrigger("0xH1234=2"); + editor.LoadAsset(vmAch); Assert::AreEqual({ 1U }, editor.Trigger().Conditions().Count()); pCondition = editor.Trigger().Conditions().GetItemAt(0); Expects(pCondition != nullptr); diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp index c10360a8..97912827 100644 --- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp +++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp @@ -172,6 +172,8 @@ private: AssetListViewModelHarness() noexcept { + mockRuntime.MockGame(); + GSL_SUPPRESS_F6 InitializeNotifyTargets(); } @@ -387,20 +389,21 @@ private: void AddAchievement(AssetCategory nCategory, unsigned nPoints, const std::wstring& sTitle) { - auto vmAchievement = std::make_unique(); + auto vmAchievement = std::make_unique(); vmAchievement->SetID(gsl::narrow_cast(mockGameContext.Assets().Count() + 1)); vmAchievement->SetCategory(nCategory); vmAchievement->SetPoints(nPoints); vmAchievement->SetName(sTitle); vmAchievement->CreateServerCheckpoint(); vmAchievement->CreateLocalCheckpoint(); + vmAchievement->AttachAndInitialize(*vmAchievement->m_pInfo); mockGameContext.Assets().Append(std::move(vmAchievement)); } void AddAchievement(AssetCategory nCategory, unsigned nPoints, const std::wstring& sTitle, const std::wstring& sDescription, const std::wstring& sBadge, const std::string& sTrigger) { - auto vmAchievement = std::make_unique(); + auto vmAchievement = std::make_unique(); vmAchievement->SetID(gsl::narrow_cast(mockGameContext.Assets().Count() + 1)); vmAchievement->SetCategory(nCategory); vmAchievement->SetPoints(nPoints); @@ -410,13 +413,14 @@ private: vmAchievement->SetTrigger(sTrigger); vmAchievement->CreateServerCheckpoint(); vmAchievement->CreateLocalCheckpoint(); + vmAchievement->AttachAndInitialize(*vmAchievement->m_pInfo); mockGameContext.Assets().Append(std::move(vmAchievement)); } void AddNewAchievement(unsigned nPoints, const std::wstring& sTitle, const std::wstring& sDescription, const std::wstring& sBadge, const std::string& sTrigger) { - auto vmAchievement = std::make_unique(); + auto vmAchievement = std::make_unique(); vmAchievement->CreateServerCheckpoint(); vmAchievement->SetID(gsl::narrow_cast(mockGameContext.Assets().Count() + 1)); vmAchievement->SetCategory(AssetCategory::Local); @@ -427,6 +431,7 @@ private: vmAchievement->SetTrigger(sTrigger); vmAchievement->CreateLocalCheckpoint(); vmAchievement->SetNew(); + vmAchievement->AttachAndInitialize(*vmAchievement->m_pInfo); mockGameContext.Assets().Append(std::move(vmAchievement)); } @@ -561,8 +566,18 @@ private: return dynamic_cast(mockGameContext.Assets().FindAsset(ra::data::models::AssetType::LocalBadges, 0)); } + class MockAchievementModel : public ra::data::models::AchievementModel + { + public: + MockAchievementModel() + { + m_pInfo = std::make_unique(); + memset(m_pInfo.get(), 0, sizeof(rc_client_achievement_info_t)); + } + + std::unique_ptr m_pInfo; + }; - private: std::map m_mValidationErrors; std::map m_mPublishServerErrors; }; @@ -1250,6 +1265,7 @@ public: vmAssetList.SetSpecialFilter(AssetListViewModel::SpecialFilter::Active); vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Ach1"); + Assert::AreEqual({ 1U }, vmAssetList.mockGameContext.Assets().Count()); auto* pAsset = dynamic_cast(vmAssetList.mockGameContext.Assets().GetItemAt(0)); Expects(pAsset != nullptr); @@ -3101,12 +3117,13 @@ public: vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1"); vmAssetList.AddAchievement(AssetCategory::Core, 7, L"Test2", L"Desc2", L"11111", "0xH1111=1"); - auto* pAchievement = vmAssetList.mockGameContext.Assets().FindAchievement(1U); + auto* pAchievement = dynamic_cast( + vmAssetList.mockGameContext.Assets().FindAchievement(1U)); Expects(pAchievement != nullptr); pAchievement->Activate(); Assert::AreEqual(AssetState::Waiting, pAchievement->GetState()); - auto* pTrigger = vmAssetList.mockRuntime.GetAchievementTrigger(1U); + auto* pTrigger = pAchievement->m_pInfo->trigger; Expects(pTrigger != nullptr); pTrigger->state = RC_TRIGGER_STATE_ACTIVE; Assert::AreEqual(AssetState::Waiting, pAchievement->GetState()); diff --git a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp index e8ee62ef..3593c792 100644 --- a/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp +++ b/tests/ui/viewmodels/IntegrationMenuViewModel_Tests.cpp @@ -24,7 +24,6 @@ #include "tests\mocks\MockEmulatorContext.hh" #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockLocalStorage.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockThreadPool.hh" #include "tests\mocks\MockUserContext.hh" @@ -51,14 +50,13 @@ private: ra::services::mocks::MockAchievementRuntime mockAchievementRuntime; ra::services::mocks::MockConfiguration mockConfiguration; ra::services::mocks::MockLocalStorage mockLocalStorage; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; ra::services::mocks::MockThreadPool mockThreadPool; ra::ui::mocks::MockDesktop mockDesktop; ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager; IntegrationMenuViewModelHarness() { - mockRcheevosClient.MockGame(); + mockAchievementRuntime.MockGame(); } void BuildMenu() diff --git a/tests/ui/viewmodels/LoginViewModel_Tests.cpp b/tests/ui/viewmodels/LoginViewModel_Tests.cpp index 4e03f996..ff28b7ad 100644 --- a/tests/ui/viewmodels/LoginViewModel_Tests.cpp +++ b/tests/ui/viewmodels/LoginViewModel_Tests.cpp @@ -3,10 +3,10 @@ #include "ui\viewmodels\LoginViewModel.hh" #include "tests\ui\UIAsserts.hh" +#include "tests\mocks\MockAchievementRuntime.hh" #include "tests\mocks\MockConfiguration.hh" #include "tests\mocks\MockDesktop.hh" #include "tests\mocks\MockEmulatorContext.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockSessionTracker.hh" #include "tests\mocks\MockUserContext.hh" @@ -18,7 +18,7 @@ using ra::data::context::mocks::MockEmulatorContext; using ra::data::context::mocks::MockSessionTracker; using ra::data::context::mocks::MockUserContext; using ra::services::mocks::MockConfiguration; -using ra::services::mocks::MockRcheevosClient; +using ra::services::mocks::MockAchievementRuntime; using ra::ui::mocks::MockDesktop; @@ -36,7 +36,7 @@ private: GSL_SUPPRESS_F6 LoginViewModelHarness() : LoginViewModel(L"User") {} MockConfiguration mockConfiguration; - MockRcheevosClient mockRcheevosClient; + MockAchievementRuntime mockAchievementRuntime; MockDesktop mockDesktop; MockServer mockServer; MockUserContext mockUserContext; @@ -70,7 +70,7 @@ public: vmLogin.SetPassword(L"Pa$$w0rd"); Assert::IsFalse(vmLogin.Login()); Assert::IsTrue(vmLogin.mockDesktop.WasDialogShown()); - vmLogin.mockRcheevosClient.AssertNoPendingRequests(); + vmLogin.mockAchievementRuntime.AssertNoPendingRequests(); } TEST_METHOD(TestLoginNoPassword) @@ -87,13 +87,13 @@ public: vmLogin.SetPassword(L""); Assert::IsFalse(vmLogin.Login()); Assert::IsTrue(vmLogin.mockDesktop.WasDialogShown()); - vmLogin.mockRcheevosClient.AssertNoPendingRequests(); + vmLogin.mockAchievementRuntime.AssertNoPendingRequests(); } TEST_METHOD(TestLoginSuccessful) { LoginViewModelHarness vmLogin; - vmLogin.mockRcheevosClient.MockResponse("r=login&u=user&p=Pa%24%24w0rd", + vmLogin.mockAchievementRuntime.MockResponse("r=login&u=user&p=Pa%24%24w0rd", "{\"Success\":true,\"User\":\"User\",\"DisplayName\":\"UserDisplay\"," "\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123," "\"Messages\":0,\"Permissions\":1,\"AccountType\":\"Registered\"}"); @@ -136,7 +136,7 @@ public: TEST_METHOD(TestLoginSuccessfulAppTitle) { LoginViewModelHarness vmLogin; - vmLogin.mockRcheevosClient.MockResponse("r=login&u=user&p=Pa%24%24w0rd", + vmLogin.mockAchievementRuntime.MockResponse("r=login&u=user&p=Pa%24%24w0rd", "{\"Success\":true,\"User\":\"User\",\"DisplayName\":\"UserDisplay\"," "\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123," "\"Messages\":0,\"Permissions\":1,\"AccountType\":\"Registered\"}"); @@ -181,7 +181,7 @@ public: TEST_METHOD(TestLoginInvalidPassword) { LoginViewModelHarness vmLogin; - vmLogin.mockRcheevosClient.MockResponse("r=login&u=User&p=Pa%24%24w0rd", + vmLogin.mockAchievementRuntime.MockResponse("r=login&u=User&p=Pa%24%24w0rd", "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}"); vmLogin.mockDesktop.ExpectWindow([](MessageBoxViewModel& vmMessageBox) @@ -201,7 +201,7 @@ public: TEST_METHOD(TestLoginSuccessfulRememberPassword) { LoginViewModelHarness vmLogin; - vmLogin.mockRcheevosClient.MockResponse("r=login&u=user&p=Pa%24%24w0rd", + vmLogin.mockAchievementRuntime.MockResponse("r=login&u=user&p=Pa%24%24w0rd", "{\"Success\":true,\"User\":\"User\",\"DisplayName\":\"UserDisplay\"," "\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123," "\"Messages\":0,\"Permissions\":1,\"AccountType\":\"Registered\"}"); diff --git a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp index c4adf14a..c2f6bae3 100644 --- a/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp +++ b/tests/ui/viewmodels/OverlayAchievementsPageViewModel_Tests.cpp @@ -7,7 +7,6 @@ #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockImageRepository.hh" #include "tests\mocks\MockOverlayManager.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockServer.hh" #include "tests\mocks\MockSessionTracker.hh" #include "tests\mocks\MockThreadPool.hh" @@ -37,7 +36,6 @@ private: ra::data::context::mocks::MockUserContext mockUserContext; ra::services::mocks::MockAchievementRuntime mockAchievementRuntime; ra::services::mocks::MockClock mockClock; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; ra::services::mocks::MockThreadPool mockThreadPool; ra::ui::mocks::MockImageRepository mockImageRepository; ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager; @@ -160,9 +158,9 @@ public: TEST_METHOD(TestRefreshUnlockedAchievement) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1, "AchievementTitle"); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1, "AchievementTitle"); pAch1->public_.description = "Trigger this"; pAch1->public_.points = 5; snprintf(pAch1->public_.badge_name, sizeof(pAch1->public_.badge_name), "BADGE"); @@ -183,9 +181,9 @@ public: TEST_METHOD(TestRefreshActiveAchievement) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1, "AchievementTitle"); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1, "AchievementTitle"); pAch1->public_.description = "Trigger this"; pAch1->public_.points = 5; snprintf(pAch1->public_.badge_name, sizeof(pAch1->public_.badge_name), "BADGE"); @@ -204,9 +202,9 @@ public: TEST_METHOD(TestRefreshOnePointAchievement) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1, "AchievementTitle"); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1, "AchievementTitle"); pAch1->public_.description = "Trigger this"; pAch1->public_.points = 1; // should say "1 point" instead of "1 points" snprintf(pAch1->public_.badge_name, sizeof(pAch1->public_.badge_name), "BADGE"); @@ -231,21 +229,21 @@ public: TEST_METHOD(TestRefreshActiveAndInactiveAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; // Waiting appears as active at this level - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch4->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -268,22 +266,22 @@ public: TEST_METHOD(TestRefreshActiveAndDisabledAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; // Waiting appears as active at this level - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; pAch2->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; // will appear as locked, even though it was unlocked pAch4->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; @@ -339,23 +337,23 @@ public: TEST_METHOD(TestRefreshInactiveAndDisabledAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; pAch2->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch3->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; // will appear as locked, even though it was unlocked pAch4->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; @@ -380,23 +378,23 @@ public: TEST_METHOD(TestRefreshInactiveAndDisabledAchievementsInitialCollapseState) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; pAch2->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch3->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; pAch4->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; @@ -420,18 +418,18 @@ public: TEST_METHOD(TestRefreshActiveAndPrimedAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch2->trigger->state = RC_TRIGGER_STATE_PRIMED; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; @@ -452,21 +450,21 @@ public: TEST_METHOD(TestRefreshCategoriesCoreOnly) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockUnofficialAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockUnofficialAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockLocalAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockLocalAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch4->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -490,17 +488,17 @@ public: TEST_METHOD(TestRefreshCategoriesWithLocal) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockUnofficialAchievement(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockUnofficialAchievement(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockLocalAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockLocalAchievement(3); pAch3->public_.points = 3; snprintf(pAch3->public_.badge_name, sizeof(pAch3->public_.badge_name), "L000003"); pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; @@ -509,7 +507,7 @@ public: vmAch3.SetID(3); vmAch3.SetBadge(L"local\\1-abcdef0123456789.png"); - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch4->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -547,23 +545,23 @@ public: TEST_METHOD(TestRefreshProgressAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch2->trigger->measured_target = 10; pAch2->trigger->measured_value = 5; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievement(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievement(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch4->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -594,28 +592,28 @@ public: TEST_METHOD(TestRefreshAlmostThereAchievements) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch1->trigger->measured_target = 10; pAch1->trigger->measured_value = 1; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch2->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; pAch2->trigger->measured_target = 10; pAch2->trigger->measured_value = 9; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch3->trigger->measured_target = 10; pAch3->trigger->measured_value = 9; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievement(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievement(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; @@ -644,27 +642,27 @@ public: TEST_METHOD(TestRefreshAlmostThereAchievementsSorting) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(1); pAch1->public_.points = 1; pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch1->trigger->measured_target = 100; pAch1->trigger->measured_value = 90; - auto* pAch2 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(2); + auto* pAch2 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(2); pAch2->public_.points = 2; pAch2->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch2->trigger->measured_target = 100; pAch2->trigger->measured_value = 85; - auto* pAch3 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(3); + auto* pAch3 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(3); pAch3->public_.points = 3; pAch3->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch3->trigger->measured_target = 100; pAch3->trigger->measured_value = 95; - auto* pAch4 = achievementsPage.mockRcheevosClient.MockAchievementWithTrigger(4); + auto* pAch4 = achievementsPage.mockAchievementRuntime.MockAchievementWithTrigger(4); pAch4->public_.points = 4; pAch4->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; pAch4->trigger->measured_target = 100; @@ -688,10 +686,10 @@ public: { OverlayAchievementsPageViewModelHarness achievementsPage; achievementsPage.mockGameContext.SetGameId(1U); - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); achievementsPage.mockSessionTracker.MockSession(1U, 1234567879, std::chrono::minutes(347)); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch1->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -705,10 +703,10 @@ public: { OverlayAchievementsPageViewModelHarness achievementsPage; achievementsPage.mockGameContext.SetGameId(1U); - achievementsPage.mockRcheevosClient.MockGame(); + achievementsPage.mockAchievementRuntime.MockGame(); achievementsPage.mockSessionTracker.MockSession(1U, 1234567879, std::chrono::seconds(17 * 60 + 12)); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1); pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch1->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; @@ -733,8 +731,8 @@ public: { OverlayAchievementsPageViewModelHarness achievementsPage; achievementsPage.mockWindowManager.AssetList.SetFilterCategory(AssetListViewModel::FilterCategory::Local); - achievementsPage.mockRcheevosClient.MockGame(); - const auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(ra::data::context::GameAssets::FirstLocalId); + achievementsPage.mockAchievementRuntime.MockGame(); + const auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(ra::data::context::GameAssets::FirstLocalId); achievementsPage.mockServer.ExpectUncalled(); @@ -752,8 +750,8 @@ public: TEST_METHOD(TestFetchItemDetail) { OverlayAchievementsPageViewModelHarness achievementsPage; - achievementsPage.mockRcheevosClient.MockGame(); - auto* pAch1 = achievementsPage.mockRcheevosClient.MockAchievement(1234); + achievementsPage.mockAchievementRuntime.MockGame(); + auto* pAch1 = achievementsPage.mockAchievementRuntime.MockAchievement(1234); pAch1->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; pAch1->public_.unlocked = RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH; diff --git a/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp b/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp index a68dc50f..25f3b184 100644 --- a/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp +++ b/tests/ui/viewmodels/OverlayLeaderboardsPageViewModel_Tests.cpp @@ -2,11 +2,11 @@ #include "ui\viewmodels\OverlayLeaderboardsPageViewModel.hh" +#include "tests\mocks\MockAchievementRuntime.hh" #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockImageRepository.hh" #include "tests\mocks\MockOverlayManager.hh" #include "tests\mocks\MockServer.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockThreadPool.hh" #include "tests\mocks\MockUserContext.hh" #include "tests\mocks\MockWindowManager.hh" @@ -28,7 +28,7 @@ private: ra::api::mocks::MockServer mockServer; ra::data::context::mocks::MockGameContext mockGameContext; ra::data::context::mocks::MockUserContext mockUserContext; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; + ra::services::mocks::MockAchievementRuntime mockAchievementRuntime; ra::services::mocks::MockThreadPool mockThreadPool; ra::ui::mocks::MockImageRepository mockImageRepository; ra::ui::viewmodels::mocks::MockOverlayManager mockOverlayManager; @@ -102,10 +102,10 @@ public: TEST_METHOD(TestRefreshLeaderboards) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(2); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(2); leaderboardsPage.SetCanCollapseHeaders(false); leaderboardsPage.Refresh(); @@ -121,12 +121,12 @@ public: TEST_METHOD(TestRefreshCoreOnly) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); // only 1 and 3 will be visible - 2 is not core, and filtered out by asset list - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - leaderboardsPage.mockRcheevosClient.MockLocalLeaderboard(2); - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + leaderboardsPage.mockAchievementRuntime.MockLocalLeaderboard(2); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetCanCollapseHeaders(false); leaderboardsPage.Refresh(); @@ -143,11 +143,11 @@ public: TEST_METHOD(TestRefreshCoreAndLocal) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLocalLeaderboard(2); - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLocalLeaderboard(2); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetAssetFilter(ra::ui::viewmodels::AssetListViewModel::FilterCategory::Local); leaderboardsPage.SetCanCollapseHeaders(false); @@ -167,13 +167,13 @@ public: TEST_METHOD(TestRefreshWithHidden) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); // only 1 and 3 will be visible - 2 is hidden - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLocalLeaderboard(2); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLocalLeaderboard(2); pLbd2->hidden = 1; - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetCanCollapseHeaders(false); leaderboardsPage.Refresh(); @@ -190,13 +190,13 @@ public: TEST_METHOD(TestRefreshWithActive) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(2); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(2); pLbd2->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; pLbd2->public_.tracker_value = "1:23.00"; - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetCanCollapseHeaders(false); leaderboardsPage.Refresh(); @@ -215,15 +215,15 @@ public: TEST_METHOD(TestRefreshWithHiddenActive) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); // only 1 and 3 will be visible - 2 is hidden (TODO: should it be?) - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(2); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(2); pLbd2->hidden = 1; pLbd2->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; pLbd2->public_.tracker_value = "1:23.00"; - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetCanCollapseHeaders(false); leaderboardsPage.Refresh(); @@ -240,12 +240,12 @@ public: TEST_METHOD(TestRefreshWithDisabled) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockGame(); - auto* pLbd1 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); - auto* pLbd2 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(2); + auto* pLbd1 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); + auto* pLbd2 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(2); pLbd2->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; - auto* pLbd3 = leaderboardsPage.mockRcheevosClient.MockLeaderboard(3); + auto* pLbd3 = leaderboardsPage.mockAchievementRuntime.MockLeaderboard(3); leaderboardsPage.SetAssetFilter(ra::ui::viewmodels::AssetListViewModel::FilterCategory::Local); leaderboardsPage.SetCanCollapseHeaders(false); @@ -265,8 +265,8 @@ public: TEST_METHOD(TestFetchItemDetail) { OverlayLeaderboardsPageViewModelHarness leaderboardsPage; - leaderboardsPage.mockRcheevosClient.MockGame(); - leaderboardsPage.mockRcheevosClient.MockLeaderboard(1); + leaderboardsPage.mockAchievementRuntime.MockGame(); + leaderboardsPage.mockAchievementRuntime.MockLeaderboard(1); leaderboardsPage.mockUserContext.Initialize("user2", "User2", "ApiToken"); leaderboardsPage.mockServer.HandleRequest([](const ra::api::FetchLeaderboardInfo::Request& request, ra::api::FetchLeaderboardInfo::Response& response) diff --git a/tests/ui/viewmodels/OverlayManager_Tests.cpp b/tests/ui/viewmodels/OverlayManager_Tests.cpp index 846cc62f..b6852b22 100644 --- a/tests/ui/viewmodels/OverlayManager_Tests.cpp +++ b/tests/ui/viewmodels/OverlayManager_Tests.cpp @@ -12,7 +12,6 @@ #include "tests\mocks\MockGameContext.hh" #include "tests\mocks\MockImageRepository.hh" #include "tests\mocks\MockOverlayTheme.hh" -#include "tests\mocks\MockRcheevosClient.hh" #include "tests\mocks\MockSurface.hh" #include "tests\mocks\MockThreadPool.hh" #include "tests\mocks\MockUserContext.hh" @@ -39,7 +38,6 @@ private: ra::services::mocks::MockAchievementRuntime mockAchievementRuntime; ra::services::mocks::MockClock mockClock; ra::services::mocks::MockConfiguration mockConfiguration; - ra::services::mocks::MockRcheevosClient mockRcheevosClient; ra::services::mocks::MockThreadPool mockThreadPool; ra::ui::mocks::MockDesktop mockDesktop; ra::ui::mocks::MockImageRepository mockImageRepository; @@ -609,7 +607,7 @@ public: OverlayManagerHarness overlay; overlay.mockConfiguration.SetPopupLocation(ra::ui::viewmodels::Popup::Progress, ra::ui::viewmodels::PopupLocation::BottomRight); - overlay.UpdateProgressTracker(ra::ui::ImageType::Badge, "12345_lock", 3, 7, false); + overlay.UpdateProgressTracker(ra::ui::ImageType::Badge, "12345_lock", L"3/7"); auto* pTracker = overlay.GetProgressTracker(); Assert::IsNotNull(pTracker); Ensures(pTracker != nullptr); @@ -625,7 +623,7 @@ public: overlay.Render(mockSurface, false); Assert::AreEqual(10, pTracker->GetVerticalOffset()); - overlay.mockClock.AdvanceTime(std::chrono::seconds(3)); + overlay.UpdateProgressTracker(ra::ui::ImageType::None, "", L""); overlay.Render(mockSurface, false); Assert::IsTrue(overlay.WasRenderRequested()); // destroy pending Assert::IsTrue(pTracker->IsDestroyPending()); diff --git a/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp b/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp index a77a5730..9ce74f97 100644 --- a/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp +++ b/tests/ui/viewmodels/RichPresenceMonitorViewModel_Tests.cpp @@ -28,6 +28,8 @@ TEST_CLASS(RichPresenceMonitorViewModel_Tests) public: RichPresenceMonitorViewModelHarness() noexcept { + mockRuntime.MockGame(); + GSL_SUPPRESS_F6 InitializeNotifyTargets(); }