diff --git a/loader/include/Geode/loader/SettingV3.hpp b/loader/include/Geode/loader/SettingV3.hpp index 6021450fe..fa9e1c7a9 100644 --- a/loader/include/Geode/loader/SettingV3.hpp +++ b/loader/include/Geode/loader/SettingV3.hpp @@ -9,8 +9,9 @@ // this unfortunately has to be included because of C++ templates #include "../utils/JsonValidation.hpp" -// todo in v4: these can be removed as well as the friend decl in LegacyCustomSettingV3 +// todo in v4: this can be removed as well as the friend decl in LegacyCustomSettingV3 class LegacyCustomSettingToV3Node; +class ModSettingsPopup; namespace geode { class ModSettingsManager; @@ -59,6 +60,11 @@ namespace geode { * Get the "enable-if" scheme for this setting */ std::optional getEnableIf() const; + /** + * Check if this setting should be enabled based on the "enable-if" scheme + */ + bool shouldEnable() const; + std::optional getEnableIfDescription() const; /** * Whether this setting requires a restart on change */ @@ -402,6 +408,8 @@ namespace geode { private: class Impl; std::shared_ptr m_impl; + + friend class ::ModSettingsPopup; protected: bool init(std::shared_ptr setting, float width); @@ -453,7 +461,7 @@ namespace geode { SettingNodeSizeChangeEventV3(SettingNodeV3* node); virtual ~SettingNodeSizeChangeEventV3(); - SettingNodeV3* getTargetNode() const; + SettingNodeV3* getNode() const; }; class GEODE_DLL SettingNodeValueChangeEventV3 : public Event { private: @@ -461,9 +469,10 @@ namespace geode { std::shared_ptr m_impl; public: - SettingNodeValueChangeEventV3(bool commit); + SettingNodeValueChangeEventV3(SettingNodeV3* node, bool commit); virtual ~SettingNodeValueChangeEventV3(); + SettingNodeV3* getNode() const; bool isCommit() const; }; diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp index 0f6ddea55..53357a6f5 100644 --- a/loader/include/Geode/utils/JsonValidation.hpp +++ b/loader/include/Geode/utils/JsonValidation.hpp @@ -427,6 +427,19 @@ namespace geode { } return *this; } + template + JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires { + { predicate(std::declval()) } -> std::convertible_to>; + } { + if (this->hasError()) return *this; + if (auto v = this->template tryGet()) { + auto p = predicate(*v); + if (!p) { + this->setError("json value is not {}: {}", name, p.unwrapErr()); + } + } + return *this; + } // -- Dealing with objects -- diff --git a/loader/src/loader/SettingNodeV3.cpp b/loader/src/loader/SettingNodeV3.cpp index 927ec27ac..5f8add60f 100644 --- a/loader/src/loader/SettingNodeV3.cpp +++ b/loader/src/loader/SettingNodeV3.cpp @@ -15,22 +15,27 @@ SettingNodeSizeChangeEventV3::SettingNodeSizeChangeEventV3(SettingNodeV3* node) } SettingNodeSizeChangeEventV3::~SettingNodeSizeChangeEventV3() = default; -SettingNodeV3* SettingNodeSizeChangeEventV3::getTargetNode() const { +SettingNodeV3* SettingNodeSizeChangeEventV3::getNode() const { return m_impl->node; } class SettingNodeValueChangeEventV3::Impl final { public: + SettingNodeV3* node; bool commit = false; }; -SettingNodeValueChangeEventV3::SettingNodeValueChangeEventV3(bool commit) +SettingNodeValueChangeEventV3::SettingNodeValueChangeEventV3(SettingNodeV3* node, bool commit) : m_impl(std::make_shared()) { + m_impl->node = node; m_impl->commit = commit; } SettingNodeValueChangeEventV3::~SettingNodeValueChangeEventV3() = default; +SettingNodeV3* SettingNodeValueChangeEventV3::getNode() const { + return m_impl->node; +} bool SettingNodeValueChangeEventV3::isCommit() const { return m_impl->commit; } @@ -43,7 +48,7 @@ class SettingNodeV3::Impl final { CCMenu* nameMenu; CCMenu* buttonMenu; CCMenuItemSpriteExtra* resetButton; - ButtonSprite* restartRequiredLabel; + CCLabelBMFont* errorLabel; ccColor4B bgColor = ccc4(0, 0, 0, 0); bool committed = false; }; @@ -68,15 +73,9 @@ bool SettingNodeV3::init(std::shared_ptr setting, float width) { m_impl->nameLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .4f)->setScalePriority(1)); m_impl->nameMenu->addChild(m_impl->nameLabel); - m_impl->restartRequiredLabel = createGeodeTagLabel( - "Restart Required", - {{ - to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)), - to3B(ColorProvider::get()->color("mod-list-restart-required-label-bg"_spr)) - }} - ); - m_impl->restartRequiredLabel->setScale(.25f); - this->addChildAtPosition(m_impl->restartRequiredLabel, Anchor::Left, ccp(10, -10), ccp(0, .5f)); + m_impl->errorLabel = CCLabelBMFont::create("", "bigFont.fnt"); + m_impl->errorLabel->setScale(.25f); + this->addChildAtPosition(m_impl->errorLabel, Anchor::Left, ccp(10, -10), ccp(0, .5f)); if (setting->getDescription()) { auto descSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); @@ -110,15 +109,26 @@ bool SettingNodeV3::init(std::shared_ptr setting, float width) { } void SettingNodeV3::updateState() { + m_impl->errorLabel->setVisible(false); + m_impl->nameLabel->setColor(this->hasUncommittedChanges() ? ccc3(17, 221, 0) : ccWHITE); m_impl->resetButton->setVisible(this->hasNonDefaultValue()); m_impl->bg->setColor(to3B(m_impl->bgColor)); m_impl->bg->setOpacity(m_impl->bgColor.a); - m_impl->restartRequiredLabel->setVisible(false); + if (!m_impl->setting->shouldEnable()) { + if (auto desc = m_impl->setting->getEnableIfDescription()) { + m_impl->nameLabel->setColor(ccGRAY); + m_impl->errorLabel->setVisible(true); + m_impl->errorLabel->setColor("mod-list-errors-found"_cc3b); + m_impl->errorLabel->setString(desc->c_str()); + } + } if (m_impl->setting->requiresRestart() && (this->hasUncommittedChanges() || m_impl->committed)) { - m_impl->restartRequiredLabel->setVisible(true); + m_impl->errorLabel->setVisible(true); + m_impl->errorLabel->setColor("mod-list-restart-required-label"_cc3b); + m_impl->errorLabel->setString("Restart Required"); m_impl->bg->setColor("mod-list-restart-required-label-bg"_cc3b); m_impl->bg->setOpacity(75); } @@ -159,19 +169,19 @@ void SettingNodeV3::setBGColor(ccColor4B const& color) { void SettingNodeV3::markChanged() { this->updateState(); - SettingNodeValueChangeEventV3(false).post(); + SettingNodeValueChangeEventV3(this, false).post(); } void SettingNodeV3::commit() { this->onCommit(); m_impl->committed = true; this->updateState(); - SettingNodeValueChangeEventV3(true).post(); + SettingNodeValueChangeEventV3(this, true).post(); } void SettingNodeV3::resetToDefault() { m_impl->setting->reset(); this->onResetToDefault(); this->updateState(); - SettingNodeValueChangeEventV3(false).post(); + SettingNodeValueChangeEventV3(this, false).post(); } void SettingNodeV3::setContentSize(CCSize const& size) { @@ -259,6 +269,16 @@ bool BoolSettingNodeV3::init(std::shared_ptr setting, float width return true; } +void BoolSettingNodeV3::updateState() { + SettingNodeV3::updateState(); + auto enable = this->getSetting()->shouldEnable(); + m_toggle->setCascadeColorEnabled(true); + m_toggle->setCascadeOpacityEnabled(true); + m_toggle->setEnabled(enable); + m_toggle->setColor(enable ? ccWHITE : ccGRAY); + m_toggle->setOpacity(enable ? 255 : 155); +} + void BoolSettingNodeV3::onCommit() { this->getSetting()->setValue(m_toggle->isToggled()); } @@ -311,19 +331,19 @@ bool StringSettingNodeV3::init(std::shared_ptr setting, float w m_input->getInputNode()->m_placeholderLabel->setOpacity(255); m_input->getInputNode()->m_placeholderLabel->setColor(ccWHITE); - auto arrowLeftSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - arrowLeftSpr->setFlipX(true); - arrowLeftSpr->setScale(.4f); + m_arrowLeftSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + m_arrowLeftSpr->setFlipX(true); + m_arrowLeftSpr->setScale(.4f); auto arrowLeftBtn = CCMenuItemSpriteExtra::create( - arrowLeftSpr, this, menu_selector(StringSettingNodeV3::onArrow) + m_arrowLeftSpr, this, menu_selector(StringSettingNodeV3::onArrow) ); arrowLeftBtn->setTag(-1); this->getButtonMenu()->addChildAtPosition(arrowLeftBtn, Anchor::Left, ccp(5, 0)); - auto arrowRightSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - arrowRightSpr->setScale(.4f); + m_arrowRightSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + m_arrowRightSpr->setScale(.4f); auto arrowRightBtn = CCMenuItemSpriteExtra::create( - arrowRightSpr, this, menu_selector(StringSettingNodeV3::onArrow) + m_arrowRightSpr, this, menu_selector(StringSettingNodeV3::onArrow) ); arrowRightBtn->setTag(1); this->getButtonMenu()->addChildAtPosition(arrowRightBtn, Anchor::Right, ccp(-5, 0)); @@ -334,6 +354,20 @@ bool StringSettingNodeV3::init(std::shared_ptr setting, float w return true; } +void StringSettingNodeV3::updateState() { + SettingNodeV3::updateState(); + auto enable = this->getSetting()->shouldEnable(); + if (!this->getSetting()->getEnumOptions()) { + m_input->setEnabled(enable); + } + else { + m_arrowRightSpr->setOpacity(enable ? 255 : 155); + m_arrowRightSpr->setColor(enable ? ccWHITE : ccGRAY); + m_arrowLeftSpr->setOpacity(enable ? 255 : 155); + m_arrowLeftSpr->setColor(enable ? ccWHITE : ccGRAY); + } +} + void StringSettingNodeV3::onArrow(CCObject* sender) { auto options = *this->getSetting()->getEnumOptions(); auto index = ranges::indexOf(options, m_input->getString()).value_or(0); @@ -350,7 +384,6 @@ void StringSettingNodeV3::onArrow(CCObject* sender) { void StringSettingNodeV3::onCommit() { this->getSetting()->setValue(m_input->getString()); } - bool StringSettingNodeV3::hasUncommittedChanges() const { return m_input->getString() != this->getSetting()->getValue(); } @@ -396,12 +429,12 @@ bool FileSettingNodeV3::init(std::shared_ptr setting, float width m_nameLabel = CCLabelBMFont::create("", "bigFont.fnt"); this->getButtonMenu()->addChildAtPosition(m_nameLabel, Anchor::Left, ccp(13, 0), ccp(0, .5f)); - auto selectSpr = CCSprite::createWithSpriteFrameName("GJ_plus2Btn_001.png"); - selectSpr->setScale(.7f); - auto selectBtn = CCMenuItemSpriteExtra::create( - selectSpr, this, menu_selector(FileSettingNodeV3::onPickFile) + m_selectBtnSpr = CCSprite::createWithSpriteFrameName("GJ_plus2Btn_001.png"); + m_selectBtnSpr->setScale(.7f); + m_selectBtn = CCMenuItemSpriteExtra::create( + m_selectBtnSpr, this, menu_selector(FileSettingNodeV3::onPickFile) ); - this->getButtonMenu()->addChildAtPosition(selectBtn, Anchor::Right, ccp(-5, 0)); + this->getButtonMenu()->addChildAtPosition(m_selectBtn, Anchor::Right, ccp(-5, 0)); this->updateState(); @@ -425,6 +458,11 @@ void FileSettingNodeV3::updateState() { m_nameLabel->setOpacity(255); } m_nameLabel->limitLabelWidth(75, .35f, .1f); + + auto enable = this->getSetting()->shouldEnable(); + m_selectBtnSpr->setOpacity(enable ? 255 : 155); + m_selectBtnSpr->setColor(enable ? ccWHITE : ccGRAY); + m_selectBtn->setEnabled(enable); } void FileSettingNodeV3::onPickFile(CCObject*) { @@ -496,10 +534,10 @@ bool Color3BSettingNodeV3::init(std::shared_ptr setting, float m_colorSprite = ColorChannelSprite::create(); m_colorSprite->setScale(.65f); - auto button = CCMenuItemSpriteExtra::create( + m_colorBtn = CCMenuItemSpriteExtra::create( m_colorSprite, this, menu_selector(Color3BSettingNodeV3::onSelectColor) ); - this->getButtonMenu()->addChildAtPosition(button, Anchor::Right, ccp(-10, 0)); + this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0)); this->updateState(); @@ -509,6 +547,10 @@ bool Color3BSettingNodeV3::init(std::shared_ptr setting, float void Color3BSettingNodeV3::updateState() { SettingNodeV3::updateState(); m_colorSprite->setColor(m_value); + + auto enable = this->getSetting()->shouldEnable(); + m_colorSprite->setOpacity(enable ? 255 : 155); + m_colorBtn->setEnabled(enable); } void Color3BSettingNodeV3::onSelectColor(CCObject*) { @@ -559,10 +601,10 @@ bool Color4BSettingNodeV3::init(std::shared_ptr setting, float m_colorSprite = ColorChannelSprite::create(); m_colorSprite->setScale(.65f); - auto button = CCMenuItemSpriteExtra::create( + m_colorBtn = CCMenuItemSpriteExtra::create( m_colorSprite, this, menu_selector(Color4BSettingNodeV3::onSelectColor) ); - this->getButtonMenu()->addChildAtPosition(button, Anchor::Right, ccp(-10, 0)); + this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0)); this->updateState(); @@ -573,6 +615,10 @@ void Color4BSettingNodeV3::updateState() { SettingNodeV3::updateState(); m_colorSprite->setColor(to3B(m_value)); m_colorSprite->updateOpacity(m_value.a / 255.f); + + auto enable = this->getSetting()->shouldEnable(); + m_colorSprite->setOpacity(enable ? 255 : 155); + m_colorBtn->setEnabled(enable); } void Color4BSettingNodeV3::onSelectColor(CCObject*) { @@ -672,10 +718,10 @@ bool LegacyCustomSettingToV3Node::init(std::shared_ptr or } void LegacyCustomSettingToV3Node::settingValueChanged(SettingNode*) { - SettingNodeValueChangeEventV3(false).post(); + SettingNodeValueChangeEventV3(this, false).post(); } void LegacyCustomSettingToV3Node::settingValueCommitted(SettingNode*) { - SettingNodeValueChangeEventV3(true).post(); + SettingNodeValueChangeEventV3(this, true).post(); } void LegacyCustomSettingToV3Node::onCommit() { diff --git a/loader/src/loader/SettingNodeV3.hpp b/loader/src/loader/SettingNodeV3.hpp index 816acfaee..8d942902d 100644 --- a/loader/src/loader/SettingNodeV3.hpp +++ b/loader/src/loader/SettingNodeV3.hpp @@ -32,6 +32,8 @@ class BoolSettingNodeV3 : public SettingNodeV3 { CCMenuItemToggler* m_toggle; bool init(std::shared_ptr setting, float width); + + void updateState() override; void onCommit() override; void onToggle(CCObject*); @@ -57,6 +59,10 @@ class NumberSettingNodeV3 : public SettingNodeV3 { CCMenuItemSpriteExtra* m_bigArrowLeftBtn; CCMenuItemSpriteExtra* m_arrowRightBtn; CCMenuItemSpriteExtra* m_bigArrowRightBtn; + CCSprite* m_arrowLeftBtnSpr; + CCSprite* m_bigArrowLeftBtnSpr; + CCSprite* m_arrowRightBtnSpr; + CCSprite* m_bigArrowRightBtnSpr; float valueToSlider(ValueType value) { auto min = this->getSetting()->getMinValue().value_or(-100); @@ -79,28 +85,28 @@ class NumberSettingNodeV3 : public SettingNodeV3 { if (!SettingNodeV3::init(setting, width)) return false; - auto bigArrowLeftSpr = CCSprite::create(); - bigArrowLeftSpr->setCascadeColorEnabled(true); - bigArrowLeftSpr->setCascadeOpacityEnabled(true); + m_bigArrowLeftBtnSpr = CCSprite::create(); + m_bigArrowLeftBtnSpr->setCascadeColorEnabled(true); + m_bigArrowLeftBtnSpr->setCascadeOpacityEnabled(true); + auto bigArrowLeftSpr1 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); auto bigArrowLeftSpr2 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); - - bigArrowLeftSpr->setContentSize(bigArrowLeftSpr1->getContentSize() + ccp(20, 0)); - bigArrowLeftSpr->addChildAtPosition(bigArrowLeftSpr2, Anchor::Center, ccp(10, 0)); - bigArrowLeftSpr->addChildAtPosition(bigArrowLeftSpr1, Anchor::Center, ccp(-10, 0)); - bigArrowLeftSpr->setScale(.3f); + m_bigArrowLeftBtnSpr->setContentSize(bigArrowLeftSpr1->getContentSize() + ccp(20, 0)); + m_bigArrowLeftBtnSpr->addChildAtPosition(bigArrowLeftSpr2, Anchor::Center, ccp(10, 0)); + m_bigArrowLeftBtnSpr->addChildAtPosition(bigArrowLeftSpr1, Anchor::Center, ccp(-10, 0)); + m_bigArrowLeftBtnSpr->setScale(.3f); m_bigArrowLeftBtn = CCMenuItemSpriteExtra::create( - bigArrowLeftSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + m_bigArrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); m_bigArrowLeftBtn->setUserObject(ObjWrapper::create(-setting->getBigArrowStepSize())); m_bigArrowLeftBtn->setVisible(setting->isBigArrowsEnabled()); this->getButtonMenu()->addChildAtPosition(m_bigArrowLeftBtn, Anchor::Left, ccp(5, 0)); - auto arrowLeftSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); - arrowLeftSpr->setScale(.5f); + m_arrowLeftBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + m_arrowLeftBtnSpr->setScale(.5f); m_arrowLeftBtn = CCMenuItemSpriteExtra::create( - arrowLeftSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + m_arrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); m_arrowLeftBtn->setUserObject(ObjWrapper::create(-setting->getArrowStepSize())); m_arrowLeftBtn->setVisible(setting->isArrowsEnabled()); @@ -119,31 +125,31 @@ class NumberSettingNodeV3 : public SettingNodeV3 { } this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center); - auto arrowRightSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); - arrowRightSpr->setFlipX(true); - arrowRightSpr->setScale(.5f); + m_arrowRightBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + m_arrowRightBtnSpr->setFlipX(true); + m_arrowRightBtnSpr->setScale(.5f); m_arrowRightBtn = CCMenuItemSpriteExtra::create( - arrowRightSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + m_arrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); m_arrowRightBtn->setUserObject(ObjWrapper::create(setting->getArrowStepSize())); m_arrowRightBtn->setVisible(setting->isArrowsEnabled()); this->getButtonMenu()->addChildAtPosition(m_arrowRightBtn, Anchor::Right, ccp(-22, 0)); - auto bigArrowRightSpr = CCSprite::create(); - bigArrowRightSpr->setCascadeColorEnabled(true); - bigArrowRightSpr->setCascadeOpacityEnabled(true); + m_bigArrowRightBtnSpr = CCSprite::create(); + m_bigArrowRightBtnSpr->setCascadeColorEnabled(true); + m_bigArrowRightBtnSpr->setCascadeOpacityEnabled(true); auto bigArrowRightSpr1 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); bigArrowRightSpr1->setFlipX(true); auto bigArrowRightSpr2 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); bigArrowRightSpr2->setFlipX(true); - bigArrowRightSpr->setContentSize(bigArrowRightSpr1->getContentSize() + ccp(20, 0)); - bigArrowRightSpr->addChildAtPosition(bigArrowRightSpr1, Anchor::Center, ccp(-10, 0)); - bigArrowRightSpr->addChildAtPosition(bigArrowRightSpr2, Anchor::Center, ccp(10, 0)); - bigArrowRightSpr->setScale(.3f); + m_bigArrowRightBtnSpr->setContentSize(bigArrowRightSpr1->getContentSize() + ccp(20, 0)); + m_bigArrowRightBtnSpr->addChildAtPosition(bigArrowRightSpr1, Anchor::Center, ccp(-10, 0)); + m_bigArrowRightBtnSpr->addChildAtPosition(bigArrowRightSpr2, Anchor::Center, ccp(10, 0)); + m_bigArrowRightBtnSpr->setScale(.3f); m_bigArrowRightBtn = CCMenuItemSpriteExtra::create( - bigArrowRightSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + m_bigArrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); m_bigArrowRightBtn->setUserObject(ObjWrapper::create(setting->getBigArrowStepSize())); m_bigArrowRightBtn->setVisible(setting->isBigArrowsEnabled()); @@ -166,27 +172,35 @@ class NumberSettingNodeV3 : public SettingNodeV3 { void updateState() override { SettingNodeV3::updateState(); + auto enable = this->getSetting()->shouldEnable(); + if (this->getSetting()->isInputEnabled()) { + m_input->setEnabled(enable); + } + + auto min = this->getSetting()->getMinValue(); + auto enableLeft = enable && (!min || this->getCurrentValue() > *min); + m_arrowLeftBtn->setEnabled(enableLeft); + m_bigArrowLeftBtn->setEnabled(enableLeft); + m_arrowLeftBtnSpr->setOpacity(enableLeft ? 255 : 155); + m_arrowLeftBtnSpr->setColor(enableLeft ? ccWHITE : ccGRAY); + m_bigArrowLeftBtnSpr->setOpacity(enableLeft ? 255 : 155); + m_bigArrowLeftBtnSpr->setColor(enableLeft ? ccWHITE : ccGRAY); + + auto max = this->getSetting()->getMaxValue(); + auto enableRight = enable && (!max || this->getCurrentValue() < *max); + m_arrowRightBtn->setEnabled(enableRight); + m_bigArrowRightBtn->setEnabled(enableRight); + m_arrowRightBtnSpr->setOpacity(enableRight ? 255 : 155); + m_arrowRightBtnSpr->setColor(enableRight ? ccWHITE : ccGRAY); + m_bigArrowRightBtnSpr->setOpacity(enableRight ? 255 : 155); + m_bigArrowRightBtnSpr->setColor(enableRight ? ccWHITE : ccGRAY); + if (m_slider) { m_slider->m_touchLogic->m_thumb->setValue(this->valueToSlider(this->getCurrentValue())); m_slider->updateBar(); - } - if (auto min = this->getSetting()->getMinValue()) { - auto enable = this->getCurrentValue() > *min; - m_arrowLeftBtn->setEnabled(enable); - m_bigArrowLeftBtn->setEnabled(enable); - static_cast(m_arrowLeftBtn->getNormalImage())->setOpacity(enable ? 255 : 155); - static_cast(m_arrowLeftBtn->getNormalImage())->setColor(enable ? ccWHITE : ccGRAY); - static_cast(m_bigArrowLeftBtn->getNormalImage())->setOpacity(enable ? 255 : 155); - static_cast(m_bigArrowLeftBtn->getNormalImage())->setColor(enable ? ccWHITE : ccGRAY); - } - if (auto max = this->getSetting()->getMaxValue()) { - auto enable = this->getCurrentValue() < *max; - m_arrowRightBtn->setEnabled(enable); - m_bigArrowRightBtn->setEnabled(enable); - static_cast(m_arrowRightBtn->getNormalImage())->setOpacity(enable ? 255 : 155); - static_cast(m_arrowRightBtn->getNormalImage())->setColor(enable ? ccWHITE : ccGRAY); - static_cast(m_bigArrowRightBtn->getNormalImage())->setOpacity(enable ? 255 : 155); - static_cast(m_bigArrowRightBtn->getNormalImage())->setColor(enable ? ccWHITE : ccGRAY); + m_slider->m_sliderBar->setColor(enable ? ccWHITE : ccGRAY); + m_slider->m_touchLogic->m_thumb->setColor(enable ? ccWHITE : ccGRAY); + m_slider->m_touchLogic->m_thumb->setEnabled(enable); } } @@ -250,13 +264,17 @@ using FloatSettingNodeV3 = NumberSettingNodeV3; class StringSettingNodeV3 : public SettingNodeV3 { protected: TextInput* m_input; + CCSprite* m_arrowLeftSpr = nullptr; + CCSprite* m_arrowRightSpr = nullptr; bool init(std::shared_ptr setting, float width); - void onCommit() override; + void updateState() override; void onArrow(CCObject* sender); + void onCommit() override; + public: static StringSettingNodeV3* create(std::shared_ptr setting, float width); @@ -273,6 +291,8 @@ class FileSettingNodeV3 : public SettingNodeV3 { std::filesystem::path m_path; CCLabelBMFont* m_nameLabel; EventListener>> m_pickListener; + CCMenuItemSpriteExtra* m_selectBtn; + CCSprite* m_selectBtnSpr; bool init(std::shared_ptr setting, float width); @@ -294,6 +314,7 @@ class FileSettingNodeV3 : public SettingNodeV3 { class Color3BSettingNodeV3 : public SettingNodeV3, public ColorPickPopupDelegate { protected: ccColor3B m_value; + CCMenuItemSpriteExtra* m_colorBtn; ColorChannelSprite* m_colorSprite; bool init(std::shared_ptr setting, float width); @@ -317,6 +338,7 @@ class Color3BSettingNodeV3 : public SettingNodeV3, public ColorPickPopupDelegate class Color4BSettingNodeV3 : public SettingNodeV3, public ColorPickPopupDelegate { protected: ccColor4B m_value; + CCMenuItemSpriteExtra* m_colorBtn; ColorChannelSprite* m_colorSprite; bool init(std::shared_ptr setting, float width); diff --git a/loader/src/loader/SettingV3.cpp b/loader/src/loader/SettingV3.cpp index 8f74da617..8f63dc97a 100644 --- a/loader/src/loader/SettingV3.cpp +++ b/loader/src/loader/SettingV3.cpp @@ -5,6 +5,398 @@ using namespace geode::prelude; +namespace enable_if_parsing { + struct Component { + virtual ~Component() = default; + virtual Result<> check() const = 0; + virtual Result<> eval(std::string const& defaultModID) const = 0; + }; + struct RequireModLoaded final : public Component { + std::string modID; + RequireModLoaded(std::string const& modID) + : modID(modID) {} + + Result<> check() const override { + return Ok(); + } + Result<> eval(std::string const& defaultModID) const override { + if (Loader::get()->getLoadedMod(modID)) { + return Ok(); + } + auto modName = modID; + if (auto mod = Loader::get()->getInstalledMod(modID)) { + modName = mod->getName(); + } + return Err("Enable the mod {}", modName); + } + }; + struct RequireSettingEnabled final : public Component { + std::string modID; + std::string settingID; + RequireSettingEnabled(std::string const& modID, std::string const& settingID) + : modID(modID), settingID(settingID) {} + + Result<> check() const override { + if (auto mod = Loader::get()->getInstalledMod(modID)) { + if (!mod->hasSetting(settingID)) { + return Err("Mod '{}' does not have setting '{}'", mod->getName(), settingID); + } + if (!typeinfo_pointer_cast(mod->getSettingV3(settingID))) { + return Err("Setting '{}' in mod '{}' is not a boolean setting", settingID, mod->getName()); + } + } + return Ok(); + } + Result<> eval(std::string const& defaultModID) const override { + if (auto mod = Loader::get()->getLoadedMod(modID)) { + if (mod->template getSettingValue(settingID)) { + return Ok(); + } + // This is an if-check just in case, even though check() should already + // make sure that getSettingV3 is guaranteed to return true + auto name = settingID; + if (auto sett = mod->getSettingV3(settingID)) { + name = sett->getDisplayName(); + } + if (modID == defaultModID) { + return Err("Enable the setting '{}'", name); + } + return Err("Enable the setting '{}' from the mod {}", name, mod->getName()); + } + auto modName = modID; + if (auto mod = Loader::get()->getInstalledMod(modID)) { + modName = mod->getName(); + } + return Err("Enable the mod {}", modName); + } + }; + struct RequireSavedValueEnabled final : public Component { + std::string modID; + std::string savedValue; + RequireSavedValueEnabled(std::string const& modID, std::string const& savedValue) + : modID(modID), savedValue(savedValue) {} + + Result<> check() const override { + return Ok(); + } + Result<> eval(std::string const& defaultModID) const override { + if (auto mod = Loader::get()->getLoadedMod(modID)) { + if (mod->template getSavedValue(savedValue)) { + return Ok(); + } + if (modID == defaultModID) { + return Err("Enable the value '{}'", savedValue); + } + return Err("Enable the value '{}' from the mod {}", savedValue, mod->getName()); + } + auto modName = modID; + if (auto mod = Loader::get()->getInstalledMod(modID)) { + modName = mod->getName(); + } + return Err("Enable the mod {}", modName); + } + }; + struct RequireNot final : public Component { + std::unique_ptr component; + RequireNot(std::unique_ptr&& component) + : component(std::move(component)) {} + + Result<> check() const override { + return component->check(); + } + Result<> eval(std::string const& defaultModID) const override { + if (auto res = component->eval(defaultModID)) { + // Surely this will never break! + auto str = res.unwrapErr(); + string::replaceIP(str, "Enable", "___TEMP"); + string::replaceIP(str, "Disable", "Enable"); + string::replaceIP(str, "___TEMP", "Disable"); + return Err(str); + } + return Ok(); + } + }; + struct RequireAll final : public Component { + std::vector> components; + RequireAll(std::vector>&& components) + : components(std::move(components)) {} + + Result<> check() const override { + for (auto& comp : components) { + GEODE_UNWRAP(comp->check()); + } + return Ok(); + } + Result<> eval(std::string const& defaultModID) const override { + // Only print out whatever the first erroring condition is to not shit out + // "Please enable X and Y and Z and Ö and Å and" + for (auto& comp : components) { + GEODE_UNWRAP(comp->eval(defaultModID)); + } + return Ok(); + } + }; + struct RequireSome final : public Component { + std::vector> components; + RequireSome(std::vector>&& components) + : components(std::move(components)) {} + + Result<> check() const override { + for (auto& comp : components) { + GEODE_UNWRAP(comp->check()); + } + return Ok(); + } + Result<> eval(std::string const& defaultModID) const override { + Result<> err = Ok(); + for (auto& comp : components) { + auto res = comp->eval(defaultModID); + if (res) { + return Ok(); + } + // Only show first condition that isn't met + if (err.isOk()) { + err = Err(res.unwrapErr()); + } + } + return err; + } + }; + + static bool isComponentStartChar(char c) { + return + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '_'; + } + static bool isComponentContinueChar(char c) { + return + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || + c == '_' || c == '-' || c == '/' || + c == '.' || c == ':'; + } + + class Parser final { + private: + std::string_view m_src; + size_t m_index = 0; + std::string m_defaultModID; + + static bool isUnOpWord(std::string_view op) { + return op == "!"; + } + static bool isBiOpWord(std::string_view op) { + return op == "&&" || op == "||"; + } + + Result> nextWord() { + // Skip whitespace + while (m_index < m_src.size() && std::isspace(m_src[m_index])) { + m_index += 1; + } + if (m_index == m_src.size()) { + return Ok(std::nullopt); + } + // Parentheses & single operators + if (m_src[m_index] == '(' || m_src[m_index] == ')' || m_src[m_index] == '!') { + m_index += 1; + return Ok(m_src.substr(m_index - 1, 1)); + } + // Double-character operators + if (m_src[m_index] == '&' || m_src[m_index] == '|') { + // Consume first character + m_index += 1; + // Next character must be the same + if (m_index == m_src.size() || m_src[m_index - 1] != m_src[m_index]) { + return Err("Expected '{}' at index {}", m_src[m_index - 1], m_index - 1); + } + // Consume second character + m_index += 1; + return Ok(m_src.substr(m_index - 2, 2)); + } + // Components + if (isComponentStartChar(m_src[m_index])) { + auto start = m_index; + m_index += 1; + while (m_index < m_src.size() && isComponentContinueChar(m_src[m_index])) { + m_index += 1; + } + return Ok(m_src.substr(start, m_index - start)); + } + return Err("Unexpected character '{}' at index {}", m_src[m_index], m_index); + } + std::optional peekWord() { + auto original = m_index; + auto ret = this->nextWord(); + m_index = original; + return ret ? *ret : std::nullopt; + } + Result> nextComponent() { + GEODE_UNWRAP_INTO(auto maybeWord, this->nextWord()); + if (!maybeWord) { + return Err("Expected component, got end-of-enable-if-string"); + } + const auto word = *maybeWord; + if (isUnOpWord(word) || isBiOpWord(word)) { + return Err("Expected component, got operator \"{}\" at index {}", word, m_index - word.size()); + } + if (word == ")") { + return Err("Unexpected closing parenthesis at index {}", m_index - 1); + } + if (word == "(") { + GEODE_UNWRAP_INTO(auto op, this->next()); + GEODE_UNWRAP_INTO(auto maybeClosing, this->nextWord()); + if (!maybeClosing) { + return Err("Expected closing parenthesis, got end-of-enable-if-string"); + } + if (maybeClosing != ")") { + return Err( + "Expected closing parenthesis, got \"{}\" at index {}", + *maybeClosing, m_index - maybeClosing->size() + ); + } + return Ok(std::move(op)); + } + std::string_view ty = "setting"; + std::string_view value = word; + if (word.find(':') != std::string::npos) { + ty = word.substr(0, word.find(':')); + value = word.substr(word.find(':') + 1); + } + switch (hash(ty)) { + case hash("setting"): { + std::string modID = m_defaultModID; + std::string settingID = std::string(value); + // mod.id/setting-id + if (value.find('/') != std::string::npos) { + modID = value.substr(0, value.find('/')); + settingID = value.substr(value.find('/') + 1); + } + if (!ModMetadata::validateID(std::string(modID))) { + return Err("Invalid mod ID '{}'", modID); + } + return Ok(std::make_unique(modID, settingID)); + } break; + + case hash("saved"): { + std::string modID = m_defaultModID; + std::string savedValue = std::string(value); + // mod.id/setting-id + if (value.find('/') != std::string::npos) { + modID = value.substr(0, value.find('/')); + savedValue = value.substr(value.find('/') + 1); + } + if (!ModMetadata::validateID(std::string(modID))) { + return Err("Invalid mod ID '{}'", modID); + } + return Ok(std::make_unique(modID, savedValue)); + } break; + + case hash("loaded"): { + if (!ModMetadata::validateID(std::string(value))) { + return Err("Invalid mod ID '{}'", value); + } + return Ok(std::make_unique(std::string(value))); + } break; + + default: { + return Err("Invalid designator '{}' at index {}", ty, m_index - word.size()); + } break; + } + } + Result> nextUnOp() { + std::string op; + if (auto peek = this->peekWord()) { + if (isUnOpWord(*peek)) { + op = *peek; + } + } + GEODE_UNWRAP_INTO(auto comp, this->nextComponent()); + if (op.empty()) { + return Ok(std::move(comp)); + } + switch (hash(op)) { + case hash("!"): { + return Ok(std::make_unique(std::move(comp))); + } break; + default: { + return Err( + "THIS SHOULD BE UNREACHABLE!! \"{}\" was an unhandled " + "unary operator despite isUnOpWord claiming it's valid! " + "REPORT THIS BUG TO GEODE DEVELOPERS", + op + ); + } break; + } + } + Result> nextBiOp() { + GEODE_UNWRAP_INTO(auto first, this->nextUnOp()); + std::string firstOp; + std::vector> components; + while (auto peek = this->peekWord()) { + if (!isBiOpWord(*peek)) { + break; + } + GEODE_UNWRAP_INTO(auto word, this->nextWord()); + auto op = *word; + if (firstOp.empty()) { + firstOp = op; + } + if (op != firstOp) { + return Err( + "Expected operator \"{}\", got operator \"{}\" - " + "parentheses are required to disambiguate operator chains", + firstOp, op + ); + } + GEODE_UNWRAP_INTO(auto comp, this->nextUnOp()); + components.emplace_back(std::move(comp)); + } + if (components.size()) { + components.emplace(components.begin(), std::move(first)); + switch (hash(firstOp)) { + case hash("&&"): { + return Ok(std::make_unique(std::move(components))); + } break; + case hash("||"): { + return Ok(std::make_unique(std::move(components))); + } break; + default: { + return Err( + "THIS SHOULD BE UNREACHABLE!! \"{}\" was an unhandled " + "binary operator despite isBiOpWord claiming it's valid! " + "REPORT THIS BUG TO GEODE DEVELOPERS", + firstOp + ); + } break; + } + } + return Ok(std::move(first)); + } + Result> next() { + return this->nextBiOp(); + } + + public: + static Result> parse(std::string_view str, std::string const& defaultModID) { + auto ret = Parser(); + ret.m_src = str; + ret.m_defaultModID = defaultModID; + GEODE_UNWRAP_INTO(auto comp, ret.next()); + GEODE_UNWRAP_INTO(auto shouldBeEOF, ret.nextWord()); + if (shouldBeEOF) { + return Err( + "Expected end-of-enable-if-string, got \"{}\" at index {}", + *shouldBeEOF, ret.m_index - shouldBeEOF->size() + ); + } + return Ok(std::move(comp)); + } + }; +} + class SettingV3::GeodeImpl { public: std::string modID; @@ -12,6 +404,8 @@ class SettingV3::GeodeImpl { std::optional name; std::optional description; std::optional enableIf; + std::unique_ptr enableIfTree; + std::optional enableIfDescription; bool requiresRestart = false; }; @@ -30,8 +424,16 @@ void SettingV3::parseSharedProperties(std::string const& key, std::string const& value.has("name").into(m_impl->name); value.has("description").into(m_impl->description); if (!onlyNameAndDesc) { - value.has("enable-if").into(m_impl->enableIf); value.has("requires-restart").into(m_impl->requiresRestart); + value.has("enable-if") + .template mustBe("a valid \"enable-if\" scheme", [this](std::string const& str) -> Result<> { + GEODE_UNWRAP_INTO(auto tree, enable_if_parsing::Parser::parse(str, m_impl->modID)); + GEODE_UNWRAP(tree->check()); + m_impl->enableIfTree = std::move(tree); + return Ok(); + }) + .into(m_impl->enableIf); + value.has("enable-if-description").into(m_impl->enableIfDescription); } } void SettingV3::init(std::string const& key, std::string const& modID) { @@ -57,6 +459,25 @@ std::optional SettingV3::getDescription() const { std::optional SettingV3::getEnableIf() const { return m_impl->enableIf; } +bool SettingV3::shouldEnable() const { + if (m_impl->enableIfTree) { + return m_impl->enableIfTree->eval(m_impl->modID).isOk(); + } + return true; +} +std::optional SettingV3::getEnableIfDescription() const { + if (m_impl->enableIfDescription) { + return *m_impl->enableIfDescription; + } + if (!m_impl->enableIfTree) { + return std::nullopt; + } + auto res = m_impl->enableIfTree->eval(m_impl->modID); + if (res) { + return std::nullopt; + } + return res.unwrapErr(); +} bool SettingV3::requiresRestart() const { return m_impl->requiresRestart; } diff --git a/loader/src/ui/mods/settings/ModSettingsPopup.cpp b/loader/src/ui/mods/settings/ModSettingsPopup.cpp index 4b618b57b..9c8a15460 100644 --- a/loader/src/ui/mods/settings/ModSettingsPopup.cpp +++ b/loader/src/ui/mods/settings/ModSettingsPopup.cpp @@ -87,8 +87,8 @@ bool ModSettingsPopup::setup(Mod* mod) { ); m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-53, 20)); - m_changeListener.bind([this](auto) { - this->updateState(); + m_changeListener.bind([this](auto* ev) { + this->updateState(ev->getNode()); return ListenerResult::Propagate; }); this->updateState(); @@ -125,7 +125,17 @@ void ModSettingsPopup::onResetAll(CCObject*) { ); } -void ModSettingsPopup::updateState() { +void ModSettingsPopup::updateState(SettingNodeV3* invoker) { + // Update all settings with "enable-if" schemes + for (auto& sett : m_settings) { + // Avoid infinite loops + if (sett == invoker) { + continue; + } + if (sett->getSetting()->getEnableIf()) { + sett->updateState(); + } + } m_applyBtnSpr->setCascadeColorEnabled(true); m_applyBtnSpr->setCascadeOpacityEnabled(true); if (this->hasUncommitted()) { diff --git a/loader/src/ui/mods/settings/ModSettingsPopup.hpp b/loader/src/ui/mods/settings/ModSettingsPopup.hpp index 51e8a9c22..bc3492938 100644 --- a/loader/src/ui/mods/settings/ModSettingsPopup.hpp +++ b/loader/src/ui/mods/settings/ModSettingsPopup.hpp @@ -16,8 +16,7 @@ class ModSettingsPopup : public GeodePopup { EventListener> m_changeListener; bool setup(Mod* mod) override; - void updateState(); - void onChangeEvent(SettingNodeValueChangeEventV3* event); + void updateState(SettingNodeV3* invoker = nullptr); bool hasUncommitted() const; void onClose(CCObject*) override; void onApply(CCObject*);