Repository: RetroAchievements/RAIntegration
Pull request #905 (2022-08-26 15:56:21)

switch points field to dropdown
Matches changes made in https://github.com/RetroAchievements/RAWeb/pull/1054#issuecomment-1207488057

![image](https://user-images.githubusercontent.com/32680403/186944706-fdeff187-dadb-4cfb-930d-939a302ffd2c.png)

If a value outside of the allowed range is provided (legacy support), the achievement will be flagged:

![image](https://user-images.githubusercontent.com/32680403/186944853-b7178baf-c411-4a0d-812d-271e408153d4.png)




Patch:
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 64849264..706e2fee 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -124,7 +124,7 @@ BEGIN
     LTEXT           "Description:",IDC_STATIC,4,23,42,8
     EDITTEXT        IDC_RA_DESCRIPTION,50,21,333,13,ES_AUTOHSCROLL
     LTEXT           "Points:",IDC_RA_LBL_POINTS,4,40,23,8
-    EDITTEXT        IDC_RA_POINTS,50,38,42,13,ES_AUTOHSCROLL,WS_EX_RIGHT
+    COMBOBOX        IDC_RA_POINTS,50,38,42,13,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     LTEXT           "Format:",IDC_RA_LBL_FORMAT,4,40,23,8
     COMBOBOX        IDC_RA_FORMAT,50,38,42,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     LTEXT           "Measured:",IDC_RA_LBL_MEASURED,100,40,35,8
diff --git a/src/data/models/AchievementModel.cpp b/src/data/models/AchievementModel.cpp
index 644029a7..c7d29937 100644
--- a/src/data/models/AchievementModel.cpp
+++ b/src/data/models/AchievementModel.cpp
@@ -20,6 +20,8 @@ const BoolModelProperty AchievementModel::PauseOnTriggerProperty("AchievementMod
 const IntModelProperty AchievementModel::TriggerProperty("AchievementModel", "Trigger", 0);
 const StringModelProperty AchievementModel::UnlockRichPresenceProperty("AchievementModel", "UnlockRichPresence", L"");
 
+const std::array<int, 10> AchievementModel::s_vValidPoints = {0, 1, 2, 3, 4, 5, 10, 25, 50, 100};
+
 AchievementModel::AchievementModel() noexcept
 {
     GSL_SUPPRESS_F6 SetValue(TypeProperty, ra::etoi(AssetType::Achievement));
@@ -119,6 +121,13 @@ void AchievementModel::CommitTransaction()
 
 bool AchievementModel::ValidateAsset(std::wstring& sError)
 {
+    const auto pIter = std::find(s_vValidPoints.begin(), s_vValidPoints.end(), GetPoints());
+    if (pIter == s_vValidPoints.end())
+    {
+        sError = L"Invalid point value";
+        return false;
+    }
+
     const auto& sTrigger = GetAssetDefinition(m_pTrigger);
     return TriggerValidation::Validate(sTrigger, sError, AssetType::Achievement);
 }
diff --git a/src/data/models/AchievementModel.hh b/src/data/models/AchievementModel.hh
index 1be05913..fe90954d 100644
--- a/src/data/models/AchievementModel.hh
+++ b/src/data/models/AchievementModel.hh
@@ -130,6 +130,11 @@ public:
     const CapturedTriggerHits& GetCapturedHits() const noexcept { return m_pCapturedTriggerHits; }
     void ResetCapturedHits() noexcept { m_pCapturedTriggerHits.Reset(); }
 
+    static const std::array<int, 10>& ValidPoints() noexcept
+    {
+        return s_vValidPoints;
+    }
+
 protected:
     void OnValueChanged(const IntModelProperty::ChangeArgs& args) override;
     void OnValueChanged(const StringModelProperty::ChangeArgs& args) override;
@@ -139,6 +144,8 @@ protected:
     bool ValidateAsset(std::wstring& sErrorMessage) override;
 
 private:
+    static const std::array<int, 10> s_vValidPoints;
+
     void HandleStateChanged(AssetState nOldState, AssetState nNewState);
 
     AssetDefinition m_pTrigger;
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index cff30aba..d0934079 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -625,6 +625,80 @@ void AssetEditorDialog::ErrorIconBinding::UpdateImage()
     }
 }
 
+AssetEditorDialog::PointBinding::PointBinding(ViewModelBase& vmViewModel) noexcept
+    : ra::ui::win32::bindings::ComboBoxBinding(vmViewModel)
+{
+    static ra::ui::viewmodels::LookupItemViewModelCollection s_vDummy;
+    m_pViewModelCollection = &s_vDummy;
+}
+
+void AssetEditorDialog::PointBinding::PopulateComboBox()
+{
+    for (const int nPoints : AchievementModel::ValidPoints())
+    {
+        ComboBox_AddString(m_hWnd, std::to_wstring(nPoints).c_str());
+    }
+}
+
+void AssetEditorDialog::PointBinding::UpdateSelectedItem()
+{
+    if (!m_hWnd)
+        return;
+
+    const auto nPoints = GetValue(*m_pSelectedIdProperty);
+
+    if (m_nCustomPointsIndex != -1)
+    {
+        if (nPoints == m_nCustomPoints)
+            return;
+
+        ComboBox_DeleteString(m_hWnd, m_nCustomPointsIndex);
+        m_nCustomPointsIndex = -1;
+        m_nCustomPoints = -1;
+    }
+
+    for (size_t nIndex = 0; nIndex < AchievementModel::ValidPoints().size(); ++nIndex)
+    {
+        if (AchievementModel::ValidPoints().at(nIndex) == nPoints)
+        {
+            ComboBox_SetCurSel(m_hWnd, nIndex);
+            return;
+        }
+    }
+
+    for (size_t nIndex = AchievementModel::ValidPoints().size(); nIndex > 0; --nIndex)
+    {
+        if (AchievementModel::ValidPoints().at(nIndex - 1) < nPoints)
+        {
+            m_nCustomPoints = nPoints;
+            ComboBox_InsertString(m_hWnd, nIndex, std::to_wstring(nPoints).c_str());
+
+            m_nCustomPointsIndex = gsl::narrow_cast<int>(nIndex);
+            ComboBox_SetCurSel(m_hWnd, nIndex);
+            break;
+        }
+    }
+}
+
+void AssetEditorDialog::PointBinding::OnValueChanged()
+{
+    const auto nIndex = ComboBox_GetCurSel(m_hWnd);
+    if (m_nCustomPointsIndex == -1 || nIndex < m_nCustomPointsIndex)
+    {
+        const auto nPoints = AchievementModel::ValidPoints().at(nIndex);
+        SetValue(*m_pSelectedIdProperty, nPoints);
+    }
+    else if (nIndex > m_nCustomPointsIndex)
+    {
+        const auto nPoints = AchievementModel::ValidPoints().at(gsl::narrow_cast<size_t>(nIndex) - 1);
+        SetValue(*m_pSelectedIdProperty, nPoints);
+    }
+    else
+    {
+        SetValue(*m_pSelectedIdProperty, m_nCustomPoints);
+    }
+}
+
 AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
     : DialogBase(vmAssetEditor),
     m_bindID(vmAssetEditor),
@@ -683,7 +757,7 @@ AssetEditorDialog::AssetEditorDialog(AssetEditorViewModel& vmAssetEditor)
     m_bindBadge.BindText(AssetEditorViewModel::BadgeProperty);
     m_bindBadge.SetWrapAround(true);
     m_bindBadgeImage.BindImage(AssetEditorViewModel::BadgeProperty, ra::ui::ImageType::Badge);
-    m_bindPoints.BindValue(AssetEditorViewModel::PointsProperty);
+    m_bindPoints.BindSelectedItem(AssetEditorViewModel::PointsProperty);
     m_bindWindow.BindVisible(IDC_RA_LBL_POINTS, AssetEditorViewModel::IsAchievementProperty);
     m_bindWindow.BindVisible(IDC_RA_POINTS, AssetEditorViewModel::IsAchievementProperty);
     m_bindWindow.BindVisible(IDC_RA_CHK_AS_PERCENT, AssetEditorViewModel::IsAchievementProperty);
diff --git a/src/ui/win32/AssetEditorDialog.hh b/src/ui/win32/AssetEditorDialog.hh
index 8867886a..76e1d642 100644
--- a/src/ui/win32/AssetEditorDialog.hh
+++ b/src/ui/win32/AssetEditorDialog.hh
@@ -115,12 +115,33 @@ private:
         HICON m_hWarningIcon = nullptr;
     };
 
+    class PointBinding : public ra::ui::win32::bindings::ComboBoxBinding
+    {
+    public:
+        PointBinding(ViewModelBase& vmViewModel) noexcept;
+        ~PointBinding() = default;
+        PointBinding(const PointBinding&) noexcept = delete;
+        PointBinding& operator=(const PointBinding&) noexcept = delete;
+        PointBinding(PointBinding&&) noexcept = delete;
+        PointBinding& operator=(PointBinding&&) noexcept = delete;
+
+        void OnValueChanged() override;
+
+    protected:
+        void PopulateComboBox() override;
+        void UpdateSelectedItem() override;
+
+    private:
+        int m_nCustomPoints = -1;
+        int m_nCustomPointsIndex = -1;
+    };
+
     ra::ui::win32::bindings::NumericTextBoxBinding m_bindID;
     ra::ui::win32::bindings::TextBoxBinding m_bindName;
     ra::ui::win32::bindings::TextBoxBinding m_bindDescription;
     BadgeNameBinding m_bindBadge;
     ra::ui::win32::bindings::ImageBinding m_bindBadgeImage;
-    ra::ui::win32::bindings::NumericTextBoxBinding m_bindPoints;
+    PointBinding m_bindPoints;
     ra::ui::win32::bindings::ComboBoxBinding m_bindFormats;
     ra::ui::win32::bindings::GridBinding m_bindLeaderboardParts;
     ra::ui::win32::bindings::CheckBoxBinding m_bindLowerIsBetter;
diff --git a/src/ui/win32/bindings/ComboBoxBinding.hh b/src/ui/win32/bindings/ComboBoxBinding.hh
index 926ef414..ee060fab 100644
--- a/src/ui/win32/bindings/ComboBoxBinding.hh
+++ b/src/ui/win32/bindings/ComboBoxBinding.hh
@@ -131,8 +131,7 @@ protected:
         ComboBox_DeleteString(m_hWnd, nIndex);
     }
 
-private:
-    void PopulateComboBox()
+    virtual void PopulateComboBox()
     {
         const auto nCount = ra::to_signed(m_pViewModelCollection->Count());
         for (gsl::index nIndex = 0; nIndex < nCount; ++nIndex)
@@ -143,7 +142,7 @@ private:
         }
     }
 
-    void UpdateSelectedItem()
+    virtual void UpdateSelectedItem()
     {
         if (m_pViewModelCollection)
         {
@@ -159,8 +158,10 @@ private:
             ComboBox_SetCurSel(m_hWnd, m_nSelectedIndex);
     }
 
-    const IntModelProperty* m_pSelectedIdProperty = nullptr;
     const ViewModelCollectionBase* m_pViewModelCollection = nullptr;
+    const IntModelProperty* m_pSelectedIdProperty = nullptr;
+
+private:
     ViewModelCollectionBase* m_pMutableViewModelCollection = nullptr;
     const IntModelProperty* m_pItemIdProperty = nullptr;
     const StringModelProperty* m_pItemTextProperty = nullptr;


Test patch:
diff --git a/tests/data/models/AchievementModel_Tests.cpp b/tests/data/models/AchievementModel_Tests.cpp
index e0e9339a..3a8fddce 100644
--- a/tests/data/models/AchievementModel_Tests.cpp
+++ b/tests/data/models/AchievementModel_Tests.cpp
@@ -285,6 +285,38 @@ public:
         achievement.UpdateLocalCheckpoint();
         Assert::AreEqual(std::wstring(), achievement.GetValidationError());
     }
+
+    TEST_METHOD(TestValidatePoints)
+    {
+        AchievementModelHarness achievement;
+        achievement.mockGameContext.SetGameId(22);
+        achievement.AddLocalBadgesModel();
+        achievement.SetBadge(L"12345.png");
+        achievement.CreateServerCheckpoint();
+        achievement.CreateLocalCheckpoint();
+
+        Assert::IsTrue(achievement.Validate());
+        Assert::AreEqual(std::wstring(), achievement.GetValidationError());
+
+        const std::array<int, 10> vValidPoints = {0, 1, 2, 3, 4, 5, 10, 25, 50, 100};
+
+        for (int i = -100; i <= 500; i++)
+        {
+            achievement.SetPoints(i);
+
+            const auto pIter = std::find(vValidPoints.begin(), vValidPoints.end(), i);
+            if (pIter == vValidPoints.end())
+            {
+                Assert::IsFalse(achievement.Validate());
+                Assert::AreEqual(std::wstring(L"Invalid point value"), achievement.GetValidationError());
+            }
+            else
+            {
+                Assert::IsTrue(achievement.Validate());
+                Assert::AreEqual(std::wstring(), achievement.GetValidationError());
+            }
+        }
+    }
 };