Repository: RetroAchievements/RAIntegration
Pull request #652 (2020-12-22 00:54:20)

allow client to suspend forced repaints
Gives more control to the client over how frequently the tool windows are allowed to forcibly repaint. Primarily allows for disabling repaints while skipping rendered frames in fast forward mode.


Patch:
diff --git a/rcheevos b/rcheevos
index 203fc282..4f139be4 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit 203fc282d542a058241f3c5a28a2d0ca15e628cb
+Subproject commit 4f139be47e3efd030cf08214a89e32e54e145410
diff --git a/src/Exports.cpp b/src/Exports.cpp
index 24c6ccb0..ecb81adf 100644
--- a/src/Exports.cpp
+++ b/src/Exports.cpp
@@ -31,6 +31,7 @@
 #include "ui\viewmodels\WindowManager.hh"
 #include "ui\win32\Desktop.hh"
 #include "ui\win32\OverlayWindow.hh"
+#include "ui\win32\bindings\ControlBinding.hh"
 
 #include "RAInterface\RA_Emulators.h"
 
@@ -502,6 +503,10 @@ static void UpdateUIForFrameChange()
 
 API void CCONV _RA_DoAchievementsFrame()
 {
+#ifndef RA_UTEST
+    ra::ui::win32::bindings::ControlBinding::RepaintGuard guard;
+#endif
+
     // make sure we process the achievements _before_ the frozen bookmarks modify the memory
     ProcessAchievements();
 
@@ -512,6 +517,20 @@ API void CCONV _RA_DoAchievementsFrame()
     CHECK_PERFORMANCE();
 }
 
+API void CCONV _RA_SuspendRepaint()
+{
+#ifndef RA_UTEST
+    ra::ui::win32::bindings::ControlBinding::SuspendRepaint();
+#endif
+}
+
+API void CCONV _RA_ResumeRepaint()
+{
+#ifndef RA_UTEST
+    ra::ui::win32::bindings::ControlBinding::ResumeRepaint();
+#endif
+}
+
 API void CCONV _RA_OnSaveState(const char* sFilename)
 {
     ra::services::ServiceLocator::Get<ra::services::AchievementRuntime>().SaveProgressToFile(sFilename);
diff --git a/src/Exports.hh b/src/Exports.hh
index 64bc6efb..e6849044 100644
--- a/src/Exports.hh
+++ b/src/Exports.hh
@@ -88,6 +88,12 @@ extern "C" {
     // Perform one test for all achievements in the current set. Call this once per frame/cycle.
     API void CCONV _RA_DoAchievementsFrame();
 
+    // Temporarily disable forced redraws
+    API void CCONV _RA_SuspendRepaint();
+
+    // Resume forced redraws
+    API void CCONV _RA_ResumeRepaint();
+
     // Use in special cases where the emulator contains more than one console ID.
     API void CCONV _RA_SetConsoleID(unsigned int nConsoleID);
 
diff --git a/src/RA_Achievement.cpp b/src/RA_Achievement.cpp
index e4f4dc82..96b84934 100644
--- a/src/RA_Achievement.cpp
+++ b/src/RA_Achievement.cpp
@@ -75,14 +75,14 @@ static void MergeHits(rc_condset_t& pGroup, rc_condset_t& pOldGroup) noexcept
                 case RC_OPERAND_ADDRESS:
                 case RC_OPERAND_DELTA:
                 case RC_OPERAND_PRIOR:
-                    if (pOldCondition->operand1.value.memref->memref.address != pCondition->operand1.value.memref->memref.address ||
+                    if (pOldCondition->operand1.value.memref->address != pCondition->operand1.value.memref->address ||
                         pOldCondition->operand1.size != pCondition->operand1.size)
                     {
                         continue;
                     }
 
-                    pCondition->operand1.value.memref->previous = pOldCondition->operand1.value.memref->previous;
-                    pCondition->operand1.value.memref->prior = pOldCondition->operand1.value.memref->prior;
+                    pCondition->operand1.value.memref->value.changed = pOldCondition->operand1.value.memref->value.changed;
+                    pCondition->operand1.value.memref->value.prior = pOldCondition->operand1.value.memref->value.prior;
                     break;
 
                 default:
@@ -261,15 +261,15 @@ inline static constexpr void SetOperand(CompVariable& var, const rc_operand_t& o
     switch (operand.type)
     {
         case RC_OPERAND_ADDRESS:
-            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::Address, operand.value.memref->memref.address);
+            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::Address, operand.value.memref->address);
             break;
 
         case RC_OPERAND_DELTA:
-            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::DeltaMem, operand.value.memref->memref.address);
+            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::DeltaMem, operand.value.memref->address);
             break;
 
         case RC_OPERAND_PRIOR:
-            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::PriorMem, operand.value.memref->memref.address);
+            var.Set(GetCompVariableSize(operand.size), CompVariable::Type::PriorMem, operand.value.memref->address);
             break;
 
         case RC_OPERAND_CONST:
diff --git a/src/data/DataModelCollection.cpp b/src/data/DataModelCollection.cpp
index 7412e863..bac48dd4 100644
--- a/src/data/DataModelCollection.cpp
+++ b/src/data/DataModelCollection.cpp
@@ -68,42 +68,51 @@ void DataModelCollectionBase::OnEndUpdate()
 
 void DataModelCollectionBase::OnItemsRemoved(const std::vector<gsl::index>& vDeletedIndices)
 {
-    // create a copy of the list of pointers in case it's modified by one of the callbacks
-    NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
-    for (auto nDeletedIndex : vDeletedIndices)
+    if (!m_vNotifyTargets.empty())
     {
-        for (NotifyTarget* target : vNotifyTargets)
+        // create a copy of the list of pointers in case it's modified by one of the callbacks
+        NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
+        for (auto nDeletedIndex : vDeletedIndices)
         {
-            Expects(target != nullptr);
-            target->OnDataModelRemoved(nDeletedIndex);
+            for (NotifyTarget* target : vNotifyTargets)
+            {
+                Expects(target != nullptr);
+                target->OnDataModelRemoved(nDeletedIndex);
+            }
         }
     }
 }
 
 void DataModelCollectionBase::OnItemsAdded(const std::vector<gsl::index>& vNewIndices)
 {
-    // create a copy of the list of pointers in case it's modified by one of the callbacks
-    NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
-    for (auto vNewIndex : vNewIndices)
+    if (!m_vNotifyTargets.empty())
     {
-        for (NotifyTarget* target : vNotifyTargets)
+        // create a copy of the list of pointers in case it's modified by one of the callbacks
+        NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
+        for (auto vNewIndex : vNewIndices)
         {
-            Expects(target != nullptr);
-            target->OnDataModelAdded(vNewIndex);
+            for (NotifyTarget* target : vNotifyTargets)
+            {
+                Expects(target != nullptr);
+                target->OnDataModelAdded(vNewIndex);
+            }
         }
     }
 }
 
 void DataModelCollectionBase::OnItemsChanged(const std::vector<gsl::index>& vChangedIndices)
 {
-    // create a copy of the list of pointers in case it's modified by one of the callbacks
-    NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
-    for (auto vChangedIndex : vChangedIndices)
+    if (!m_vNotifyTargets.empty())
     {
-        for (NotifyTarget* target : vNotifyTargets)
+        // create a copy of the list of pointers in case it's modified by one of the callbacks
+        NotifyTargetSet vNotifyTargets(m_vNotifyTargets);
+        for (auto vChangedIndex : vChangedIndices)
         {
-            Expects(target != nullptr);
-            target->OnDataModelChanged(vChangedIndex);
+            for (NotifyTarget* target : vNotifyTargets)
+            {
+                Expects(target != nullptr);
+                target->OnDataModelChanged(vChangedIndex);
+            }
         }
     }
 }
diff --git a/src/data/ModelCollectionBase.cpp b/src/data/ModelCollectionBase.cpp
index c440f9f4..857401af 100644
--- a/src/data/ModelCollectionBase.cpp
+++ b/src/data/ModelCollectionBase.cpp
@@ -272,7 +272,7 @@ void ModelCollectionBase::UpdateIndices()
             }
         }
 
-        if (bWatching && !vDeletedIndices.empty())
+        if (!vDeletedIndices.empty())
             OnItemsRemoved(vDeletedIndices);
     }
 
@@ -295,10 +295,9 @@ void ModelCollectionBase::UpdateIndices()
             pItem->m_nCollectionIndex = nIndex;
 
             if (bWatching)
-            {
                 StartWatching(*pItem, nIndex);
-                vNewIndices.push_back(nIndex);
-            }
+
+            vNewIndices.push_back(nIndex);
         }
     }
 
@@ -313,9 +312,7 @@ void ModelCollectionBase::UpdateIndices()
         if (pItem->m_nCollectionIndex != nIndex)
         {
             pItem->m_nCollectionIndex = nIndex;
-
-            if (bWatching)
-                vChangedIndices.push_back(nIndex);
+            vChangedIndices.push_back(nIndex);
         }
 
         ++nIndex;
diff --git a/src/data/context/GameAssets.cpp b/src/data/context/GameAssets.cpp
index 5f0bc2e7..6e0e1ace 100644
--- a/src/data/context/GameAssets.cpp
+++ b/src/data/context/GameAssets.cpp
@@ -49,6 +49,40 @@ ra::data::models::AchievementModel& GameAssets::NewAchievement()
     return dynamic_cast<ra::data::models::AchievementModel&>(AddItem(std::move(vmAchievement)));
 }
 
+void GameAssets::OnItemsAdded(const std::vector<gsl::index>& vNewIndices)
+{
+    auto* pLocalBadges = dynamic_cast<ra::data::models::LocalBadgesModel*>(FindAsset(ra::data::models::AssetType::LocalBadges, 0));
+    if (pLocalBadges)
+    {
+        for (const auto nIndex : vNewIndices)
+        {
+            const auto* pAsset = GetItemAt(nIndex);
+            const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(pAsset);
+            if (pAchievement && ra::StringStartsWith(pAchievement->GetBadge(), L"local\\"))
+                pLocalBadges->AddReference(pAchievement->GetBadge(), pAchievement->IsBadgeCommitted());
+        }
+    }
+
+    ra::data::DataModelCollection<ra::data::models::AssetModelBase>::OnItemsAdded(vNewIndices);
+}
+
+void GameAssets::OnItemsRemoved(const std::vector<gsl::index>& vDeletedIndices)
+{
+    auto* pLocalBadges = dynamic_cast<ra::data::models::LocalBadgesModel*>(FindAsset(ra::data::models::AssetType::LocalBadges, 0));
+    if (pLocalBadges)
+    {
+        for (const auto nIndex : vDeletedIndices)
+        {
+            const auto* pAsset = GetItemAt(nIndex);
+            const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(pAsset);
+            if (pAchievement && ra::StringStartsWith(pAchievement->GetBadge(), L"local\\"))
+                pLocalBadges->RemoveReference(pAchievement->GetBadge(), pAchievement->IsBadgeCommitted());
+        }
+    }
+
+    ra::data::DataModelCollection<ra::data::models::AssetModelBase>::OnItemsRemoved(vDeletedIndices);
+}
+
 void GameAssets::ReloadAssets(const std::vector<ra::data::models::AssetModelBase*>& vAssetsToReload)
 {
     const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
@@ -99,12 +133,6 @@ void GameAssets::ReloadAssets(const std::vector<ra::data::models::AssetModelBase
                 nType = ra::data::models::AssetType::Achievement;
                 nId = pTokenizer.ReadNumber();
                 break;
-
-            case 'b': case 'B':
-                nType = ra::data::models::AssetType::LocalBadges;
-                nId = 0;
-                pTokenizer.Advance();
-                break;
         }
 
         if (nType == ra::data::models::AssetType::None)
@@ -156,14 +184,6 @@ void GameAssets::ReloadAssets(const std::vector<ra::data::models::AssetModelBase
                     pAsset = &Append(std::move(vmAchievement));
                     break;
                 }
-
-                case ra::data::models::AssetType::LocalBadges:
-                {
-                    auto* pLocalBadges = FindAsset(ra::data::models::AssetType::LocalBadges, 0);
-                    if (pLocalBadges)
-                        pLocalBadges->Deserialize(pTokenizer);
-                    break;
-                }
             }
 
             if (pAsset)
@@ -287,13 +307,6 @@ void GameAssets::SaveAssets(const std::vector<ra::data::models::AssetModelBase*>
         pData->WriteLine();
     }
 
-    const auto* pLocalBadges = dynamic_cast<ra::data::models::LocalBadgesModel*>(FindAsset(ra::data::models::AssetType::LocalBadges, 0));
-    if (pLocalBadges != nullptr && pLocalBadges->NeedsSerialized())
-    {
-        pLocalBadges->Serialize(*pData);
-        pData->WriteLine();
-    }
-
     RA_LOG_INFO("Wrote user assets file");
 }
 
diff --git a/src/data/context/GameAssets.hh b/src/data/context/GameAssets.hh
index 9eac0977..4f436497 100644
--- a/src/data/context/GameAssets.hh
+++ b/src/data/context/GameAssets.hh
@@ -68,6 +68,9 @@ public:
     void ResetLocalId() noexcept { m_nNextLocalId = FirstLocalId; }
 
 protected:
+    void OnItemsRemoved(const std::vector<gsl::index>& vDeletedIndices) override;
+    void OnItemsAdded(const std::vector<gsl::index>& vNewIndices) override;
+
     uint32_t m_nNextLocalId = FirstLocalId;
 };
 
diff --git a/src/data/models/AchievementModel.hh b/src/data/models/AchievementModel.hh
index 10f5275a..08c9022a 100644
--- a/src/data/models/AchievementModel.hh
+++ b/src/data/models/AchievementModel.hh
@@ -43,6 +43,14 @@ public:
     /// </summary>
     void SetBadge(const std::wstring& sValue) { SetValue(BadgeProperty, sValue); }
 
+    /// <summary>
+    /// Determines if the badge has been committed.
+    /// </summary>
+    bool IsBadgeCommitted() const
+    {
+        return (GetLocalValue(BadgeProperty) == GetValue(BadgeProperty));
+    }
+
     /// <summary>
     /// The <see cref="ModelProperty" /> for whether or not the emulator should be paused when the trigger is reset.
     /// </summary>
diff --git a/src/data/models/LocalBadgesModel.cpp b/src/data/models/LocalBadgesModel.cpp
index d69add80..f20a1e1c 100644
--- a/src/data/models/LocalBadgesModel.cpp
+++ b/src/data/models/LocalBadgesModel.cpp
@@ -55,13 +55,9 @@ void LocalBadgesModel::AddReference(const std::wstring& sBadgeName, bool bCommit
     }
     else
     {
-        // a committed entry should exist in m_mReferences from the Deserialize call
-        // or from a previously uncommitted entry
-        Expects(!bCommitted); 
-
-        BadgeReferenceCount pCount;
-        pCount.nCommittedCount = 0;
-        pCount.nUncommittedCount = 1;
+        BadgeReferenceCount pCount{};
+        pCount.nCommittedCount = bCommitted ? 1 : 0;
+        pCount.nUncommittedCount = bCommitted ? 0 : 1;
 
         m_mReferences.insert_or_assign(sBadgeName, pCount);
     }
@@ -85,11 +81,11 @@ void LocalBadgesModel::RemoveReference(const std::wstring& sBadgeName, bool bCom
     }
 }
 
-int LocalBadgesModel::GetReferenceCount(const std::wstring& sBadgeName) const
+int LocalBadgesModel::GetReferenceCount(const std::wstring& sBadgeName, bool bCommitted) const
 {
     const auto pIter = m_mReferences.find(sBadgeName);
     if (pIter != m_mReferences.end())
-        return pIter->second.nCommittedCount + pIter->second.nUncommittedCount;
+        return bCommitted ? pIter->second.nCommittedCount : pIter->second.nUncommittedCount;
 
     return 0;
 }
@@ -121,60 +117,6 @@ void LocalBadgesModel::Commit(const std::wstring& sPreviousBadgeName, const std:
     }
 }
 
-bool LocalBadgesModel::NeedsSerialized() const noexcept
-{
-    // ASSERT: NeedsSerialized and Serialize are called after each asset has had a chance to call Commit
-    for (const auto& pIter : m_mReferences)
-    {
-        if (pIter.second.nCommittedCount != 0)
-            return true;
-    }
-
-    return false;
-}
-
-void LocalBadgesModel::Serialize(ra::services::TextWriter& pWriter) const
-{
-    pWriter.Write("b");
-    for (const auto& pIter : m_mReferences)
-    {
-        if (pIter.second.nCommittedCount > 0)
-        {
-            const auto nIndex = pIter.first.find('-');
-            pWriter.Write(":");
-            pWriter.Write(pIter.first.substr(nIndex + 1));
-            pWriter.Write("=");
-            pWriter.Write(std::to_string(pIter.second.nCommittedCount));
-        }
-    }
-}
-
-bool LocalBadgesModel::Deserialize(ra::Tokenizer& pTokenizer)
-{
-    const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
-    const auto sPrefix = ra::StringPrintf(L"local\\%u-", pGameContext.GameId());
-
-    m_mReferences.clear();
-
-    do
-    {
-        const auto sFilename = pTokenizer.ReadTo('=');
-        pTokenizer.Advance();
-
-        const auto nCount = pTokenizer.ReadNumber();
-        if (nCount > 0)
-        {
-            BadgeReferenceCount pCount;
-            pCount.nCommittedCount = nCount;
-            pCount.nUncommittedCount = 0;
-
-            m_mReferences.insert_or_assign(sPrefix + ra::Widen(sFilename), pCount);
-        }
-    } while (pTokenizer.Consume(':'));
-
-    return true;
-}
-
 } // namespace models
 } // namespace data
 } // namespace ra
diff --git a/src/data/models/LocalBadgesModel.hh b/src/data/models/LocalBadgesModel.hh
index 8f73a96b..8a5c6104 100644
--- a/src/data/models/LocalBadgesModel.hh
+++ b/src/data/models/LocalBadgesModel.hh
@@ -22,12 +22,12 @@ public:
 	void RemoveReference(const std::wstring& sBadgeName, bool bCommitted);
 
 	void Commit(const std::wstring& sPreviousBadgeName, const std::wstring& sNewBadgeName);
-	bool NeedsSerialized() const noexcept;
+	bool NeedsSerialized() const noexcept { return false; }
 
-	void Serialize(ra::services::TextWriter& pWriter) const override;
-	bool Deserialize(ra::Tokenizer& pTokenizer) override;
+	void Serialize(ra::services::TextWriter&) const noexcept override {}
+	bool Deserialize(ra::Tokenizer&) noexcept override { return true; }
 
-	int GetReferenceCount(const std::wstring& sBadgeName) const;
+	int GetReferenceCount(const std::wstring& sBadgeName, bool bCommitted) const;
 	void DeleteUncommittedBadges();
 
 private:
diff --git a/src/services/AchievementRuntime.cpp b/src/services/AchievementRuntime.cpp
index c5b58480..99a5d4f4 100644
--- a/src/services/AchievementRuntime.cpp
+++ b/src/services/AchievementRuntime.cpp
@@ -156,39 +156,35 @@ int AchievementRuntime::ActivateLeaderboard(unsigned int nId, const std::string&
     return rc_runtime_activate_lboard(&m_pRuntime, nId, sDefinition.c_str(), nullptr, 0);
 }
 
-void AchievementRuntime::ActivateRichPresence(const std::string& sScript)
+void AchievementRuntime::ActivateRichPresence(const std::string& sScript) noexcept
 {
     EnsureInitialized();
 
     if (sScript.empty())
     {
-        if (m_pRuntime.richpresence_display_buffer != nullptr)
-        {
-            free(m_pRuntime.richpresence_display_buffer);
-            m_pRuntime.richpresence_display_buffer = nullptr;
-        }
+        m_nRichPresenceParseResult = RC_OK;
+        if (m_pRuntime.richpresence != nullptr)
+            m_pRuntime.richpresence->richpresence = nullptr;
     }
     else
     {
-        const auto nResult = rc_runtime_activate_richpresence(&m_pRuntime, sScript.c_str(), nullptr, 0);
-        if (nResult != RC_OK)
-        {
-            const std::string sErrorRP = ra::StringPrintf("Parse error %d: %s\n", nResult, rc_error_str(nResult));
-            m_pRuntime.richpresence_display_buffer = _strdup(sErrorRP.c_str());
-        }
+        m_nRichPresenceParseResult = rc_runtime_activate_richpresence(&m_pRuntime, sScript.c_str(), nullptr, 0);
     }
 }
 
 std::wstring AchievementRuntime::GetRichPresenceDisplayString() const
 {
-    if (!m_bInitialized)
-        return std::wstring(L"No Rich Presence defined.");
+    if (m_bInitialized)
+    {
+        if (m_nRichPresenceParseResult != RC_OK)
+            return ra::StringPrintf(L"Parse error %d: %s\n", m_nRichPresenceParseResult, rc_error_str(m_nRichPresenceParseResult));
 
-    const char* sRichPresence = rc_runtime_get_richpresence(&m_pRuntime);
-    if (sRichPresence == nullptr || *sRichPresence == '\0')
-        return std::wstring(L"No Rich Presence defined.");
+        char sRichPresence[256];
+        if (rc_runtime_get_richpresence(&m_pRuntime, sRichPresence, sizeof(sRichPresence), rc_peek_callback, nullptr, nullptr) > 0)
+            return ra::Widen(sRichPresence);
+    }
 
-    return ra::Widen(sRichPresence);
+    return std::wstring(L"No Rich Presence defined.");
 }
 
 void AchievementRuntime::SetPaused(bool bValue) 
@@ -405,13 +401,15 @@ static void ProcessStateString(Tokenizer& pTokenizer, unsigned int nId, rc_trigg
                         pCondition->current_hits = condSource.nHits;
                         if (IsMemoryOperand(pCondition->operand1.type))
                         {
-                            pCondition->operand1.value.memref->value = condSource.nSourceVal;
-                            pCondition->operand1.value.memref->previous = condSource.nSourcePrev;
+                            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))
                         {
-                            pCondition->operand2.value.memref->value = condSource.nTargetVal;
-                            pCondition->operand2.value.memref->previous = condSource.nTargetPrev;
+                            pCondition->operand2.value.memref->value.value = condSource.nTargetVal;
+                            pCondition->operand2.value.memref->value.prior = condSource.nTargetPrev;
+                            pCondition->operand2.value.memref->value.changed = 0;
                         }
 
                         pCondition = pCondition->next;
@@ -472,15 +470,16 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se
 
         if (c == '$') // memory reference
         {
-            rc_memref_value_t pMemRef{};
+            rc_memref_t pMemRef{};
 
             const auto cPrefix = tokenizer.PeekChar();
             tokenizer.Advance();
-            pMemRef.memref.size = ComparisonSizeFromPrefix(cPrefix);
+            pMemRef.value.size = ComparisonSizeFromPrefix(cPrefix);
 
             const auto sAddr = tokenizer.ReadTo(':');
-            pMemRef.memref.address = ra::ByteAddressFromString(sAddr);
+            pMemRef.address = ra::ByteAddressFromString(sAddr);
 
+            unsigned delta = 0;
             while (tokenizer.PeekChar() != '#' && !tokenizer.EndOfString())
             {
                 tokenizer.Advance();
@@ -490,11 +489,12 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se
 
                 switch (cField)
                 {
-                    case 'v': pMemRef.value = nValue; break;
-                    case 'd': pMemRef.previous = nValue; break;
-                    case 'p': pMemRef.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);
 
             if (tokenizer.PeekChar() == '#')
             {
@@ -506,15 +506,15 @@ bool AchievementRuntime::LoadProgressV2(ra::services::TextReader& pFile, std::se
                     if (tokenizer.PeekChar() == sLineMD5.at(31))
                     {
                         // match! attempt to store it
-                        rc_memref_value_t* pMemoryReference = m_pRuntime.memrefs;
+                        rc_memref_t* pMemoryReference = m_pRuntime.memrefs;
                         while (pMemoryReference)
                         {
-                            if (pMemoryReference->memref.address == pMemRef.memref.address &&
-                                pMemoryReference->memref.size == pMemRef.memref.size)
+                            if (pMemoryReference->address == pMemRef.address &&
+                                pMemoryReference->value.size == pMemRef.value.size)
                             {
-                                pMemoryReference->value = pMemRef.value;
-                                pMemoryReference->previous = pMemRef.previous;
-                                pMemoryReference->prior = pMemRef.prior;
+                                pMemoryReference->value.value = pMemRef.value.value;
+                                pMemoryReference->value.changed = pMemRef.value.changed;
+                                pMemoryReference->value.prior = pMemRef.value.prior;
                                 break;
                             }
 
diff --git a/src/services/AchievementRuntime.hh b/src/services/AchievementRuntime.hh
index 1d131025..95adc265 100644
--- a/src/services/AchievementRuntime.hh
+++ b/src/services/AchievementRuntime.hh
@@ -104,7 +104,7 @@ public:
     /// Specifies the rich presence to process each frame.
     /// </summary>
     /// <remarks>Only updates the memrefs each frame (for deltas), the script is not processed here.</remarks>
-    void ActivateRichPresence(const std::string& sScript);
+    void ActivateRichPresence(const std::string& sScript) noexcept;
 
     /// <summary>
     /// Gets whether or not the loaded game has a rich presence script.
@@ -114,7 +114,7 @@ public:
         if (!m_bInitialized)
             return false;
 
-        return (m_pRuntime.richpresence_display_buffer != nullptr);
+        return (m_pRuntime.richpresence != nullptr && m_pRuntime.richpresence->richpresence != nullptr);
     }
 
     /// <summary>
@@ -208,6 +208,7 @@ private:
     void EnsureInitialized() noexcept;
 
     std::map<unsigned int, std::string> m_vQueuedAchievements;
+    int m_nRichPresenceParseResult = RC_OK;
     bool m_bInitialized = false;
 };
 
diff --git a/src/services/GameIdentifier.cpp b/src/services/GameIdentifier.cpp
index de92ee47..f706f648 100644
--- a/src/services/GameIdentifier.cpp
+++ b/src/services/GameIdentifier.cpp
@@ -181,7 +181,7 @@ void GameIdentifier::ActivateGame(unsigned int nGameId)
                 ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\info.wav");
                 ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>().QueueMessage(
                     L"Playing in Softcore Mode",
-                    bLeaderboardsEnabled ? L"Leaderboard submissions will be canceled." : L"");
+                    bLeaderboardsEnabled ? L"Leaderboard entries will not be submitted." : L"");
             }
         }
     }
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index 21b89c56..e877e824 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -3,6 +3,7 @@
 #include "data\context\GameContext.hh"
 #include "data\context\UserContext.hh"
 
+#include "services\AchievementRuntime.hh"
 #include "services\ILocalStorage.hh"
 #include "services\IThreadPool.hh"
 #include "services\ServiceLocator.hh"
@@ -256,6 +257,20 @@ void AssetListViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args
     WindowViewModelBase::OnValueChanged(args);
 }
 
+void AssetListViewModel::OnValueChanged(const BoolModelProperty::ChangeArgs& args)
+{
+    if (args.Property == IsProcessingActiveProperty)
+    {
+        auto& pRuntime = ra::services::ServiceLocator::GetMutable<ra::services::AchievementRuntime>();
+        pRuntime.SetPaused(!args.tNewValue);
+
+        auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
+        pEmulatorContext.SetMemoryModified();
+    }
+
+    WindowViewModelBase::OnValueChanged(args);
+}
+
 void AssetListViewModel::ApplyFilter()
 {
     const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index 541863fd..2c24772d 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -199,6 +199,7 @@ private:
     void OnActiveGameChanged() override;
 
     void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
+    void OnValueChanged(const BoolModelProperty::ChangeArgs& args) override;
 
     class FilteredListMonitor : public ViewModelCollectionBase::NotifyTarget
     {
diff --git a/src/ui/viewmodels/TriggerConditionViewModel.cpp b/src/ui/viewmodels/TriggerConditionViewModel.cpp
index d071ab56..125cd1e9 100644
--- a/src/ui/viewmodels/TriggerConditionViewModel.cpp
+++ b/src/ui/viewmodels/TriggerConditionViewModel.cpp
@@ -217,7 +217,7 @@ void TriggerConditionViewModel::SetOperand(const IntModelProperty& pTypeProperty
         {
             const auto nSize = MapMemSize(operand.size);
             SetValue(pSizeProperty, ra::etoi(nSize));
-            SetValue(pValueProperty, operand.value.memref->memref.address);
+            SetValue(pValueProperty, operand.value.memref->address);
             break;
         }
 
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index e54429bb..be39680a 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -7,6 +7,7 @@
 #include "services\IConfiguration.hh"
 
 #include "ui\viewmodels\MessageBoxViewModel.hh"
+#include "ui\viewmodels\WindowManager.hh"
 
 #include "ui\win32\bindings\GridAddressColumnBinding.hh"
 #include "ui\win32\bindings\GridLookupColumnBinding.hh"
@@ -167,6 +168,24 @@ public:
         return L"";
     }
 
+    bool HandleRightClick(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) override
+    {
+        if (IsAddressType(vmItems, nIndex))
+        {
+            const auto nAddress = vmItems.GetItemValue(nIndex, *m_pBoundProperty);
+
+            auto& pMemoryInspector = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryInspector;
+            pMemoryInspector.SetCurrentAddress(nAddress);
+
+            if (!pMemoryInspector.IsVisible())
+                pMemoryInspector.Show();
+
+            return true;
+        }
+
+        return GridNumberColumnBinding::HandleRightClick(vmItems, nIndex);
+    }
+
 protected:
     bool IsAddressType(const ra::ui::ViewModelCollectionBase& vmItems, gsl::index nIndex) const
     {
diff --git a/src/ui/win32/DialogBase.cpp b/src/ui/win32/DialogBase.cpp
index 59adda10..087d590a 100644
--- a/src/ui/win32/DialogBase.cpp
+++ b/src/ui/win32/DialogBase.cpp
@@ -281,6 +281,22 @@ INT_PTR CALLBACK DialogBase::DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPA
                     return 0;
                 }
 
+                case NM_RCLICK:
+                {
+                    ra::ui::win32::bindings::GridBinding* pGridBinding;
+                    GSL_SUPPRESS_TYPE1 pGridBinding = dynamic_cast<ra::ui::win32::bindings::GridBinding*>(
+                        FindControlBinding(pnmHdr->hwndFrom));
+
+                    if (pGridBinding)
+                    {
+                        const NMITEMACTIVATE* pnmItemActivate;
+                        GSL_SUPPRESS_TYPE1{ pnmItemActivate = reinterpret_cast<const NMITEMACTIVATE*>(lParam); }
+                        pGridBinding->OnNmRClick(pnmItemActivate);
+                    }
+
+                    return 0;
+                }
+
                 case NM_DBLCLK:
                 {
                     ra::ui::win32::bindings::GridBinding* pGridBinding;
diff --git a/src/ui/win32/bindings/ControlBinding.cpp b/src/ui/win32/bindings/ControlBinding.cpp
index fbdb241a..d26d1f62 100644
--- a/src/ui/win32/bindings/ControlBinding.cpp
+++ b/src/ui/win32/bindings/ControlBinding.cpp
@@ -5,6 +5,8 @@
 #include "services\ServiceLocator.hh"
 #include "services\IClock.hh"
 
+#include "ui\viewmodels\MessageBoxViewModel.hh"
+
 #include "ui\win32\Desktop.hh"
 
 namespace ra {
@@ -12,6 +14,9 @@ namespace ui {
 namespace win32 {
 namespace bindings {
 
+static std::atomic_int g_nSuspendRepaintCount = 0;
+static std::set<HWND> g_vSuspendedRepaintHWnds;
+
 ControlBinding::~ControlBinding() noexcept
 {
     DisableBinding();
@@ -23,10 +28,8 @@ ControlBinding::~ControlBinding() noexcept
     }
 }
 
-void ControlBinding::ForceRepaint(HWND hWnd)
+static bool NeedsUpdate()
 {
-    InvalidateRect(hWnd, nullptr, FALSE);
-
     // When using SDL, the Windows message queue is never empty (there's a flood of WM_PAINT messages for the
     // SDL window). InvalidateRect only generates a WM_PAINT when the message queue is empty, so we have to
     // explicitly generate (and dispatch) a WM_PAINT message by calling UpdateWindow.
@@ -34,8 +37,60 @@ void ControlBinding::ForceRepaint(HWND hWnd)
     {
         case RA_Libretro:
         case RA_Oricutron:
-            UpdateWindow(hWnd);
-            break;
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+void ControlBinding::ForceRepaint(HWND hWnd)
+{
+    if (g_nSuspendRepaintCount == 0)
+    {
+        ::InvalidateRect(hWnd, nullptr, FALSE);
+
+        if (NeedsUpdate())
+            ::UpdateWindow(hWnd);
+    }
+    else
+    {
+#ifdef DEBUG
+        if (g_vSuspendedRepaintHWnds.find(hWnd) != g_vSuspendedRepaintHWnds.end())
+            return;
+#endif
+        g_vSuspendedRepaintHWnds.insert(hWnd);
+    }
+}
+
+void ControlBinding::SuspendRepaint()
+{
+    if (++g_nSuspendRepaintCount == 50)
+        ra::ui::viewmodels::MessageBoxViewModel::ShowWarningMessage(L"SuspendRepaint count is abnormally high.", L"Make sure to call ResumeRepaint whenever you call SuspendRepaint.");
+}
+
+void ControlBinding::ResumeRepaint()
+{
+    if (--g_nSuspendRepaintCount == 0)
+    {
+        std::set<HWND> vSuspendedRepaintWnds;
+        vSuspendedRepaintWnds.swap(g_vSuspendedRepaintHWnds);
+
+        if (!vSuspendedRepaintWnds.empty())
+        {
+            for (HWND hWnd : vSuspendedRepaintWnds)
+                ::InvalidateRect(hWnd, nullptr, FALSE);
+
+            if (NeedsUpdate())
+            {
+                for (HWND hWnd : vSuspendedRepaintWnds)
+                    ::UpdateWindow(hWnd);
+            }
+        }
+    }
+    else
+    {
+        Expects(g_nSuspendRepaintCount > 0);
     }
 }
 
diff --git a/src/ui/win32/bindings/ControlBinding.hh b/src/ui/win32/bindings/ControlBinding.hh
index 7a46a9d9..ec219c25 100644
--- a/src/ui/win32/bindings/ControlBinding.hh
+++ b/src/ui/win32/bindings/ControlBinding.hh
@@ -87,6 +87,35 @@ public:
     /// </summary>
     static void ForceRepaint(HWND hWnd);
 
+    /// <summary>
+    /// Delays ForceRepaint calls until resumed.
+    /// </summary>
+    static void SuspendRepaint();
+
+    /// <summary>
+    /// Resumes ForceRepaint calls.
+    /// </summary>
+    static void ResumeRepaint();
+
+    class RepaintGuard final
+    {
+    public:
+        RepaintGuard() noexcept
+        {
+            GSL_SUPPRESS_F6 ControlBinding::SuspendRepaint();
+        }
+
+        ~RepaintGuard() noexcept
+        {
+            GSL_SUPPRESS_F6 ControlBinding::ResumeRepaint();
+        }
+
+        RepaintGuard(const RepaintGuard&) noexcept = delete;
+        RepaintGuard& operator=(const RepaintGuard&) noexcept = delete;
+        RepaintGuard(RepaintGuard&&) noexcept = delete;
+        RepaintGuard& operator=(RepaintGuard&&) noexcept = delete;
+    };
+
     /// <summary>
     /// DO NOT CALL! Must be public for WINAPI interop.
     /// </summary>
diff --git a/src/ui/win32/bindings/GridBinding.cpp b/src/ui/win32/bindings/GridBinding.cpp
index 400a8810..df599045 100644
--- a/src/ui/win32/bindings/GridBinding.cpp
+++ b/src/ui/win32/bindings/GridBinding.cpp
@@ -1167,6 +1167,20 @@ void GridBinding::OnNmClick(const NMITEMACTIVATE* pnmItemActivate)
     }
 }
 
+void GridBinding::OnNmRClick(const NMITEMACTIVATE* pnmItemActivate)
+{
+    // if an in-place editor is open, close it.
+    if (m_hInPlaceEditor)
+        SendMessage(m_hInPlaceEditor, WM_KILLFOCUS, 0, 0);
+
+    if (ra::to_unsigned(pnmItemActivate->iSubItem) < m_vColumns.size())
+    {
+        const auto& pColumn = m_vColumns.at(pnmItemActivate->iSubItem);
+        if (pColumn->HandleRightClick(GetItems(), gsl::narrow_cast<gsl::index>(pnmItemActivate->iItem) - m_nScrollOffset))
+            return;
+    }
+}
+
 void GridBinding::OnNmDblClick(const NMITEMACTIVATE* pnmItemActivate)
 {
     if (ra::to_unsigned(pnmItemActivate->iSubItem) < m_vColumns.size())
diff --git a/src/ui/win32/bindings/GridBinding.hh b/src/ui/win32/bindings/GridBinding.hh
index a76dc892..a2abb28f 100644
--- a/src/ui/win32/bindings/GridBinding.hh
+++ b/src/ui/win32/bindings/GridBinding.hh
@@ -52,6 +52,7 @@ public:
     void OnLvnGetDispInfo(NMLVDISPINFO& pnmDispInfo);
     void OnTooltipGetDispInfo(NMTTDISPINFO& pnmTtDispInfo);
     virtual void OnNmClick(const NMITEMACTIVATE* pnmItemActivate);
+    virtual void OnNmRClick(const NMITEMACTIVATE* pnmItemActivate);
     virtual void OnNmDblClick(const NMITEMACTIVATE* pnmItemActivate);
     LRESULT OnCustomDraw(NMLVCUSTOMDRAW* pCustomDraw) override;
 
diff --git a/src/ui/win32/bindings/GridColumnBinding.hh b/src/ui/win32/bindings/GridColumnBinding.hh
index 37fb4d0f..cfd7743e 100644
--- a/src/ui/win32/bindings/GridColumnBinding.hh
+++ b/src/ui/win32/bindings/GridColumnBinding.hh
@@ -78,6 +78,7 @@ public:
     virtual HWND CreateInPlaceEditor(HWND, InPlaceEditorInfo&) noexcept(false) { return nullptr; }
 
     virtual bool HandleDoubleClick(const ra::ui::ViewModelCollectionBase&, gsl::index) noexcept(false) { return false; }
+    virtual bool HandleRightClick(const ra::ui::ViewModelCollectionBase&, gsl::index) noexcept(false) { return false; }
 
 protected:
     std::wstring m_sHeader;


Test patch:
diff --git a/tests/data/context/GameAssets_Tests.cpp b/tests/data/context/GameAssets_Tests.cpp
index 0dcc03f4..6edfb1b4 100644
--- a/tests/data/context/GameAssets_Tests.cpp
+++ b/tests/data/context/GameAssets_Tests.cpp
@@ -187,8 +187,7 @@ public:
         gameAssets.ReloadAsset(AssetType::Achievement, GameAssets::FirstLocalId);
 
         const auto& sExpected = ra::StringPrintf("0.0.0.0\nGameName\n"
-            "%u:\"1=1\":Temp:Temp::::Authl:10:::::\"local\\\\22-ABCDE.png\"\n"
-            "b:ABCDE.png=1\n", GameAssets::FirstLocalId);
+            "%u:\"1=1\":Temp:Temp::::Authl:10:::::\"local\\\\22-ABCDE.png\"\n", GameAssets::FirstLocalId);
         Assert::AreEqual(sExpected, gameAssets.GetUserFile());
     }
 
@@ -348,8 +347,7 @@ public:
         gameAssets.mockGameContext.SetGameId(22);
         gameAssets.AddLocalBadgesModel();
         gameAssets.MockUserFileContents(
-            "111000001:\"0xH2345=0\":Test2:::::User:0:0:0:::local\\22-A.png\n"
-            "b:A.png=1\n");
+            "111000001:\"0xH2345=0\":Test2:::::User:0:0:0:::local\\22-A.png\n");
 
         gameAssets.ReloadAllAssets();
 
@@ -359,7 +357,7 @@ public:
         Assert::AreEqual(std::wstring(L"local\\22-A.png"), pAsset->GetBadge());
 
         const auto& pLocalBadges = gameAssets.GetLocalBadgesModel();
-        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-A.png"));
+        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-A.png", true));
     }
 
     TEST_METHOD(TestReloadModifiedFromFile)
diff --git a/tests/data/models/AchievementModel_Tests.cpp b/tests/data/models/AchievementModel_Tests.cpp
index 13db9eae..7227dd4e 100644
--- a/tests/data/models/AchievementModel_Tests.cpp
+++ b/tests/data/models/AchievementModel_Tests.cpp
@@ -194,15 +194,14 @@ public:
         achievement.SetBadge(L"local\\22-ABC.png");
 
         const auto& pLocalBadges = achievement.GetLocalBadgesModel();
-        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png"));
-        Assert::IsFalse(pLocalBadges.NeedsSerialized());
+        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", false));
+        Assert::AreEqual(0, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", true));
 
         // this sets the committed badge reference so it'll show up in the serialized string
         achievement.UpdateLocalCheckpoint();
 
-        Assert::IsTrue(pLocalBadges.NeedsSerialized());
-        pLocalBadges.Serialize(achievement.textWriter);
-        Assert::AreEqual(std::string("b:ABC.png=1"), achievement.textWriter.GetString());
+        Assert::AreEqual(0, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", false));
+        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", true));
     }
 
     TEST_METHOD(TestSetBadgeNameAndReset)
@@ -219,15 +218,16 @@ public:
         achievement.SetBadge(L"local\\22-ABC.png");
 
         const auto& pLocalBadges = achievement.GetLocalBadgesModel();
-        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png"));
-        Assert::IsFalse(pLocalBadges.NeedsSerialized());
+        Assert::AreEqual(1, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", false));
+        Assert::AreEqual(0, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", true));
 
         achievement.SetBadge(L"12345.png");
 
         // the local badge is no longer referenced, so shouldn't be serialized
         achievement.UpdateLocalCheckpoint();
 
-        Assert::IsFalse(pLocalBadges.NeedsSerialized());
+        Assert::AreEqual(0, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", false));
+        Assert::AreEqual(0, pLocalBadges.GetReferenceCount(L"local\\22-ABC.png", true));
     }
 };
 
diff --git a/tests/data/models/LocalBadgesModel_Tests.cpp b/tests/data/models/LocalBadgesModel_Tests.cpp
index 344934d1..482e44f5 100644
--- a/tests/data/models/LocalBadgesModel_Tests.cpp
+++ b/tests/data/models/LocalBadgesModel_Tests.cpp
@@ -46,205 +46,14 @@ public:
         Assert::IsFalse(badges.NeedsSerialized());
     }
 
-    TEST_METHOD(TestSerializeSingle)
-    {
-        LocalBadgesModelHarness badges;
-
-        badges.AddReference(L"local\\123-A.png", false);
-        Assert::IsFalse(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b"), badges.textWriter.GetString());
-        badges.textWriter.GetString().clear();
-
-        badges.Commit(L"", L"local\\123-A.png");
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=1"), badges.textWriter.GetString());
-    }
-
-    TEST_METHOD(TestSerializeSingleMultipleReference)
-    {
-        LocalBadgesModelHarness badges;
-
-        badges.AddReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-A.png", false);
-        Assert::IsFalse(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b"), badges.textWriter.GetString());
-        badges.textWriter.GetString().clear();
-
-        badges.Commit(L"", L"local\\123-A.png");
-        badges.Commit(L"12345", L"local\\123-A.png");
-        badges.Commit(L"22222", L"local\\123-A.png");
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=3"), badges.textWriter.GetString());
-    }
-
-    TEST_METHOD(TestSerializeSinglePartialMultipleReference)
-    {
-        LocalBadgesModelHarness badges;
-
-        badges.AddReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-A.png", false);
-        Assert::IsFalse(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b"), badges.textWriter.GetString());
-        badges.textWriter.GetString().clear();
-
-        badges.Commit(L"", L"local\\123-A.png");
-        badges.Commit(L"12345", L"local\\123-A.png");
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=2"), badges.textWriter.GetString());
-    }
-
-    TEST_METHOD(TestSerializeMultiple)
-    {
-        LocalBadgesModelHarness badges;
-
-        badges.AddReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-B.png", false);
-        badges.AddReference(L"local\\123-C.png", false);
-        Assert::IsFalse(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b"), badges.textWriter.GetString());
-        badges.textWriter.GetString().clear();
-
-        badges.Commit(L"", L"local\\123-A.png");
-        badges.Commit(L"12345", L"local\\123-B.png");
-        badges.Commit(L"22222", L"local\\123-C.png");
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=1:B.png=1:C.png=1"), badges.textWriter.GetString());
-    }
-
-    TEST_METHOD(TestDeserialize)
-    {
-        LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=1:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
-        badges.mockGameContext.SetGameId(123U);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-        Assert::AreEqual(1, badges.GetReferenceCount(L"local\\123-A.png"));
-        Assert::AreEqual(1, badges.GetReferenceCount(L"local\\123-B.png"));
-
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=1:B.png=1"), badges.textWriter.GetString());
-    }
-
-    TEST_METHOD(TestDeserializeChanged)
-    {
-        LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=1:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
-        badges.mockGameContext.SetGameId(123U);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-A.png", 1234);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-B.png", 2345);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-
-        badges.RemoveReference(L"local\\123-A.png", true);
-        badges.AddReference(L"local\\123-C.png", false);
-        badges.Commit(L"local\\123-A.png", L"local\\123-C.png");
-
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:B.png=1:C.png=1"), badges.textWriter.GetString());
-
-        Assert::AreEqual({ -1 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-A.png"));
-        Assert::AreEqual({ 2345 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-B.png"));
-    }
-
-    TEST_METHOD(TestDeserializePublished)
-    {
-        LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=1:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
-        badges.mockGameContext.SetGameId(123U);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-A.png", 1234);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-B.png", 2345);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-
-        badges.RemoveReference(L"local\\123-A.png", true);
-        badges.Commit(L"local\\123-A.png", L"22222");
-
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:B.png=1"), badges.textWriter.GetString());
-
-        // combination of RemoveReference and Commit should have deleted 123-A.png
-        Assert::AreEqual({ -1 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-A.png"));
-        Assert::AreEqual({ 2345 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-B.png"));
-    }
-
-    TEST_METHOD(TestDeserializeChangedStillReferenced)
-    {
-        LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=2:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
-        badges.mockGameContext.SetGameId(123U);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-A.png", 1234);
-        badges.mockFileSystem.MockFileSize(L"RACache\\Badges\\local\\123-B.png", 2345);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-
-        badges.RemoveReference(L"local\\123-A.png", true);
-        badges.AddReference(L"local\\123-C.png", false);
-        badges.Commit(L"local\\123-A.png", L"local\\123-C.png");
-
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=1:B.png=1:C.png=1"), badges.textWriter.GetString());
-
-        Assert::AreEqual({ 1234 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-A.png"));
-        Assert::AreEqual({ 2345 }, badges.mockFileSystem.GetFileSize(L"RACache\\Badges\\local\\123-B.png"));
-    }
-
-    TEST_METHOD(TestDeserializeChangedNotCommitted)
-    {
-        LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=1:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
-        badges.mockGameContext.SetGameId(123U);
-        badges.mockFileSystem.MockFileSize(L"local\\123-A.png", 1234);
-        badges.mockFileSystem.MockFileSize(L"local\\123-B.png", 2345);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-
-        badges.RemoveReference(L"local\\123-A.png", false);
-        badges.AddReference(L"local\\123-C.png", false);
-
-        Assert::IsTrue(badges.NeedsSerialized());
-        badges.Serialize(badges.textWriter);
-        Assert::AreEqual(std::string("b:A.png=1:B.png=1"), badges.textWriter.GetString());
-
-        Assert::AreEqual({ 1234 }, badges.mockFileSystem.GetFileSize(L"local\\123-A.png"));
-        Assert::AreEqual({ 2345 }, badges.mockFileSystem.GetFileSize(L"local\\123-B.png"));
-    }
-
     TEST_METHOD(TestDeleteUncommittedBadges)
     {
         LocalBadgesModelHarness badges;
-        std::string sInput = "b:A.png=1:B.png=1";
-        ra::Tokenizer pTokenizer(sInput);
         badges.mockGameContext.SetGameId(123U);
         badges.mockFileSystem.MockFileSize(L"local\\123-A.png", 1234);
+        badges.AddReference(L"local\\123-A.png", true);
         badges.mockFileSystem.MockFileSize(L"local\\123-B.png", 2345);
-
-        pTokenizer.Advance(2); // GameAssets reads "b:"
-        Assert::IsTrue(badges.Deserialize(pTokenizer));
-
+        badges.AddReference(L"local\\123-B.png", true);
         badges.mockFileSystem.MockFileSize(L"local\\123-C.png", 3456);
         badges.AddReference(L"local\\123-C.png", false);
         Assert::AreEqual({ 3456 }, badges.mockFileSystem.GetFileSize(L"local\\123-C.png"));
diff --git a/tests/services/AchievementRuntime_Tests.cpp b/tests/services/AchievementRuntime_Tests.cpp
index f74155ed..0070c9fa 100644
--- a/tests/services/AchievementRuntime_Tests.cpp
+++ b/tests/services/AchievementRuntime_Tests.cpp
@@ -54,16 +54,11 @@ public:
     ra::services::mocks::MockThreadPool mockThreadPool;
     ra::ui::viewmodels::mocks::MockWindowManager mockWindowManager;
 
-    rc_memref_value_t* GetMemRefs() noexcept
+    rc_memref_t* GetMemRefs() noexcept
     {
         return m_pRuntime.memrefs;
     }
 
-    void ForceRichPresenceUpdate() noexcept
-    {
-        m_pRuntime.richpresence_update_timer = 0;
-    }
-
     size_t GetTriggerCount() const noexcept
     {
         return m_pRuntime.trigger_count;
@@ -526,25 +521,25 @@ public:
         ach.SetConditionHitCount(0, 0, 2);
         ach.SetConditionHitCount(0, 1, 0);
         auto pMemRef = runtime.GetMemRefs(); // 0xH1234
-        pMemRef->value = 0x02;
-        pMemRef->previous = 0x03;
-        pMemRef->prior = 0x03;
+        pMemRef->value.value = 0x02;
+        pMemRef->value.changed = 0;
+        pMemRef->value.prior = 0x03;
         pMemRef = pMemRef->next; // 0xX1234
-        pMemRef->value = 0x020000;
-        pMemRef->previous = 0x020000;
-        pMemRef->prior = 0x040000;
+        pMemRef->value.value = 0x020000;
+        pMemRef->value.changed = 1;
+        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 = 0;
-        pMemRef->previous = 0;
-        pMemRef->prior = 0;
+        pMemRef->value.value = 0;
+        pMemRef->value.changed = 0;
+        pMemRef->value.prior = 0;
         pMemRef = pMemRef->next;
-        pMemRef->value = 0;
-        pMemRef->previous = 0;
-        pMemRef->prior = 0;
+        pMemRef->value.value = 0;
+        pMemRef->value.changed = 0;
+        pMemRef->value.prior = 0;
         ach.SetConditionHitCount(0, 0, 1);
         ach.SetConditionHitCount(0, 1, 1);
 
@@ -555,13 +550,13 @@ public:
         Assert::AreEqual(0U, ach.GetConditionHitCount(0, 1));
 
         pMemRef = runtime.GetMemRefs();
-        Assert::AreEqual(0x02U, pMemRef->value);
-        Assert::AreEqual(0x03U, pMemRef->previous);
-        Assert::AreEqual(0x03U, pMemRef->prior);
+        Assert::AreEqual(0x02U, pMemRef->value.value);
+        Assert::AreEqual(0, (int)pMemRef->value.changed);
+        Assert::AreEqual(0x03U, pMemRef->value.prior);
         pMemRef = pMemRef->next;
-        Assert::AreEqual(0x020000U, pMemRef->value);
-        Assert::AreEqual(0x020000U, pMemRef->previous);
-        Assert::AreEqual(0x040000U, pMemRef->prior);
+        Assert::AreEqual(0x020000U, pMemRef->value.value);
+        Assert::AreEqual(1, (int)pMemRef->value.changed);
+        Assert::AreEqual(0x040000U, pMemRef->value.prior);
     }
 
     TEST_METHOD(TestLoadProgressV1)
@@ -603,25 +598,25 @@ public:
         ach.SetConditionHitCount(1, 0, 2);
         ach.SetConditionHitCount(1, 1, 0);
         auto pMemRef = runtime.GetMemRefs(); // 0xH1234
-        pMemRef->value = 0x02;
-        pMemRef->previous = 0x03;
-        pMemRef->prior = 0x03;
+        pMemRef->value.value = 0x02;
+        pMemRef->value.changed = 1;
+        pMemRef->value.prior = 0x03;
         pMemRef = pMemRef->next; // 0xX1234
-        pMemRef->value = 0x020000;
-        pMemRef->previous = 0x020000;
-        pMemRef->prior = 0x040000;
+        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 = 0;
-        pMemRef->previous = 0;
-        pMemRef->prior = 0;
+        pMemRef->value.value = 0;
+        pMemRef->value.changed = 0;
+        pMemRef->value.prior = 0;
         pMemRef = pMemRef->next;
-        pMemRef->value = 0;
-        pMemRef->previous = 0;
-        pMemRef->prior = 0;
+        pMemRef->value.value = 0;
+        pMemRef->value.changed = 0;
+        pMemRef->value.prior = 0;
         ach.SetConditionHitCount(1, 0, 1);
         ach.SetConditionHitCount(1, 1, 1);
 
@@ -632,13 +627,13 @@ public:
         Assert::AreEqual(0U, ach.GetConditionHitCount(1, 1));
 
         pMemRef = runtime.GetMemRefs();
-        Assert::AreEqual(0x02U, pMemRef->value);
-        Assert::AreEqual(0x03U, pMemRef->previous);
-        Assert::AreEqual(0x03U, pMemRef->prior);
+        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);
-        Assert::AreEqual(0x020000U, pMemRef->previous);
-        Assert::AreEqual(0x040000U, pMemRef->prior);
+        Assert::AreEqual(0x020000U, pMemRef->value.value);
+        Assert::AreEqual(0, (int)pMemRef->value.changed);
+        Assert::AreEqual(0x040000U, pMemRef->value.prior);
     }
 
     TEST_METHOD(TestActivateClonedAchievement)
@@ -885,24 +880,20 @@ public:
         Assert::AreEqual(std::wstring(L"18 0"), runtime.GetRichPresenceDisplayString());
 
         // first update - updates value and delta
-        runtime.ForceRichPresenceUpdate();
         runtime.Process(changes);
         Assert::AreEqual(std::wstring(L"18 18"), runtime.GetRichPresenceDisplayString());
 
         // second update - updates delta
-        runtime.ForceRichPresenceUpdate();
         runtime.Process(changes);
         Assert::AreEqual(std::wstring(L"18 18"), runtime.GetRichPresenceDisplayString());
 
         // third update - updates value after change
         memory.at(1) = 11;
-        runtime.ForceRichPresenceUpdate();
         runtime.Process(changes);
         Assert::AreEqual(std::wstring(L"11 18"), runtime.GetRichPresenceDisplayString());
 
         // fourth update - updates delta and value after change
         memory.at(1) = 13;
-        runtime.ForceRichPresenceUpdate();
         runtime.Process(changes);
         Assert::AreEqual(std::wstring(L"13 11"), runtime.GetRichPresenceDisplayString());
     }
diff --git a/tests/services/GameIdentifier_Tests.cpp b/tests/services/GameIdentifier_Tests.cpp
index 6b516429..7249e769 100644
--- a/tests/services/GameIdentifier_Tests.cpp
+++ b/tests/services/GameIdentifier_Tests.cpp
@@ -321,7 +321,7 @@ public:
         const auto* pPopup = identifier.mockOverlayManager.GetMessage(1U);
         Expects(pPopup != nullptr);
         Assert::AreEqual(std::wstring(L"Playing in Softcore Mode"), pPopup->GetTitle());
-        Assert::AreEqual(std::wstring(L"Leaderboard submissions will be canceled."), pPopup->GetDescription());
+        Assert::AreEqual(std::wstring(L"Leaderboard entries will not be submitted."), pPopup->GetDescription());
     }
 
     TEST_METHOD(TestActivateGamePendingNonHardcoreNoLeaderboards)
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index 5e21fff2..bbaab666 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -10,6 +10,7 @@
 
 #include "tests\mocks\MockAchievementRuntime.hh"
 #include "tests\mocks\MockDesktop.hh"
+#include "tests\mocks\MockEmulatorContext.hh"
 #include "tests\mocks\MockGameContext.hh"
 #include "tests\mocks\MockLocalStorage.hh"
 #include "tests\mocks\MockThreadPool.hh"
@@ -86,6 +87,7 @@ private:
         ra::services::mocks::MockAchievementRuntime mockRuntime;
         ra::services::mocks::MockThreadPool mockThreadPool;
         ra::services::mocks::MockLocalStorage mockLocalStorage;
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
         ra::data::context::mocks::MockGameContext mockGameContext;
         ra::data::context::mocks::MockUserContext mockUserContext;
         ra::ui::mocks::MockDesktop mockDesktop;
@@ -2267,6 +2269,24 @@ public:
         // new item will be ignored when a selection is reset.
         Assert::IsNull(vmAssetList.mockGameContext.Assets().FindAchievement({ 111000001U }));
     }
+
+    TEST_METHOD(TestIsProcessingActive)
+    {
+        AssetListViewModelHarness vmAssetList;
+        Assert::IsTrue(vmAssetList.IsProcessingActive());
+        Assert::IsFalse(vmAssetList.mockRuntime.IsPaused());
+        Assert::IsFalse(vmAssetList.mockEmulatorContext.WasMemoryModified());
+
+        vmAssetList.SetProcessingActive(false);
+        Assert::IsFalse(vmAssetList.IsProcessingActive());
+        Assert::IsTrue(vmAssetList.mockRuntime.IsPaused());
+        Assert::IsTrue(vmAssetList.mockEmulatorContext.WasMemoryModified());
+
+        vmAssetList.SetProcessingActive(true);
+        Assert::IsTrue(vmAssetList.IsProcessingActive());
+        Assert::IsFalse(vmAssetList.mockRuntime.IsPaused());
+        Assert::IsTrue(vmAssetList.mockEmulatorContext.WasMemoryModified());
+    }
 };
 
 } // namespace tests