Repository: RetroAchievements/RAIntegration
Pull request #651 (2020-12-20 16:16:24)

handle right-click to go to address


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/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/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