Repository: RetroAchievements/RAIntegration (cpp)

Issue:
No error message when attempting to overwrite code notes as a Jr Dev
After clicking 'Yes' to (attempt to) overwrite a code note as a Jr Dev, the Progress Monitor shows up, then goes away, making it seem like the code note was successfully overwritten, even though it wasn't.

Pull request:
report when code note update fails
fixes #991 

Patch:
diff --git a/src/RA_Resource.h b/src/RA_Resource.h
index ae5df029..72e61579 100644
--- a/src/RA_Resource.h
+++ b/src/RA_Resource.h
@@ -144,6 +144,8 @@
 #define IDC_RA_CAPTURE_4                1239
 #define IDC_RA_MEMVIEWER_4              1240
 #define IDC_RA_DISPLAY_PROGRESS_INDICATOR 1241
+#define IDC_RA_MEMVIEW_32BITBE          1242
+
 
 #define IDD_RA_MEMORY                   1501
 #define IDD_RA_ACHIEVEMENTS             1502
@@ -211,7 +213,7 @@
 #ifndef APSTUDIO_READONLY_SYMBOLS
 #define _APS_NEXT_RESOURCE_VALUE        122
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1242
+#define _APS_NEXT_CONTROL_VALUE         1243
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 89669243..cd5d3f7d 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -65,6 +65,7 @@ BEGIN
     CONTROL         "8-bit",IDC_RA_MEMVIEW_8BIT,"Button",BS_AUTORADIOBUTTON | WS_GROUP,58,207,31,10
     CONTROL         "16-bit",IDC_RA_MEMVIEW_16BIT,"Button",BS_AUTORADIOBUTTON,90,207,33,10
     CONTROL         "32-bit",IDC_RA_MEMVIEW_32BIT,"Button",BS_AUTORADIOBUTTON,126,207,33,10
+    CONTROL         "32-bit BE",IDC_RA_MEMVIEW_32BITBE,"Button",BS_AUTORADIOBUTTON,160,207,40,10
     LTEXT           "Bit: 7 6 5 4 3 2 1 0",IDC_RA_MEMBITS_TITLE,214,200,110,8,0,WS_EX_RIGHT
     LTEXT           "    0 0 0 0 0 0 0 0",IDC_RA_MEMBITS,214,209,110,8,0,WS_EX_RIGHT
     CONTROL         "Viewer",IDC_RA_MEMVIEWER,"MemoryViewerControl",WS_TABSTOP,4,218,324,88
diff --git a/src/data/models/CapturedTriggerHits.cpp b/src/data/models/CapturedTriggerHits.cpp
index ea149c41..2613ec48 100644
--- a/src/data/models/CapturedTriggerHits.cpp
+++ b/src/data/models/CapturedTriggerHits.cpp
@@ -31,11 +31,23 @@ static void CaptureHitCounts(const rc_trigger_t* pTrigger, std::vector<unsigned>
 
 static void RestoreCondSetHitCounts(rc_condset_t* pCondSet, const std::vector<unsigned>& vCapturedHitCounts, gsl::index& nIndex)
 {
-    rc_condition_t* pCondition = pCondSet->conditions;
-    while (pCondition != nullptr)
+    if (vCapturedHitCounts.empty())
     {
-        pCondition->current_hits = vCapturedHitCounts.at(nIndex++);
-        pCondition = pCondition->next;
+        rc_condition_t* pCondition = pCondSet->conditions;
+        while (pCondition != nullptr)
+        {
+            pCondition->current_hits = 0;
+            pCondition = pCondition->next;
+        }
+    }
+    else
+    {
+        rc_condition_t* pCondition = pCondSet->conditions;
+        while (pCondition != nullptr)
+        {
+            pCondition->current_hits = vCapturedHitCounts.at(nIndex++);
+            pCondition = pCondition->next;
+        }
     }
 }
 
@@ -72,8 +84,8 @@ bool CapturedTriggerHits::Restore(rc_trigger_t* pTrigger, const std::string& sTr
     if (sCapturedHitsMD5 != m_sCapturedHitsMD5)
         return false;
 
-    if (!m_vCapturedHitCounts.empty())
-        RestoreHitCounts(pTrigger, m_vCapturedHitCounts);
+    RestoreHitCounts(pTrigger, m_vCapturedHitCounts);
+    pTrigger->has_hits = !m_vCapturedHitCounts.empty();
 
     return true;
 }
@@ -84,8 +96,8 @@ void CapturedTriggerHits::Capture(const rc_value_t* pValue, const std::string& s
 
     m_sCapturedHitsMD5 = RAGenerateMD5(sValue);
 
-    if (pValue->conditions)
-        CaptureCondSetHitCounts(pValue->conditions, m_vCapturedHitCounts);
+    for (const auto* conditions = pValue->conditions; conditions; conditions = conditions->next)
+        CaptureCondSetHitCounts(conditions, m_vCapturedHitCounts);
 }
 
 bool CapturedTriggerHits::Restore(rc_value_t* pValue, const std::string& sValue) const
@@ -97,11 +109,9 @@ bool CapturedTriggerHits::Restore(rc_value_t* pValue, const std::string& sValue)
     if (sCapturedHitsMD5 != m_sCapturedHitsMD5)
         return false;
 
-    if (!m_vCapturedHitCounts.empty())
-    {
-        gsl::index nIndex = 0;
-        RestoreCondSetHitCounts(pValue->conditions, m_vCapturedHitCounts, nIndex);
-    }
+    gsl::index nIndex = 0;
+    for (auto* conditions = pValue->conditions; conditions; conditions = conditions->next)
+        RestoreCondSetHitCounts(conditions, m_vCapturedHitCounts, nIndex);
 
     return true;
 }
diff --git a/src/data/models/CodeNotesModel.cpp b/src/data/models/CodeNotesModel.cpp
index 5c493cd7..d0fab892 100644
--- a/src/data/models/CodeNotesModel.cpp
+++ b/src/data/models/CodeNotesModel.cpp
@@ -39,6 +39,11 @@ void CodeNotesModel::Refresh(unsigned int nGameId, CodeNoteChangedFunction fCode
     if (callback == nullptr) // unit test workaround to avoid server call
         return;
 
+    {
+        std::unique_lock<std::mutex> lock(m_oMutex);
+        m_bRefreshing = true;
+    }
+
     ra::api::FetchCodeNotes::Request request;
     request.GameId = nGameId;
     request.CallAsync([this, callback](const ra::api::FetchCodeNotes::Response& response)
@@ -67,6 +72,16 @@ void CodeNotesModel::Refresh(unsigned int nGameId, CodeNoteChangedFunction fCode
             }
         }
 
+        std::map<ra::ByteAddress, std::wstring> mPendingCodeNotes;
+        {
+            std::unique_lock<std::mutex> lock(m_oMutex);
+            mPendingCodeNotes.swap(m_mPendingCodeNotes);
+            m_bRefreshing = false;
+        }
+
+        for (const auto pNote : mPendingCodeNotes)
+            SetCodeNote(pNote.first, pNote.second);
+
         callback();
     });
 }
@@ -609,6 +624,12 @@ void CodeNotesModel::SetCodeNote(ra::ByteAddress nAddress, const std::wstring& s
     {
         std::unique_lock<std::mutex> lock(m_oMutex);
 
+        if (m_bRefreshing)
+        {
+            m_mPendingCodeNotes[nAddress] = sNote;
+            return;
+        }
+
         const auto pIter = m_mCodeNotes.find(nAddress);
         if (pIter != m_mCodeNotes.end())
         {
@@ -985,6 +1006,7 @@ bool CodeNotesModel::Deserialize(ra::Tokenizer& pTokenizer)
     if (!ReadQuoted(pTokenizer, sNote))
         return false;
 
+    ra::NormalizeLineEndings(sNote);
     SetCodeNote(nAddress, sNote);
     return true;
 }
diff --git a/src/data/models/CodeNotesModel.hh b/src/data/models/CodeNotesModel.hh
index 3283e5b8..a65e4c6c 100644
--- a/src/data/models/CodeNotesModel.hh
+++ b/src/data/models/CodeNotesModel.hh
@@ -220,11 +220,14 @@ protected:
     std::map<ra::ByteAddress, CodeNote> m_mCodeNotes;
     std::map<ra::ByteAddress, std::pair<std::string, std::wstring>> m_mOriginalCodeNotes;
 
+    std::map<ra::ByteAddress, std::wstring> m_mPendingCodeNotes;
+
     const CodeNote* FindCodeNoteInternal(ra::ByteAddress nAddress) const;
     void EnumerateCodeNotes(std::function<bool(ra::ByteAddress nAddress, const CodeNote& pCodeNote)> callback, bool bIncludeDerived) const;
 
     unsigned int m_nGameId = 0;
     bool m_bHasPointers = false;
+    bool m_bRefreshing = false;
 
     CodeNoteChangedFunction m_fCodeNoteChanged;
 
diff --git a/src/ui/viewmodels/AssetUploadViewModel.cpp b/src/ui/viewmodels/AssetUploadViewModel.cpp
index 89839172..f334c24b 100644
--- a/src/ui/viewmodels/AssetUploadViewModel.cpp
+++ b/src/ui/viewmodels/AssetUploadViewModel.cpp
@@ -368,6 +368,17 @@ void AssetUploadViewModel::UploadCodeNote(ra::data::models::CodeNotesModel& pNot
     }
 }
 
+bool AssetUploadViewModel::HasFailures() const
+{
+    for (const auto& pItem : m_vUploadQueue)
+    {
+        if (pItem.nState == UploadState::Failed)
+            return true;
+    }
+
+    return false;
+}
+
 void AssetUploadViewModel::ShowResults() const
 {
     int nSuccessful = 0;
diff --git a/src/ui/viewmodels/AssetUploadViewModel.hh b/src/ui/viewmodels/AssetUploadViewModel.hh
index 394f427b..ef9fd8a6 100644
--- a/src/ui/viewmodels/AssetUploadViewModel.hh
+++ b/src/ui/viewmodels/AssetUploadViewModel.hh
@@ -23,6 +23,8 @@ public:
     void QueueCodeNotes(ra::data::models::CodeNotesModel& pLeaderboard);
     void QueueCodeNote(ra::data::models::CodeNotesModel& pCodeNotes, ra::ByteAddress nAddress);
 
+    bool HasFailures() const;
+
     void ShowResults() const;
 
 protected:
diff --git a/src/ui/viewmodels/CodeNotesViewModel.cpp b/src/ui/viewmodels/CodeNotesViewModel.cpp
index 88316152..a884afd7 100644
--- a/src/ui/viewmodels/CodeNotesViewModel.cpp
+++ b/src/ui/viewmodels/CodeNotesViewModel.cpp
@@ -376,6 +376,8 @@ void CodeNotesViewModel::PublishSelected()
         vmAssetUpload.QueueCodeNote(*pCodeNotes, nAddress);
 
     vmAssetUpload.ShowModal(*this);
+    if (vNotesToPublish.size() > 1 || vmAssetUpload.HasFailures())
+        vmAssetUpload.ShowResults();
 
     std::vector<ra::data::models::AssetModelBase*> vAssets;
     vAssets.push_back(pCodeNotes);
diff --git a/src/ui/viewmodels/MemoryInspectorViewModel.cpp b/src/ui/viewmodels/MemoryInspectorViewModel.cpp
index df1c7260..de7061d0 100644
--- a/src/ui/viewmodels/MemoryInspectorViewModel.cpp
+++ b/src/ui/viewmodels/MemoryInspectorViewModel.cpp
@@ -326,6 +326,9 @@ void MemoryInspectorViewModel::PublishCurrentAddressNote()
     vmAssetUpload.QueueCodeNote(*pCodeNotes, nAddress);
     vmAssetUpload.ShowModal(*this);
 
+    if (vmAssetUpload.HasFailures())
+        vmAssetUpload.ShowResults();
+
     if (pCodeNotes->IsNoteModified(nAddress))
     {
         // if canceled, re-enable buttons
diff --git a/src/ui/viewmodels/MemoryViewerViewModel.cpp b/src/ui/viewmodels/MemoryViewerViewModel.cpp
index db4e7028..1276090c 100644
--- a/src/ui/viewmodels/MemoryViewerViewModel.cpp
+++ b/src/ui/viewmodels/MemoryViewerViewModel.cpp
@@ -205,11 +205,13 @@ void MemoryViewerViewModel::UpdateColors()
     if (pCodeNotes != nullptr)
     {
         const auto nStopAddress = nFirstAddress + nVisibleLines * 16;
-        pCodeNotes->EnumerateCodeNotes([nFirstAddress, nStopAddress, this](ra::ByteAddress nAddress, unsigned nBytes, const std::wstring&) {
-            if (nAddress + nBytes <= nFirstAddress)
+        pCodeNotes->EnumerateCodeNotes([nFirstAddress, nStopAddress, this](ra::ByteAddress nAddress, unsigned nBytes, const std::wstring& sNote) {
+            if (nAddress + nBytes <= nFirstAddress) // not to viewing window yet
                 return true;
-            if (nAddress >= nStopAddress)
+            if (nAddress >= nStopAddress) // past viewing window
                 return false;
+            if (sNote.empty()) // ignore deleted notes
+                return true;
 
             uint8_t* pOffset = m_pColor;
             Expects(pOffset != nullptr);
@@ -1104,6 +1106,7 @@ void MemoryViewerViewModel::RenderMemory()
     if (nFirstAddress + nVisibleLines * 16 > m_nTotalMemorySize)
         nVisibleLines = (m_nTotalMemorySize - nFirstAddress) / 16;
 
+    const bool bBigEndian = (GetSize() == MemSize::ThirtyTwoBitBigEndian);
     const int nWordSpacing = NibblesPerWord() + 1;
     const int nBytesPerWord = nWordSpacing / 2;
     for (int i = 0; i < nVisibleLines * 16; ++i)
@@ -1122,7 +1125,8 @@ void MemoryViewerViewModel::RenderMemory()
         int nX = (((i & 0x0F) / nBytesPerWord) * nWordSpacing + ADDRESS_COLUMN_WIDTH) * s_szChar.Width;
         if (nBytesPerWord > 1)
         {
-            const int nByteOffset = ((nBytesPerWord - 1) - (i % nBytesPerWord));
+            const int nByteOffset = (bBigEndian) ?
+                (i % nBytesPerWord) : ((nBytesPerWord - 1) - (i % nBytesPerWord));
             nX += (nByteOffset * 2) * s_szChar.Width;
 
             if (nColorUpper == TextColor::Selected && m_bHasFocus && !m_bReadOnly)
diff --git a/src/ui/viewmodels/ProgressTrackerViewModel.cpp b/src/ui/viewmodels/ProgressTrackerViewModel.cpp
index af617ebe..2a611ea1 100644
--- a/src/ui/viewmodels/ProgressTrackerViewModel.cpp
+++ b/src/ui/viewmodels/ProgressTrackerViewModel.cpp
@@ -39,7 +39,7 @@ bool ProgressTrackerViewModel::UpdateRenderImage(double fElapsed)
 
     bool bUpdated = false;
 
-    if (m_pSurface == nullptr)
+    if (m_pSurface == nullptr || m_bRegenerate)
     {
         const auto& pTheme = ra::services::ServiceLocator::Get<ra::ui::OverlayTheme>();
         const auto nShadowOffset = pTheme.ShadowOffset();
@@ -79,6 +79,7 @@ bool ProgressTrackerViewModel::UpdateRenderImage(double fElapsed)
         pSurface->WriteText(nX + 2, nY + 2, nFontSubtitle, pOverlayTheme.ColorTextShadow(), m_sProgress);
         pSurface->WriteText(nX, nY, nFontSubtitle, pOverlayTheme.ColorDetail(), m_sProgress);
 
+        m_bRegenerate = false;
         m_pSurface = std::move(pSurface);
 
         bUpdated = true;
diff --git a/src/ui/viewmodels/ProgressTrackerViewModel.hh b/src/ui/viewmodels/ProgressTrackerViewModel.hh
index f7fc28b3..77cd65ba 100644
--- a/src/ui/viewmodels/ProgressTrackerViewModel.hh
+++ b/src/ui/viewmodels/ProgressTrackerViewModel.hh
@@ -19,7 +19,7 @@ public:
     void SetImage(ra::ui::ImageType nImageType, const std::string& sImageName)
     {
         m_hImage.ChangeReference(nImageType, sImageName);
-        m_pSurface.reset();
+        m_bRegenerate = true;
         SetRenderLocationY(-1); // force redraw when position calculated
     }
 
@@ -54,6 +54,7 @@ public:
 private:
     ra::ui::ImageReference m_hImage;
     std::wstring m_sProgress;
+    bool m_bRegenerate = false;
 
     double m_fAnimationProgress = TOTAL_ANIMATION_TIME;
 
diff --git a/src/ui/viewmodels/TriggerViewModel.cpp b/src/ui/viewmodels/TriggerViewModel.cpp
index e8d00c38..22a1029f 100644
--- a/src/ui/viewmodels/TriggerViewModel.cpp
+++ b/src/ui/viewmodels/TriggerViewModel.cpp
@@ -865,6 +865,9 @@ void TriggerViewModel::UpdateConditions(const GroupViewModel* pGroup)
                 m_bHasHitChain |= (pCondition->type == RC_CONDITION_ADD_HITS || pCondition->type == RC_CONDITION_SUB_HITS);
             }
         }
+
+        if (m_bHasHitChain)
+            UpdateTotalHits();
     }
 
     for (gsl::index nScan = m_vConditions.Count() - 1; nScan >= nIndex; --nScan)
@@ -874,6 +877,47 @@ void TriggerViewModel::UpdateConditions(const GroupViewModel* pGroup)
     m_vConditions.AddNotifyTarget(m_pConditionsMonitor);
 }
 
+void TriggerViewModel::UpdateTotalHits()
+{
+    unsigned int nHits = 0;
+    bool bIsHitsChain = false;
+
+    for (size_t nIndex = 0; nIndex < m_vConditions.Count(); ++nIndex)
+    {
+        auto* vmCondition = m_vConditions.GetItemAt(nIndex);
+        switch (vmCondition->GetType())
+        {
+            case ra::ui::viewmodels::TriggerConditionType::AddHits:
+                bIsHitsChain = true;
+                nHits += vmCondition->GetCurrentHits();
+                break;
+
+            case ra::ui::viewmodels::TriggerConditionType::SubHits:
+                bIsHitsChain = true;
+                nHits -= vmCondition->GetCurrentHits();
+                break;
+
+            case ra::ui::viewmodels::TriggerConditionType::AddAddress:
+            case ra::ui::viewmodels::TriggerConditionType::AddSource:
+            case ra::ui::viewmodels::TriggerConditionType::SubSource:
+            case ra::ui::viewmodels::TriggerConditionType::AndNext:
+            case ra::ui::viewmodels::TriggerConditionType::OrNext:
+            case ra::ui::viewmodels::TriggerConditionType::ResetNextIf:
+                break;
+
+            default:
+                if (bIsHitsChain)
+                {
+                    nHits += vmCondition->GetCurrentHits();
+                    vmCondition->SetTotalHits(nHits);
+                    bIsHitsChain = false;
+                }
+                nHits = 0;
+                break;
+        }
+    }
+}
+
 void TriggerViewModel::AddGroup()
 {
     const int nGroups = gsl::narrow_cast<int>(m_vGroups.Count());
@@ -952,9 +996,6 @@ void TriggerViewModel::DoFrame()
         m_vConditions.RemoveNotifyTarget(m_pConditionsMonitor);
         m_vConditions.BeginUpdate();
 
-        unsigned int nHits = 0;
-        bool bIsHitsChain = false;
-
         gsl::index nConditionIndex = 0;
         rc_condition_t* pCondition = pGroup->m_pConditionSet->conditions;
         for (; pCondition != nullptr; pCondition = pCondition->next)
@@ -968,42 +1009,11 @@ void TriggerViewModel::DoFrame()
             }
 
             vmCondition->SetCurrentHits(pCondition->current_hits);
-
-            if (m_bHasHitChain)
-            {
-                switch (vmCondition->GetType())
-                {
-                    case ra::ui::viewmodels::TriggerConditionType::AddHits:
-                        bIsHitsChain = true;
-                        nHits += vmCondition->GetCurrentHits();
-                        break;
-
-                    case ra::ui::viewmodels::TriggerConditionType::SubHits:
-                        bIsHitsChain = true;
-                        nHits -= vmCondition->GetCurrentHits();
-                        break;
-
-                    case ra::ui::viewmodels::TriggerConditionType::AddAddress:
-                    case ra::ui::viewmodels::TriggerConditionType::AddSource:
-                    case ra::ui::viewmodels::TriggerConditionType::SubSource:
-                    case ra::ui::viewmodels::TriggerConditionType::AndNext:
-                    case ra::ui::viewmodels::TriggerConditionType::OrNext:
-                    case ra::ui::viewmodels::TriggerConditionType::ResetNextIf:
-                        break;
-
-                    default:
-                        if (bIsHitsChain)
-                        {
-                            nHits += vmCondition->GetCurrentHits();
-                            vmCondition->SetTotalHits(nHits);
-                            bIsHitsChain = false;
-                        }
-                        nHits = 0;
-                        break;
-                }
-            }
         }
 
+        if (m_bHasHitChain)
+            UpdateTotalHits();
+
         m_vConditions.EndUpdate();
         m_vConditions.AddNotifyTarget(m_pConditionsMonitor);
     }
diff --git a/src/ui/viewmodels/TriggerViewModel.hh b/src/ui/viewmodels/TriggerViewModel.hh
index e2a0328a..9b00b8bb 100644
--- a/src/ui/viewmodels/TriggerViewModel.hh
+++ b/src/ui/viewmodels/TriggerViewModel.hh
@@ -163,6 +163,7 @@ private:
     void InitializeGroups(const rc_trigger_t& pTrigger);
     void UpdateGroups(const rc_trigger_t& pTrigger);
     void UpdateConditions(const GroupViewModel* pGroup);
+    void UpdateTotalHits();
 
     void DeselectAllConditions();
     void UpdateIndicesAndEnsureSelectionVisible();
diff --git a/src/ui/win32/MemoryInspectorDialog.cpp b/src/ui/win32/MemoryInspectorDialog.cpp
index 38d2566f..9fd09a89 100644
--- a/src/ui/win32/MemoryInspectorDialog.cpp
+++ b/src/ui/win32/MemoryInspectorDialog.cpp
@@ -144,6 +144,7 @@ MemoryInspectorDialog::MemoryInspectorDialog(MemoryInspectorViewModel& vmMemoryI
       m_bindViewer8Bit(vmMemoryInspector.Viewer()),
       m_bindViewer16Bit(vmMemoryInspector.Viewer()),
       m_bindViewer32Bit(vmMemoryInspector.Viewer()),
+      m_bindViewer32BitBE(vmMemoryInspector.Viewer()),
       m_bindViewer(vmMemoryInspector.Viewer())
 {
     m_bindWindow.SetInitialPosition(RelativePosition::After, RelativePosition::Near, "Memory Inspector");
@@ -241,6 +242,7 @@ MemoryInspectorDialog::MemoryInspectorDialog(MemoryInspectorViewModel& vmMemoryI
     m_bindViewer8Bit.BindCheck(MemoryViewerViewModel::SizeProperty, ra::etoi(MemSize::EightBit));
     m_bindViewer16Bit.BindCheck(MemoryViewerViewModel::SizeProperty, ra::etoi(MemSize::SixteenBit));
     m_bindViewer32Bit.BindCheck(MemoryViewerViewModel::SizeProperty, ra::etoi(MemSize::ThirtyTwoBit));
+    m_bindViewer32BitBE.BindCheck(MemoryViewerViewModel::SizeProperty, ra::etoi(MemSize::ThirtyTwoBitBigEndian));
     m_bindWindow.BindLabel(IDC_RA_MEMBITS, MemoryInspectorViewModel::CurrentAddressBitsProperty);
     m_bindWindow.BindVisible(IDC_RA_MEMBITS_TITLE, MemoryInspectorViewModel::CurrentBitsVisibleProperty);
     m_bindWindow.BindVisible(IDC_RA_MEMBITS, MemoryInspectorViewModel::CurrentBitsVisibleProperty);
@@ -318,6 +320,7 @@ BOOL MemoryInspectorDialog::OnInitDialog()
     m_bindViewer8Bit.SetControl(*this, IDC_RA_MEMVIEW_8BIT);
     m_bindViewer16Bit.SetControl(*this, IDC_RA_MEMVIEW_16BIT);
     m_bindViewer32Bit.SetControl(*this, IDC_RA_MEMVIEW_32BIT);
+    m_bindViewer32BitBE.SetControl(*this, IDC_RA_MEMVIEW_32BITBE);
     m_bindViewer.SetControl(*this, IDC_RA_MEMVIEWER);
 
     SetWindowFont(GetDlgItem(GetHWND(), IDC_RA_MEMBITS), GetStockObject(SYSTEM_FIXED_FONT), TRUE);
diff --git a/src/ui/win32/MemoryInspectorDialog.hh b/src/ui/win32/MemoryInspectorDialog.hh
index 15e0d9ea..fdd7c759 100644
--- a/src/ui/win32/MemoryInspectorDialog.hh
+++ b/src/ui/win32/MemoryInspectorDialog.hh
@@ -78,6 +78,7 @@ private:
     bindings::RadioButtonBinding m_bindViewer8Bit;
     bindings::RadioButtonBinding m_bindViewer16Bit;
     bindings::RadioButtonBinding m_bindViewer32Bit;
+    bindings::RadioButtonBinding m_bindViewer32BitBE;
     bindings::MemoryViewerControlBinding m_bindViewer;
 };
 


Test patch:
diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj
index cd3e9d5c..cac29901 100644
--- a/tests/RA_Integration.Tests.vcxproj
+++ b/tests/RA_Integration.Tests.vcxproj
@@ -375,6 +375,7 @@
     <ClCompile Include="data\ModelProperty_Tests.cpp" />
     <ClCompile Include="data\models\AchievementModel_Tests.cpp" />
     <ClCompile Include="data\models\AssetModelBase_Tests.cpp" />
+    <ClCompile Include="data\models\CapturedTriggerHits_Tests.cpp" />
     <ClCompile Include="data\models\CodeNotesModel_Tests.cpp" />
     <ClCompile Include="data\models\LeaderboardModel_Tests.cpp" />
     <ClCompile Include="data\models\LocalBadgesModel_Tests.cpp" />
diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters
index c2990fd4..9e1ec7ae 100644
--- a/tests/RA_Integration.Tests.vcxproj.filters
+++ b/tests/RA_Integration.Tests.vcxproj.filters
@@ -459,6 +459,9 @@
     <ClCompile Include="..\src\ui\viewmodels\ProgressTrackerViewModel.cpp">
       <Filter>Code</Filter>
     </ClCompile>
+    <ClCompile Include="data\models\CapturedTriggerHits_Tests.cpp">
+      <Filter>Tests\Data\Models</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <None Include="base.props" />
diff --git a/tests/data/models/CapturedTriggerHits_Tests.cpp b/tests/data/models/CapturedTriggerHits_Tests.cpp
new file mode 100644
index 00000000..dd82d4f4
--- /dev/null
+++ b/tests/data/models/CapturedTriggerHits_Tests.cpp
@@ -0,0 +1,224 @@
+#include "CppUnitTest.h"
+
+#include "data\models\CapturedTriggerHits.hh"
+
+#include "tests\mocks\MockConsoleContext.hh"
+#include "tests\mocks\MockEmulatorContext.hh"
+
+using namespace Microsoft::VisualStudio::CppUnitTestFramework;
+
+namespace ra {
+namespace data {
+namespace models {
+namespace tests {
+
+TEST_CLASS(CapturedTriggerHits_Tests)
+{
+public:
+    TEST_METHOD(TestRestoreTrigger)
+    {
+        const std::string trigger_definition = "0xH1234=1_0xH2345=2";
+        char buffer[512];
+        rc_trigger_t* trigger = rc_parse_trigger(buffer, trigger_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        trigger->requirement->conditions->current_hits = 6U;
+        trigger->requirement->conditions->next->current_hits = 7U;
+        trigger->has_hits = 1;
+
+        hits.Capture(trigger, trigger_definition);
+
+        trigger->requirement->conditions->current_hits = 8U;
+        trigger->requirement->conditions->next->current_hits = 15U;
+
+        Assert::IsTrue(hits.Restore(trigger, trigger_definition));
+
+        Assert::AreEqual(6U, trigger->requirement->conditions->current_hits);
+        Assert::AreEqual(7U, trigger->requirement->conditions->next->current_hits);
+        Assert::AreEqual({1}, trigger->has_hits);
+    }
+
+    TEST_METHOD(TestRestoreTriggerAlts)
+    {
+        const std::string trigger_definition = "0xH1234=1S0xH2345=2S0xH3456=3";
+        char buffer[512];
+        rc_trigger_t* trigger = rc_parse_trigger(buffer, trigger_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        trigger->requirement->conditions->current_hits = 6U;
+        trigger->alternative->conditions->current_hits = 7U;
+        trigger->alternative->next->conditions->current_hits = 8U;
+        trigger->has_hits = 1;
+
+        hits.Capture(trigger, trigger_definition);
+
+        trigger->requirement->conditions->current_hits = 8U;
+        trigger->alternative->conditions->current_hits = 10U;
+        trigger->alternative->next->conditions->current_hits = 15U;
+
+        Assert::IsTrue(hits.Restore(trigger, trigger_definition));
+
+        Assert::AreEqual(6U, trigger->requirement->conditions->current_hits);
+        Assert::AreEqual(7U, trigger->alternative->conditions->current_hits);
+        Assert::AreEqual(8U, trigger->alternative->next->conditions->current_hits);
+        Assert::AreEqual({1}, trigger->has_hits);
+    }
+
+    TEST_METHOD(TestRestoreTriggerUncaptured)
+    {
+        const std::string trigger_definition = "0xH1234=1_0xH2345=2";
+        char buffer[512];
+        rc_trigger_t* trigger = rc_parse_trigger(buffer, trigger_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        trigger->requirement->conditions->current_hits = 6U;
+        trigger->requirement->conditions->next->current_hits = 7U;
+        trigger->has_hits = 1;
+
+        Assert::IsFalse(hits.Restore(trigger, trigger_definition));
+
+        Assert::AreEqual(6U, trigger->requirement->conditions->current_hits);
+        Assert::AreEqual(7U, trigger->requirement->conditions->next->current_hits);
+        Assert::AreEqual({1}, trigger->has_hits);
+    }
+
+    TEST_METHOD(TestRestoreTriggerAltered)
+    {
+        const std::string trigger_definition = "0xH1234=1_0xH2345=2";
+        const std::string trigger_definition2 = "0xH1234=1_0xH2345=3";
+        char buffer[512];
+        rc_trigger_t* trigger = rc_parse_trigger(buffer, trigger_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        trigger->requirement->conditions->current_hits = 6U;
+        trigger->requirement->conditions->next->current_hits = 7U;
+        trigger->has_hits = 1;
+
+        hits.Capture(trigger, trigger_definition);
+
+        trigger->requirement->conditions->current_hits = 8U;
+        trigger->requirement->conditions->next->current_hits = 10U;
+
+        Assert::IsFalse(hits.Restore(trigger, trigger_definition2));
+
+        Assert::AreEqual(8U, trigger->requirement->conditions->current_hits);
+        Assert::AreEqual(10U, trigger->requirement->conditions->next->current_hits);
+        Assert::AreEqual({1}, trigger->has_hits);
+    }
+
+    TEST_METHOD(TestRestoreTriggerZeroes)
+    {
+        const std::string trigger_definition = "0xH1234=1_0xH2345=2";
+        char buffer[512];
+        rc_trigger_t* trigger = rc_parse_trigger(buffer, trigger_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        hits.Capture(trigger, trigger_definition);
+
+        trigger->requirement->conditions->current_hits = 8U;
+        trigger->requirement->conditions->next->current_hits = 15U;
+
+        Assert::IsTrue(hits.Restore(trigger, trigger_definition));
+
+        Assert::AreEqual(0U, trigger->requirement->conditions->current_hits);
+        Assert::AreEqual(0U, trigger->requirement->conditions->next->current_hits);
+        Assert::AreEqual({0}, trigger->has_hits);
+    }
+
+    TEST_METHOD(TestRestoreValue)
+    {
+        const std::string value_definition = "M:0xH1234=1";
+        char buffer[512];
+        rc_value_t* value = rc_parse_value(buffer, value_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        value->conditions->conditions->current_hits = 6U;
+
+        hits.Capture(value, value_definition);
+
+        value->conditions->conditions->current_hits = 8U;
+
+        Assert::IsTrue(hits.Restore(value, value_definition));
+
+        Assert::AreEqual(6U, value->conditions->conditions->current_hits);
+    }
+
+    TEST_METHOD(TestRestoreValueAlts)
+    {
+        const std::string value_definition = "M:0xH1234=1$M:0xH2345=2$M:0xH3456=3";
+        char buffer[512];
+        rc_value_t* value = rc_parse_value(buffer, value_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        value->conditions->conditions->current_hits = 6U;
+        value->conditions->next->conditions->current_hits = 7U;
+        value->conditions->next->next->conditions->current_hits = 8U;
+
+        hits.Capture(value, value_definition);
+
+        value->conditions->conditions->current_hits = 8U;
+        value->conditions->next->conditions->current_hits = 10U;
+        value->conditions->next->next->conditions->current_hits = 15U;
+
+        Assert::IsTrue(hits.Restore(value, value_definition));
+
+        Assert::AreEqual(6U, value->conditions->conditions->current_hits);
+        Assert::AreEqual(7U, value->conditions->next->conditions->current_hits);
+        Assert::AreEqual(8U, value->conditions->next->next->conditions->current_hits);
+    }
+
+    TEST_METHOD(TestRestoreValueUncaptured)
+    {
+        const std::string value_definition = "M:0xH1234=1";
+        char buffer[512];
+        rc_value_t* value = rc_parse_value(buffer, value_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        value->conditions->conditions->current_hits = 6U;
+
+        Assert::IsFalse(hits.Restore(value, value_definition));
+
+        Assert::AreEqual(6U, value->conditions->conditions->current_hits);
+    }
+
+    TEST_METHOD(TestRestoreValueAltered)
+    {
+        const std::string value_definition = "M:0xH1234=1";
+        const std::string value_definition2 = "M:0xH1234=2";
+        char buffer[512];
+        rc_value_t* value = rc_parse_value(buffer, value_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        value->conditions->conditions->current_hits = 6U;
+
+        hits.Capture(value, value_definition);
+
+        value->conditions->conditions->current_hits = 8U;
+
+        Assert::IsFalse(hits.Restore(value, value_definition2));
+
+        Assert::AreEqual(8U, value->conditions->conditions->current_hits);
+    }
+
+    TEST_METHOD(TestRestoreValueZeroes)
+    {
+        const std::string value_definition = "M:0xH1234=1";
+        char buffer[512];
+        rc_value_t* value = rc_parse_value(buffer, value_definition.c_str(), NULL, 0);
+        CapturedTriggerHits hits;
+
+        hits.Capture(value, value_definition);
+
+        value->conditions->conditions->current_hits = 8U;
+
+        Assert::IsTrue(hits.Restore(value, value_definition));
+
+        Assert::AreEqual(0U, value->conditions->conditions->current_hits);
+    }
+
+};
+
+} // namespace tests
+} // namespace models
+} // namespace data
+} // namespace ra
diff --git a/tests/data/models/CodeNotesModel_Tests.cpp b/tests/data/models/CodeNotesModel_Tests.cpp
index bf3eca46..0d41b8fe 100644
--- a/tests/data/models/CodeNotesModel_Tests.cpp
+++ b/tests/data/models/CodeNotesModel_Tests.cpp
@@ -1059,7 +1059,7 @@ public:
 
         Assert::IsTrue(notes.Deserialize(pTokenizer));
         Assert::AreEqual(AssetChanges::Unpublished, notes.GetChanges());
-        notes.AssertNote(0x1234, std::wstring(L"16-bit pointer\n+2:\t\"a\"\n+4:\tb\n"));
+        notes.AssertNote(0x1234, std::wstring(L"16-bit pointer\r\n+2:\t\"a\"\r\n+4:\tb\r\n"));
     }
 
     TEST_METHOD(TestDeserializeUnchanged)
diff --git a/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp b/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
index 39e189e9..7facf293 100644
--- a/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/CodeNotesViewModel_Tests.cpp
@@ -897,19 +897,31 @@ public:
         Assert::IsTrue(notes.CanPublishCurrentAddressNote());
         Assert::IsTrue(notes.CanRevertCurrentAddressNote());
 
-        bool bWindowSeen = false;
-        notes.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWindowSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
+        int nWindowsSeen = 0;
+        notes.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&nWindowsSeen, &notes](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox)
         {
-            Assert::AreEqual(std::wstring(L"Publish 2 notes?"), vmMessageBox.GetHeader());
-            Assert::AreEqual(std::wstring(L"The selected modified notes will be uploaded to the server."), vmMessageBox.GetMessage());
-            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
+            if (nWindowsSeen == 0)
+            {
+                Assert::AreEqual(std::wstring(L"Publish 2 notes?"), vmMessageBox.GetHeader());
+                Assert::AreEqual(std::wstring(L"The selected modified notes will be uploaded to the server."), vmMessageBox.GetMessage());
+                Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::YesNo, vmMessageBox.GetButtons());
+
+                nWindowsSeen++;
+                return ra::ui::DialogResult::Yes;
+            }
+            else
+            {
+                Assert::AreEqual(std::wstring(L"Publish succeeded."), vmMessageBox.GetHeader());
+                Assert::AreEqual(std::wstring(L"2 items successfully uploaded."), vmMessageBox.GetMessage());
+                Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::OK, vmMessageBox.GetButtons());
 
-            bWindowSeen = true;
-            return ra::ui::DialogResult::Yes;
+                nWindowsSeen++;
+                return ra::ui::DialogResult::OK;
+            }
         });
 
         notes.PublishSelected();
-        Assert::IsTrue(bWindowSeen);
+        Assert::AreEqual(nWindowsSeen, 2);
 
         AssertRow(notes, 4, 0x0016, L"0x0016", L"Changed 20");
         AssertRow(notes, 13, 0x0040, L"0x0040", L"Changed 64");
diff --git a/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp b/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp
index fdf89d6c..c49d19d9 100644
--- a/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/MemoryInspectorViewModel_Tests.cpp
@@ -110,6 +110,34 @@ private:
             Assert::AreEqual(sNote, GetCurrentAddressNote());
         }
 
+        void PreparePublishFailure(ra::ByteAddress nAddress, std::wstring sNote)
+        {
+            mockServer.HandleRequest<ra::api::UpdateCodeNote>(
+                [this](const ra::api::UpdateCodeNote::Request& pRequest, ra::api::UpdateCodeNote::Response& pResponse) {
+                    m_nPublishedAddress = pRequest.Address;
+
+                    pResponse.Result = ra::api::ApiResult::Failed;
+                    return true;
+                });
+
+            mockServer.HandleRequest<ra::api::DeleteCodeNote>(
+                [this](const ra::api::DeleteCodeNote::Request& pRequest, ra::api::DeleteCodeNote::Response& pResponse) {
+                    m_nPublishedAddress = pRequest.Address;
+
+                    pResponse.Result = ra::api::ApiResult::Failed;
+                    return true;
+                });
+
+            mockThreadPool.SetSynchronous(true);
+
+            SetValue(CanModifyNotesProperty, true);
+
+            mockGameContext.Assets().FindCodeNotes()->SetCodeNote(nAddress, sNote);
+            SetCurrentAddress(nAddress);
+
+            Assert::AreEqual(sNote, GetCurrentAddressNote());
+        }
+
         ra::ByteAddress GetPublishedAddress() const noexcept { return m_nPublishedAddress; }
 
     private:
@@ -326,6 +354,9 @@ public:
         inspector.Viewer().SetSize(MemSize::ThirtyTwoBit);
         Assert::IsFalse(inspector.CurrentBitsVisible());
 
+        inspector.Viewer().SetSize(MemSize::ThirtyTwoBitBigEndian);
+        Assert::IsFalse(inspector.CurrentBitsVisible());
+
         inspector.Viewer().SetSize(MemSize::EightBit);
         Assert::IsTrue(inspector.CurrentBitsVisible());
     }
@@ -400,9 +431,17 @@ public:
         MemoryInspectorViewModelHarness inspector;
         inspector.PreparePublish(0x12, L"Test");
 
+        bool bWindowSeen = false;
+        inspector.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWindowSeen](ra::ui::viewmodels::MessageBoxViewModel&)
+        {
+            bWindowSeen = true;
+            return ra::ui::DialogResult::No;
+        });
+
         Assert::IsTrue(inspector.CanPublishCurrentAddressNote());
         inspector.PublishCurrentAddressNote();
 
+        Assert::IsFalse(bWindowSeen);
         Assert::AreEqual(0x12U, inspector.GetPublishedAddress());
         Assert::IsFalse(inspector.CanPublishCurrentAddressNote());
         Assert::IsFalse(inspector.CanRevertCurrentAddressNote());
@@ -505,6 +544,29 @@ public:
         Assert::IsTrue(inspector.mockGameContext.Assets().FindCodeNotes()->IsNoteModified(0x12));
     }
 
+    TEST_METHOD(TestPublishCurrentAddressNoteFailed)
+    {
+        MemoryInspectorViewModelHarness inspector;
+        inspector.PreparePublishFailure(0x12, L"Test");
+
+        bool bWindowSeen = false;
+        inspector.mockDesktop.ExpectWindow<ra::ui::viewmodels::MessageBoxViewModel>([&bWindowSeen](ra::ui::viewmodels::MessageBoxViewModel& vmMessageBox) {
+            Assert::AreEqual(std::wstring(L"Publish failed."), vmMessageBox.GetHeader());
+            Assert::AreEqual(std::wstring(L"0 items successfully uploaded.\n\n1 items failed:\n* Code Notes: "), vmMessageBox.GetMessage());
+            Assert::AreEqual(ra::ui::viewmodels::MessageBoxViewModel::Buttons::OK, vmMessageBox.GetButtons());
+
+            bWindowSeen = true;
+            return ra::ui::DialogResult::OK;
+        });
+
+        Assert::IsTrue(inspector.CanPublishCurrentAddressNote());
+        inspector.PublishCurrentAddressNote();
+
+        Assert::IsTrue(bWindowSeen);
+        Assert::IsTrue(inspector.CanPublishCurrentAddressNote());
+        Assert::IsTrue(inspector.CanRevertCurrentAddressNote());
+    }
+
     TEST_METHOD(TestActiveGameChanged)
     {
         MemoryInspectorViewModelHarness inspector;
@@ -647,6 +709,18 @@ public:
         Assert::AreEqual(MemSize::EightBit, pBookmarks.GetItemAt(0)->GetSize());
         Assert::AreEqual({ 5U }, pBookmarks.GetItemAt(1)->GetAddress());
         Assert::AreEqual(MemSize::ThirtyTwoBit, pBookmarks.GetItemAt(1)->GetSize());
+
+        inspector.Viewer().SetSize(MemSize::ThirtyTwoBitBigEndian);
+        inspector.SetCurrentAddress(12U);
+        inspector.BookmarkCurrentAddress();
+
+        Assert::AreEqual({3U}, pBookmarks.Count());
+        Assert::AreEqual({2U}, pBookmarks.GetItemAt(0)->GetAddress());
+        Assert::AreEqual(MemSize::EightBit, pBookmarks.GetItemAt(0)->GetSize());
+        Assert::AreEqual({5U}, pBookmarks.GetItemAt(1)->GetAddress());
+        Assert::AreEqual(MemSize::ThirtyTwoBit, pBookmarks.GetItemAt(1)->GetSize());
+        Assert::AreEqual({12U}, pBookmarks.GetItemAt(2)->GetAddress());
+        Assert::AreEqual(MemSize::ThirtyTwoBitBigEndian, pBookmarks.GetItemAt(2)->GetSize());
     }
 };
 
diff --git a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
index 1960eca8..f2849e38 100644
--- a/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/TriggerViewModel_Tests.cpp
@@ -1419,6 +1419,28 @@ public:
         Assert::AreEqual(sTooltip, vmTrigger.GetHitChainTooltip(5)); // non hit modifier
         Assert::AreEqual(sTooltip, vmTrigger.GetHitChainTooltip(6)); // end of hits-chain
     }
+
+    TEST_METHOD(TestAltHitChain)
+    {
+        TriggerViewModelHarness vmTrigger;
+        Parse(vmTrigger, "1=1SC:0xH1234=1_0=1.10.SC:0xH2345=1_0=1.10.");
+        Assert::AreEqual({3U}, vmTrigger.Groups().Count());
+
+        vmTrigger.Groups().GetItemAt(1)->m_pConditionSet->conditions->current_hits = 2;
+        vmTrigger.Groups().GetItemAt(2)->m_pConditionSet->conditions->current_hits = 3;
+
+        vmTrigger.SetSelectedGroupIndex(2);
+        Assert::AreEqual(3U, vmTrigger.Conditions().GetItemAt(0)->GetCurrentHits());
+        Assert::AreEqual(0U, vmTrigger.Conditions().GetItemAt(0)->GetTotalHits());
+        Assert::AreEqual(0U, vmTrigger.Conditions().GetItemAt(1)->GetCurrentHits());
+        Assert::AreEqual(3U, vmTrigger.Conditions().GetItemAt(1)->GetTotalHits());
+
+        vmTrigger.SetSelectedGroupIndex(1);
+        Assert::AreEqual(2U, vmTrigger.Conditions().GetItemAt(0)->GetCurrentHits());
+        Assert::AreEqual(0U, vmTrigger.Conditions().GetItemAt(0)->GetTotalHits());
+        Assert::AreEqual(0U, vmTrigger.Conditions().GetItemAt(1)->GetCurrentHits());
+        Assert::AreEqual(2U, vmTrigger.Conditions().GetItemAt(1)->GetTotalHits());
+    }
 };
 
 } // namespace tests