diff --git a/CHANGELOG.md b/CHANGELOG.md index dc997bcb..4c29d15d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Geode Changelog +## v3.6.0 + * Major rework of the entire settings system with lots of new features; see the [docs page](https://docs.geode-sdk.org/mods/settings) for more + * Rework JSON validation; now uses the `JsonExpectedValue` class with the `checkJson` helper (89d1a51) + * Add `Task::cancelled` for creating immediately cancelled Tasks (1a82d12) + * Add function type utilities in `utils/function.hpp` (659c168) + * Add `typeinfo_pointer_cast` for casting `std::shared_ptr`s (28cc6fd) + * Add `GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH` (1032d9a) + * Add `PlatformID::getCovered` (d5718be) + * Rename `toByteArray` to `toBytes` (6eb0797) + * Improve `AxisLayout::getSizeHint` (85e7b5e) + * Fix issues with file dialogs on Windows (62b6241, 971e3fb) + * Mod incompatibilities may now be platform-specific (9f1c70a) + ## v3.5.0 * Move CCLighting to cocos headers (#1036) * Add new `gd::string` constructor (bae22b4) diff --git a/VERSION b/VERSION index 1545d966..40c341bd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 +3.6.0 diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 33b43175..52a87771 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -10,6 +10,7 @@ #include "Hook.hpp" #include "ModMetadata.hpp" #include "Setting.hpp" +#include "SettingV3.hpp" #include "Types.hpp" #include "Loader.hpp" @@ -23,6 +24,8 @@ #include namespace geode { + class SettingV3; + template struct HandleToSaved : public T { Mod* m_mod; @@ -170,12 +173,32 @@ namespace geode { */ std::filesystem::path getConfigDir(bool create = true) const; + /** + * Returns true if this mod has any settings + */ bool hasSettings() const; + /** + * Get a list of all this mod's setting keys (in the order they were + * declared in `mod.json`) + */ std::vector getSettingKeys() const; bool hasSetting(std::string_view const key) const; + + // todo in v4: remove these + [[deprecated("Use Mod::getSettingV3")]] std::optional getSettingDefinition(std::string_view const key) const; + [[deprecated("Use Mod::getSettingV3")]] SettingValue* getSetting(std::string_view const key) const; + // todo in v4: possibly rename this to getSetting? + /** + * Get the definition of a setting, or null if the setting was not found, + * or if it's a custom setting that has not yet been registered using + * `Mod::registerCustomSettingType` + * @param key The key of the setting as defined in `mod.json` + */ + std::shared_ptr getSettingV3(std::string_view const key) const; + /** * Register a custom setting's value class. See Mod::addCustomSetting * for a convenience wrapper that creates the value in-place to avoid @@ -186,6 +209,7 @@ namespace geode { * @param value The SettingValue class that shall handle this setting * @see addCustomSetting */ + [[deprecated("Use Mod::registerCustomSettingType")]] void registerCustomSetting(std::string_view const key, std::unique_ptr value); /** * Register a custom setting's value class. The new SettingValue class @@ -200,10 +224,21 @@ namespace geode { * } */ template + [[deprecated("Use Mod::registerCustomSettingType")]] void addCustomSetting(std::string_view const key, V const& value) { this->registerCustomSetting(key, std::make_unique(std::string(key), this->getID(), value)); } + /** + * Register a custom setting type. See + * [the setting docs](https://docs.geode-sdk.org/mods/settings) for more + * @param type The type of the setting. This should **not** include the + * `custom:` prefix! + * @param generator A pointer to a function that, when called, returns a + * newly-created instance of the setting type + */ + Result<> registerCustomSettingType(std::string_view type, SettingGenerator generator); + /** * Returns a prefixed launch argument name. See `Mod::getLaunchArgument` * for details about mod-specific launch arguments. @@ -246,19 +281,27 @@ namespace geode { matjson::Value& getSaveContainer(); matjson::Value& getSavedSettingsData(); + /** + * Get the value of a [setting](https://docs.geode-sdk.org/mods/settings). + * To use this for custom settings, first specialize the + * `SettingTypeForValueType` class, and then make sure your custom + * setting type has a `getValue` function which returns the value + */ template T getSettingValue(std::string_view const key) const { - if (auto sett = this->getSetting(key)) { - return SettingValueSetter::get(sett); + using S = typename SettingTypeForValueType::SettingType; + if (auto sett = cast::typeinfo_pointer_cast(this->getSettingV3(key))) { + return sett->getValue(); } return T(); } template T setSettingValue(std::string_view const key, T const& value) { - if (auto sett = this->getSetting(key)) { - auto old = this->getSettingValue(key); - SettingValueSetter::set(sett, value); + using S = typename SettingTypeForValueType::SettingType; + if (auto sett = cast::typeinfo_pointer_cast(this->getSettingV3(key))) { + auto old = sett->getValue(); + sett->setValue(value); return old; } return T(); diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index b3b8454c..588e9f96 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -184,7 +184,12 @@ namespace geode { * Mod settings * @note Not a map because insertion order must be preserved */ - [[nodiscard]] std::vector> getSettings() const; + [[nodiscard, deprecated("Use getSettingsV3")]] std::vector> getSettings() const; + /** + * Mod settings + * @note Not a map because insertion order must be preserved + */ + [[nodiscard]] std::vector> getSettingsV3() const; /** * Get the tags for this mod */ @@ -232,7 +237,9 @@ namespace geode { void setDependencies(std::vector const& value); void setIncompatibilities(std::vector const& value); void setSpritesheets(std::vector const& value); + [[deprecated("This function does NOTHING")]] void setSettings(std::vector> const& value); + void setSettings(std::vector> const& value); void setTags(std::unordered_set const& value); void setNeedsEarlyLoad(bool const& value); void setIsAPI(bool const& value); diff --git a/loader/include/Geode/loader/ModSettingsManager.hpp b/loader/include/Geode/loader/ModSettingsManager.hpp new file mode 100644 index 00000000..6281bf7b --- /dev/null +++ b/loader/include/Geode/loader/ModSettingsManager.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include "SettingV3.hpp" + +namespace geode { + class GEODE_DLL ModSettingsManager final { + private: + class Impl; + std::unique_ptr m_impl; + + friend class ::geode::SettingV3; + + void markRestartRequired(); + + public: + static ModSettingsManager* from(Mod* mod); + + ModSettingsManager(ModMetadata const& metadata); + ~ModSettingsManager(); + + ModSettingsManager(ModSettingsManager&&); + ModSettingsManager(ModSettingsManager const&) = delete; + + /** + * Load setting values from savedata. + * The format of the savedata should be an object with the keys being + * setting IDs and then the values the values of the saved settings + * @returns Ok if no horrible errors happened. Note that a setting value + * missing is not considered a horrible error, but will instead just log a + * warning into the console! + */ + Result<> load(matjson::Value const& json); + /** + * Save setting values to savedata. + * The format of the savedata will be an object with the keys being + * setting IDs and then the values the values of the saved settings + * @note If saving a setting fails, it will log a warning to the console + */ + void save(matjson::Value& json); + + Result<> registerCustomSettingType(std::string_view type, SettingGenerator generator); + // todo in v4: remove this + Result<> registerLegacyCustomSetting(std::string_view key, std::unique_ptr&& ptr); + + std::shared_ptr get(std::string_view key); + std::shared_ptr getLegacy(std::string_view key); + std::optional getLegacyDefinition(std::string_view key); + + /** + * Returns true if any setting with the `"restart-required"` attribute + * has been altered + */ + bool restartRequired() const; + }; +} diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp index 50317b70..45cc6491 100644 --- a/loader/include/Geode/loader/Setting.hpp +++ b/loader/include/Geode/loader/Setting.hpp @@ -286,9 +286,7 @@ namespace geode { return Setting(m_key, m_modID, m_definition); } - ValueType getValue() const { - return m_value; - } + GEODE_DLL ValueType getValue() const; GEODE_DLL void setValue(ValueType const& value); GEODE_DLL Result<> validate(ValueType const& value) const; }; @@ -301,8 +299,10 @@ namespace geode { using ColorSettingValue = GeodeSettingValue; using ColorAlphaSettingValue = GeodeSettingValue; + // todo: remove in v3 + template - struct GEODE_DLL SettingValueSetter { + struct [[deprecated("Use SettingTypeForValueType from SettingV3 instead")]] GEODE_DLL SettingValueSetter { static T get(SettingValue* setting); static void set(SettingValue* setting, T const& value); }; diff --git a/loader/include/Geode/loader/SettingEvent.hpp b/loader/include/Geode/loader/SettingEvent.hpp index 7ad33fe1..b84b7884 100644 --- a/loader/include/Geode/loader/SettingEvent.hpp +++ b/loader/include/Geode/loader/SettingEvent.hpp @@ -4,18 +4,18 @@ #include "Loader.hpp" #include "Setting.hpp" #include "Mod.hpp" - +#include "SettingV3.hpp" #include namespace geode { - struct GEODE_DLL SettingChangedEvent : public Event { + struct GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedEvent : public Event { Mod* mod; SettingValue* value; SettingChangedEvent(Mod* mod, SettingValue* value); }; - class GEODE_DLL SettingChangedFilter : public EventFilter { + class GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedFilter : public EventFilter { protected: std::string m_modID; std::optional m_targetKey; @@ -40,7 +40,7 @@ namespace geode { * Listen for built-in setting changes */ template - class GeodeSettingChangedFilter : public SettingChangedFilter { + class [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] GeodeSettingChangedFilter : public SettingChangedFilter { public: using Callback = void(T); @@ -60,21 +60,4 @@ namespace geode { ) : SettingChangedFilter(modID, settingID) {} GeodeSettingChangedFilter(GeodeSettingChangedFilter const&) = default; }; - - template - std::monostate listenForSettingChanges( - std::string const& settingKey, void (*callback)(T) - ) { - (void)new EventListener( - callback, GeodeSettingChangedFilter(getMod()->getID(), settingKey) - ); - return std::monostate(); - } - - static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) { - (void)new EventListener( - callback, SettingChangedFilter(getMod()->getID(), std::nullopt) - ); - return std::monostate(); - } } diff --git a/loader/include/Geode/loader/SettingV3.hpp b/loader/include/Geode/loader/SettingV3.hpp new file mode 100644 index 00000000..3552f5a7 --- /dev/null +++ b/loader/include/Geode/loader/SettingV3.hpp @@ -0,0 +1,822 @@ +#pragma once + +#include "../DefaultInclude.hpp" +#include +#include +// todo: remove this header in 4.0.0 +#include "Setting.hpp" +#include "../utils/cocos.hpp" +// this unfortunately has to be included because of C++ templates +#include "../utils/JsonValidation.hpp" +#include "../utils/function.hpp" + +// todo in v4: this can be removed as well as the friend decl in LegacyCustomSettingV3 +class LegacyCustomSettingToV3Node; +class ModSettingsPopup; + +namespace geode { + class ModSettingsManager; + class SettingNodeV3; + // todo in v4: remove this + class SettingValue; + + class GEODE_DLL SettingV3 : public std::enable_shared_from_this { + private: + class GeodeImpl; + std::shared_ptr m_impl; + + protected: + /** + * Only call this function if you aren't going to call + * `parseBaseProperties`, which will call it for you! + * If you don't want to call `parseBaseProperties`, at the very least + * you **must** call this! + * Select which properties you want to parse using the `parseX` + * functions + * @param key The setting's key as defined in `mod.json` + * @param modID The ID of the mod this settings is being parsed for + * @param json The current JSON checking instance being used. This + * should be the JSON object that defines the setting. If you aren't + * using Geode's JSON checking utilities, you can use the other + * overload of `init` + */ + void init(std::string const& key, std::string const& modID, JsonExpectedValue& json); + /** + * Only call this function if you aren't going to call + * `parseBaseProperties`, which will call it for you! + * If you don't want to call `parseBaseProperties`, at the very least + * you **must** call this! + * Select which properties you want to parse using the `parseX` + * functions + * @param key The setting's key as defined in `mod.json` + * @param modID The ID of the mod this settings is being parsed for + * @note If you are using Geode's JSON checking utilities + * (`checkJson` / `JsonExpectedValue`), you should be using the other + * overload that takes a `JsonExpectedValue&`! + */ + void init(std::string const& key, std::string const& modID); + + /** + * Parses the `"name"` and `"description"` keys from the setting's + * definition in `mod.json` (if they exist), so their values can be + * accessed via `getName` and `getDescription`. + * @param json The current JSON checking instance being used. This + * should be the JSON object that defines the setting + * @warning In most cases, you should be using `parseBaseProperties` + * instead to do all of this in one go! + * If you do need the fine-grained control however, make sure to call + * `init` before calling these parsing functions! + */ + void parseNameAndDescription(JsonExpectedValue& json); + /** + * Parses the `"enable-if"` and `"enable-if-description"` keys from + * the setting's definition in `mod.json` (if they exist), so + * `shouldEnable` and `getEnableIfDescription` work. + * @param json The current JSON checking instance being used. This + * should be the JSON object that defines the setting + * @warning In most cases, you should be using `parseBaseProperties` + * instead to do all of this in one go! + * If you do need the fine-grained control however, make sure to call + * `init` before calling these parsing functions! + */ + void parseEnableIf(JsonExpectedValue& json); + /** + * Parses the `"requires-restart"` key from the setting's definition in + * `mod.json` (if they exist), so `requiresRestart` works. + * @param json The current JSON checking instance being used. This + * should be the JSON object that defines the setting + * @warning In most cases, you should be using `parseBaseProperties` + * instead to do all of this in one go! + * If you do need the fine-grained control however, make sure to call + * `init` before calling these parsing functions! + */ + void parseValueProperties(JsonExpectedValue& json); + + /** + * Parse all of the base properties such as `"name"` and `"description"` + * for this setting + * @param key The setting's key as defined in `mod.json` + * @param modID The ID of the mod this settings is being parsed for + * @param json The current JSON checking instance being used. If you + * aren't using Geode's JSON checking utilities, use the other overload + * of this function + * @note If you don't want to parse some of the base properties, such as + * `"requires-restart"` (because you're doing a cosmetic setting), then + * you can call `init` instead and then the specific `parseX` functions + */ + void parseBaseProperties(std::string const& key, std::string const& modID, JsonExpectedValue& json); + /** + * Parse all of the base properties such as `"name"` and `"description"` + * for this setting + * @param key The setting's key as defined in `mod.json` + * @param modID The ID of the mod this settings is being parsed for + * @param json The JSON value. If you are using Geode's JSON checking + * utilities (`checkJson` / `JsonExpectedValue`), you should use the + * other overload directly! + * @note If you don't want to parse some of the base properties, such as + * `"requires-restart"` (because you're doing a cosmetic setting), then + * you can call `init` instead and then the specific `parseX` functions + */ + Result<> parseBaseProperties(std::string const& key, std::string const& modID, matjson::Value const& json); + + /** + * Mark that the value of this setting has changed. This should be + * ALWAYS called on every setter that can modify the setting's state! + */ + void markChanged(); + + friend class ::geode::SettingValue; + + public: + SettingV3(); + virtual ~SettingV3(); + + /** + * Get the key of this setting + */ + std::string getKey() const; + /** + * Get the mod ID this setting is for + */ + std::string getModID() const; + /** + * Get the mod this setting is for. Note that this may return null + * while the mod is still being initialized + */ + Mod* getMod() const; + /** + * Get the name of this setting + */ + std::optional getName() const; + /** + * Get the name of this setting, or its key if it has no name + */ + std::string getDisplayName() const; + /** + * Get the description of this setting + */ + std::optional getDescription() const; + /** + * 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 + */ + bool requiresRestart() const; + /** + * Get the platforms this setting is available on + */ + std::vector getPlatforms() const; + + virtual bool load(matjson::Value const& json) = 0; + virtual bool save(matjson::Value& json) const = 0; + virtual SettingNodeV3* createNode(float width) = 0; + + virtual bool isDefaultValue() const = 0; + /** + * Reset this setting's value back to its original value + */ + virtual void reset() = 0; + + [[deprecated( + "This function will be removed alongside legacy settings in 4.0.0! " + "You should NOT be implementing it for your own custom setting classes" + )]] + virtual std::optional convertToLegacy() const; + [[deprecated( + "This function will be removed alongside legacy settings in 4.0.0! " + "You should NOT be implementing it for your own custom setting classes" + )]] + virtual std::optional> convertToLegacyValue() const; + }; + + using SettingGenerator = std::function>( + std::string const& key, + std::string const& modID, + matjson::Value const& json + )>; + + /** + * A helper class for creating a basic setting with a simple value. + * Override the virtual function `isValid` to + * @tparam T The type of the setting's value. This type must be JSON- + * serializable and deserializable! + * @tparam V The type used for the `setValue` function, if it differs from T + */ + template + class SettingBaseValueV3 : public SettingV3 { + private: + class Impl final { + private: + T defaultValue; + T value; + friend class SettingBaseValueV3; + }; + std::shared_ptr m_impl; + + protected: + /** + * Parses the `"default"` key from the setting's definition in + * `mod.json`. The key may also be defined per-platform, i.e. + * `"default": { "win": ..., "android": ... }` + * @param json The current JSON checking instance being used. This + * should be the JSON object that defines the setting + * @warning In most cases, you should be using `parseBaseProperties` + * instead to do all of this in one go! + * If you do need the fine-grained control however, make sure to call + * `init` before calling these parsing functions! + */ + void parseDefaultValue(JsonExpectedValue& json) { + auto root = json.needs("default"); + // Check if this is a platform-specific default value + if (root.isObject() && root.has(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH)) { + root.needs(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH).into(m_impl->defaultValue); + } + else { + root.into(m_impl->defaultValue); + } + m_impl->value = m_impl->defaultValue; + } + + /** + * Parse shared value, including the default value for this setting + * @param key The key of the setting + * @param modID The ID of the mod this setting is being parsed for + * @param json The current JSON checking instance being used. If you + * aren't using Geode's JSON checking utilities, use the other overload + * of this function + */ + void parseBaseProperties(std::string const& key, std::string const& modID, JsonExpectedValue& json) { + SettingV3::parseBaseProperties(key, modID, json); + this->parseDefaultValue(json); + } + /** + * Parse shared value, including the default value for this setting + * @param key The key of the setting + * @param modID The ID of the mod this setting is being parsed for + * @param json The JSON value. If you are using Geode's JSON checking + * utilities (`checkJson` / `JsonExpectedValue`), you should use the + * other overload directly! + */ + Result<> parseBaseProperties(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "SettingBaseValueV3"); + this->parseBaseProperties(key, modID, root); + return root.ok(); + } + + /** + * Set the default value. This does not check that the value is + * actually valid! + */ + void setDefaultValue(V value) { + m_impl->defaultValue = value; + } + + public: + SettingBaseValueV3() : m_impl(std::make_shared()) {} + + using ValueType = T; + using ValueAssignType = V; + + /** + * Get the default value for this setting + */ + T getDefaultValue() const { + return m_impl->defaultValue; + } + + /** + * Get the current value of this setting + */ + T getValue() const { + return m_impl->value; + } + /** + * Set the value of this setting. This will broadcast a new + * SettingChangedEventV3, letting any listeners now the value has changed + * @param value The new value for the setting. If the value is not a + * valid value for this setting (as determined by `isValue`), then the + * setting's value is reset to the default value + */ + void setValue(V value) { + m_impl->value = this->isValid(value) ? value : m_impl->defaultValue; + this->markChanged(); + } + /** + * Check if a given value is valid for this setting. If not, an error + * describing why the value isn't valid is returned + */ + virtual Result<> isValid(V value) const { + return Ok(); + } + + bool isDefaultValue() const override { + return m_impl->value == m_impl->defaultValue; + } + void reset() override { + this->setValue(m_impl->defaultValue); + } + + bool load(matjson::Value const& json) override { + if (json.template is()) { + m_impl->value = json.template as(); + return true; + } + return false; + } + bool save(matjson::Value& json) const override { + json = m_impl->value; + return true; + } + }; + + class GEODE_DLL TitleSettingV3 final : public SettingV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + TitleSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + }; + + // todo in v4: remove this class completely + class GEODE_DLL LegacyCustomSettingV3 final : public SettingV3 { + private: + class Impl; + std::shared_ptr m_impl; + + friend class ::geode::ModSettingsManager; + friend class ::LegacyCustomSettingToV3Node; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + LegacyCustomSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + std::shared_ptr getValue() const; + void setValue(std::shared_ptr value); + + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL BoolSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + BoolSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(bool value) const override; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL IntSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + IntSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(int64_t value) const override; + + std::optional getMinValue() const; + std::optional getMaxValue() const; + + bool isArrowsEnabled() const; + bool isBigArrowsEnabled() const; + size_t getArrowStepSize() const; + size_t getBigArrowStepSize() const; + bool isSliderEnabled() const; + int64_t getSliderSnap() const; + bool isInputEnabled() const; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL FloatSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + FloatSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(double value) const override; + + std::optional getMinValue() const; + std::optional getMaxValue() const; + + bool isArrowsEnabled() const; + bool isBigArrowsEnabled() const; + double getArrowStepSize() const; + double getBigArrowStepSize() const; + bool isSliderEnabled() const; + double getSliderSnap() const; + bool isInputEnabled() const; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL StringSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + StringSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(std::string_view value) const override; + + std::optional getRegexValidator() const; + std::optional getAllowedCharacters() const; + std::optional> getEnumOptions() const; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL FileSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + FileSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(std::filesystem::path const& value) const override; + + bool isFolder() const; + bool useSaveDialog() const; + + std::optional> getFilters() const; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL Color3BSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + Color3BSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(cocos2d::ccColor3B value) const override; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL Color4BSettingV3 final : public SettingBaseValueV3 { + private: + class Impl; + std::shared_ptr m_impl; + + private: + class PrivateMarker {}; + friend class SettingV3; + + public: + Color4BSettingV3(PrivateMarker); + static Result> parse(std::string const& key, std::string const& modID, matjson::Value const& json); + + Result<> isValid(cocos2d::ccColor4B value) const override; + + SettingNodeV3* createNode(float width) override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL SettingNodeV3 : public cocos2d::CCNode { + private: + class Impl; + std::shared_ptr m_impl; + + friend class ::ModSettingsPopup; + + protected: + bool init(std::shared_ptr setting, float width); + + /** + * Update the state of this setting node, bringing all inputs + * up-to-date with the current value. Derivatives of `SettingNodeV3` + * should set update the state (such as visibility, value, etc.) of all + * its controls, except for the one that's passed as the `invoker` + * argument. Derivatives should remember to **always call the base + * class's `updateState` function**, as it updates the built-in title + * label as well as the description and reset buttons! + * @param invoker The button or other interactive element that caused + * this state update. If that element is for example a text input, it + * may wish to ignore the state update, as it itself is the source of + * truth for the node's value at that moment. May be nullptr to mark + * that no specific node requested this state update + */ + virtual void updateState(cocos2d::CCNode* invoker); + + /** + * Mark this setting as changed. This updates the UI for committing + * the value, as well as posts a `SettingNodeValueChangeEventV3` + * @param invoker The node to be passed onto `updateState` + */ + void markChanged(cocos2d::CCNode* invoker); + + /** + * When the setting value is committed (aka can't be undone), this + * function will be called. This should take care of actually saving + * the value in some sort of global manager + */ + virtual void onCommit() = 0; + virtual void onResetToDefault() = 0; + + void onDescription(CCObject*); + void onReset(CCObject*); + + public: + void commit(); + void resetToDefault(); + virtual bool hasUncommittedChanges() const = 0; + virtual bool hasNonDefaultValue() const = 0; + + // Can be overridden by the setting itself + // Can / should be used to do alternating BG + void setDefaultBGColor(cocos2d::ccColor4B color); + + cocos2d::CCLabelBMFont* getNameLabel() const; + cocos2d::CCLabelBMFont* getStatusLabel() const; + cocos2d::CCMenu* getNameMenu() const; + cocos2d::CCMenu* getButtonMenu() const; + cocos2d::CCLayerColor* getBG() const; + + void setContentSize(cocos2d::CCSize const& size) override; + + std::shared_ptr getSetting() const; + }; + + /** + * Helper class for creating `SettingNode`s for simple settings that + * implement `SettingBaseValueV3` + */ + template + class SettingValueNodeV3 : public SettingNodeV3 { + protected: + private: + class Impl final { + private: + typename S::ValueType currentValue; + friend class SettingValueNodeV3; + }; + std::shared_ptr m_impl; + + protected: + bool init(std::shared_ptr setting, float width) { + if (!SettingNodeV3::init(setting, width)) + return false; + + m_impl = std::make_shared(); + m_impl->currentValue = setting->getValue(); + + return true; + } + + void updateState(cocos2d::CCNode* invoker) { + SettingNodeV3::updateState(invoker); + auto validate = this->getSetting()->isValid(m_impl->currentValue); + if (!validate) { + this->getStatusLabel()->setVisible(true); + this->getStatusLabel()->setString(validate.unwrapErr().c_str()); + this->getStatusLabel()->setColor(cocos2d::ccc3(235, 35, 52)); + } + } + + void onCommit() override { + this->getSetting()->setValue(m_impl->currentValue); + // The value may be different, if the current value was an invalid + // value for the setting + this->setValue(this->getSetting()->getValue(), nullptr); + } + bool hasUncommittedChanges() const override { + return m_impl->currentValue != this->getSetting()->getValue(); + } + bool hasNonDefaultValue() const override { + return m_impl->currentValue != this->getSetting()->getDefaultValue(); + } + void onResetToDefault() override { + this->setValue(this->getSetting()->getDefaultValue(), nullptr); + } + + public: + /** + * Get the **uncommitted** value for this node + */ + typename S::ValueType getValue() const { + return m_impl->currentValue; + } + /** + * Set the **uncommitted** value for this node + * @param value The value to set + * @param invoker The node that invoked this value change; see the docs + * for `SettingNodeV3::updateState` to know more + */ + void setValue(typename S::ValueAssignType value, cocos2d::CCNode* invoker) { + m_impl->currentValue = value; + this->markChanged(invoker); + } + + std::shared_ptr getSetting() const { + return std::static_pointer_cast(SettingNodeV3::getSetting()); + } + }; + + class GEODE_DLL SettingChangedEventV3 final : public Event { + private: + class Impl; + std::shared_ptr m_impl; + + public: + SettingChangedEventV3(std::shared_ptr setting); + + std::shared_ptr getSetting() const; + }; + class GEODE_DLL SettingChangedFilterV3 final : public EventFilter { + private: + class Impl; + std::shared_ptr m_impl; + + public: + using Callback = void(std::shared_ptr); + + ListenerResult handle(utils::MiniFunction fn, SettingChangedEventV3* event); + /** + * Listen to changes on a setting, or all settings + * @param modID Mod whose settings to listen to + * @param settingKey Setting to listen to, or all settings if nullopt + */ + SettingChangedFilterV3( + std::string const& modID, + std::optional const& settingKey + ); + SettingChangedFilterV3(Mod* mod, std::optional const& settingKey); + SettingChangedFilterV3(SettingChangedFilterV3 const&); + }; + + class GEODE_DLL SettingNodeSizeChangeEventV3 : public Event { + private: + class Impl; + std::shared_ptr m_impl; + + public: + SettingNodeSizeChangeEventV3(SettingNodeV3* node); + virtual ~SettingNodeSizeChangeEventV3(); + + SettingNodeV3* getNode() const; + }; + class GEODE_DLL SettingNodeValueChangeEventV3 : public Event { + private: + class Impl; + std::shared_ptr m_impl; + + public: + SettingNodeValueChangeEventV3(SettingNodeV3* node, bool commit); + virtual ~SettingNodeValueChangeEventV3(); + + SettingNodeV3* getNode() const; + bool isCommit() const; + }; + + template + struct SettingTypeForValueType { + static_assert( + !std::is_same_v, + "specialize the SettingTypeForValueType class to use Mod::getSettingValue for custom settings" + ); + }; + + template <> + struct SettingTypeForValueType { + using SettingType = BoolSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = IntSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = FloatSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = StringSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = FileSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = Color3BSettingV3; + }; + template <> + struct SettingTypeForValueType { + using SettingType = Color4BSettingV3; + }; + + template + EventListener* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) { + using Ty = typename SettingTypeForValueType::SettingType; + return new EventListener( + [callback = std::move(callback)](std::shared_ptr setting) { + if (auto ty = geode::cast::typeinfo_pointer_cast(setting)) { + callback(ty->getValue()); + } + }, + SettingChangedFilterV3(mod, std::string(settingKey)) + ); + } + EventListener* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) { + using T = std::remove_cvref_t>; + return listenForSettingChanges(settingKey, std::move(callback), mod); + } + GEODE_DLL EventListener* listenForAllSettingChanges( + std::function)> const& callback, + Mod* mod = getMod() + ); +} diff --git a/loader/include/Geode/platform/cplatform.h b/loader/include/Geode/platform/cplatform.h index c930d4c2..54676313 100644 --- a/loader/include/Geode/platform/cplatform.h +++ b/loader/include/Geode/platform/cplatform.h @@ -16,6 +16,7 @@ #define GEODE_PLATFORM_NAME "Windows" #define GEODE_PLATFORM_EXTENSION ".dll" #define GEODE_PLATFORM_SHORT_IDENTIFIER "win" + #define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "win" #define CC_TARGET_OS_WIN32 #if defined(WIN64) || defined(_WIN64) || defined(__WIN64) && !defined(__CYGWIN__) @@ -47,6 +48,7 @@ #define GEODE_PLATFORM_NAME "iOS" #define GEODE_PLATFORM_EXTENSION ".ios.dylib" #define GEODE_PLATFORM_SHORT_IDENTIFIER "ios" + #define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "ios" #define CC_TARGET_OS_IPHONE #else #define GEODE_IOS(...) @@ -54,6 +56,7 @@ #define GEODE_IS_MACOS #define GEODE_IS_DESKTOP #define GEODE_PLATFORM_EXTENSION ".dylib" + #define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "mac" #define CC_TARGET_OS_MAC #if TARGET_CPU_ARM64 @@ -85,6 +88,7 @@ #define GEODE_IS_MOBILE #define GEODE_CALL #define CC_TARGET_OS_ANDROID + #define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "android" #if defined(__arm__) #define GEODE_ANDROID32(...) __VA_ARGS__ diff --git a/loader/include/Geode/platform/platform.hpp b/loader/include/Geode/platform/platform.hpp index afca28da..c8d52484 100644 --- a/loader/include/Geode/platform/platform.hpp +++ b/loader/include/Geode/platform/platform.hpp @@ -3,6 +3,7 @@ #include "cplatform.h" #include #include +#include #if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__) #define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__) @@ -113,15 +114,16 @@ namespace geode { class PlatformID { public: + // todo in v4: make these flags and add archless Mac and Android as well as Desktop and Mobile and remove Linux enum { - Unknown = -1, - Windows, - MacIntel, - MacArm, - iOS, - Android32, - Android64, - Linux, + Unknown = -1, + Windows = 0, + MacIntel = 1, + MacArm = 2, + iOS = 3, + Android32 = 4, + Android64 = 5, + Linux = 6, }; using Type = decltype(Unknown); @@ -171,7 +173,14 @@ namespace geode { */ static GEODE_DLL bool coveredBy(const char* str, PlatformID t); static GEODE_DLL bool coveredBy(std::string const& str, PlatformID t); + /** + * Returns the list of platforms covered by this string name. For + * example, "android" would return both Android32 and Android64 + * todo in v4: deprecate this as the flagged version deals with this + */ + static GEODE_DLL std::vector getCovered(std::string_view str); + // todo in v4: this does not need to be constexpr in the header. dllexport it static constexpr char const* toString(Type lp) { switch (lp) { case Unknown: return "Unknown"; @@ -187,6 +196,7 @@ namespace geode { return "Undefined"; } + // todo in v4: this does not need to be constexpr in the header. dllexport it static constexpr char const* toShortString(Type lp, bool ignoreArch = false) { switch (lp) { case Unknown: return "unknown"; @@ -242,3 +252,13 @@ namespace std { #elif defined(GEODE_IS_ANDROID64) #define GEODE_PLATFORM_TARGET PlatformID::Android64 #endif + +// this is cross-platform so not duplicating it across the typeinfo_cast definitions +namespace geode::cast { + template + std::shared_ptr typeinfo_pointer_cast(std::shared_ptr const& r) noexcept { + // https://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast + auto p = typeinfo_cast::element_type*>(r.get()); + return std::shared_ptr(r, p); + } +} diff --git a/loader/include/Geode/platform/windows.hpp b/loader/include/Geode/platform/windows.hpp index 84c34891..ee22e0d3 100644 --- a/loader/include/Geode/platform/windows.hpp +++ b/loader/include/Geode/platform/windows.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace geode { struct PlatformInfo { diff --git a/loader/include/Geode/ui/ColorPickPopup.hpp b/loader/include/Geode/ui/ColorPickPopup.hpp index 4c0b5d4b..8a0dc68d 100644 --- a/loader/include/Geode/ui/ColorPickPopup.hpp +++ b/loader/include/Geode/ui/ColorPickPopup.hpp @@ -2,16 +2,23 @@ #include "Popup.hpp" #include "TextInput.hpp" -#include "Popup.hpp" - +#include "../loader/Event.hpp" #include namespace geode { + class ColorPickPopup; + class GEODE_DLL ColorPickPopupDelegate { public: virtual void updateColor(cocos2d::ccColor4B const& color) {} }; + // todo in v4: make this pimpl and maybe use events over the delegate? + // thing with events is that if you just filter via ColorPickPopup* it + // won't work unless you automatically detach the filter when closing the + // popup (otherwise opening another popup really quickly will just be + // allocated into the same memory and now the old filter is catching the + // new popup too) class GEODE_DLL ColorPickPopup : public Popup, public cocos2d::extension::ColorPickerDelegate, diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp index 12190338..53357a6f 100644 --- a/loader/include/Geode/utils/JsonValidation.hpp +++ b/loader/include/Geode/utils/JsonValidation.hpp @@ -77,7 +77,9 @@ namespace geode { struct JsonMaybeObject; struct JsonMaybeValue; - struct GEODE_DLL JsonMaybeSomething { + struct GEODE_DLL + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + JsonMaybeSomething { protected: JsonChecker& m_checker; matjson::Value& m_json; @@ -102,7 +104,9 @@ namespace geode { operator bool() const; }; - struct GEODE_DLL JsonMaybeValue : public JsonMaybeSomething { + struct GEODE_DLL + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + JsonMaybeValue : public JsonMaybeSomething { bool m_inferType = true; JsonMaybeValue( @@ -254,7 +258,9 @@ namespace geode { Iterator> items(); }; - struct GEODE_DLL JsonMaybeObject : JsonMaybeSomething { + struct + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + GEODE_DLL JsonMaybeObject : JsonMaybeSomething { std::set m_knownKeys; JsonMaybeObject( @@ -276,7 +282,9 @@ namespace geode { void checkUnknownKeys(); }; - struct GEODE_DLL JsonChecker { + struct + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + GEODE_DLL JsonChecker { std::variant m_result; matjson::Value& m_json; @@ -289,4 +297,208 @@ namespace geode { JsonMaybeValue root(std::string const& hierarchy); }; + class GEODE_DLL JsonExpectedValue final { + protected: + class Impl; + std::unique_ptr m_impl; + + JsonExpectedValue(); + JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key); + + static const char* matJsonTypeToString(matjson::Type ty); + + bool hasError() const; + void setError(std::string_view error); + + matjson::Value const& getJSONRef() const; + + template + void setError(fmt::format_string error, Args&&... args) { + this->setError(fmt::format(error, std::forward(args)...)); + } + + template + std::optional tryGet() { + if (this->hasError()) return std::nullopt; + if constexpr (std::is_same_v) { + return this->getJSONRef(); + } + else { + try { + if (this->getJSONRef().template is()) { + return this->getJSONRef().template as(); + } + else { + this->setError( + "unexpected type {}", + this->matJsonTypeToString(this->getJSONRef().type()) + ); + } + } + // matjson can throw variant exceptions too so you need to do this + catch(std::exception const& e) { + this->setError("unable to parse json: {}", e); + } + } + return std::nullopt; + } + + public: + JsonExpectedValue(matjson::Value const& value, std::string_view rootScopeName); + ~JsonExpectedValue(); + + JsonExpectedValue(JsonExpectedValue&&); + JsonExpectedValue& operator=(JsonExpectedValue&&); + JsonExpectedValue(JsonExpectedValue const&) = delete; + JsonExpectedValue& operator=(JsonExpectedValue const&) = delete; + + /** + * Get a copy of the underlying raw JSON value + */ + matjson::Value json() const; + /** + * Get the key name of this JSON value. If this is an array index, + * returns the index as a string. If this is the root object, + * returns the root scope name. + */ + std::string key() const; + + /** + * Check the type of this JSON value. Does not set an error. If an + * error is already set, always returns false + */ + bool is(matjson::Type type) const; + bool isNull() const; + bool isBool() const; + bool isNumber() const; + bool isString() const; + bool isArray() const; + bool isObject() const; + /** + * Asserts that this JSON value is of the specified type. If it is + * not, an error is set and all subsequent operations are no-ops + * @returns Itself + */ + JsonExpectedValue& assertIs(matjson::Type type); + JsonExpectedValue& assertIsNull(); + JsonExpectedValue& assertIsBool(); + JsonExpectedValue& assertIsNumber(); + JsonExpectedValue& assertIsString(); + JsonExpectedValue& assertIsArray(); + JsonExpectedValue& assertIsObject(); + /** + * Asserts that this JSON value is one of a list of specified types + * @returns Itself + */ + JsonExpectedValue& assertIs(std::initializer_list type); + + // -- Dealing with values -- + + template + T get(T const& defaultValue = T()) { + if (auto v = this->template tryGet()) { + return *std::move(v); + } + return defaultValue; + } + template + JsonExpectedValue& into(T& value) { + if (auto v = this->template tryGet()) { + value = *std::move(v); + } + return *this; + } + template + JsonExpectedValue& into(std::optional& value) { + if (auto v = this->template tryGet()) { + value.emplace(*std::move(v)); + } + 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()) { + if (!predicate(*v)) { + this->setError("json value is not {}", name); + } + } + 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 -- + + /** + * Check if this object has an optional key. Asserts that this JSON + * value is an object. If the key doesn't exist, returns a + * `JsonExpectValue` that does nothing + * @returns The key, which is a no-op value if it didn't exist + */ + JsonExpectedValue has(std::string_view key); + /** + * Check if this object has an optional key. Asserts that this JSON + * value is an object. If the key doesn't exist, sets an error and + * returns a `JsonExpectValue` that does nothing + * @returns The key, which is a no-op value if it didn't exist + */ + JsonExpectedValue needs(std::string_view key); + /** + * Asserts that this JSON value is an object. Get all object + * properties + */ + std::vector> properties(); + /** + * Asserts that this JSON value is an object. Logs unknown keys to + * the console as warnings + */ + void checkUnknownKeys(); + + // -- Dealing with arrays -- + + /** + * Asserts that this JSON value is an array. Returns the length of + * the array, or 0 on error + */ + size_t length(); + /** + * Asserts that this JSON value is an array. Returns the value at + * the specified index. If there is no value at that index, sets an + * error + */ + JsonExpectedValue at(size_t index); + /** + * Asserts that this JSON value is an array. Returns the array items + * @warning The old JsonChecker used `items` for iterating object + * properties - on this new API that function is called `properties`! + */ + std::vector items(); + + operator bool() const; + + Result<> ok(); + template + Result ok(T value) { + auto ok = this->ok(); + if (!ok) { + return Err(ok.unwrapErr()); + } + return Ok(std::forward(value)); + } + }; + GEODE_DLL JsonExpectedValue checkJson(matjson::Value const& json, std::string_view rootScopeName); } diff --git a/loader/include/Geode/utils/Task.hpp b/loader/include/Geode/utils/Task.hpp index 38868bf1..d6691257 100644 --- a/loader/include/Geode/utils/Task.hpp +++ b/loader/include/Geode/utils/Task.hpp @@ -390,6 +390,15 @@ namespace geode { return m_handle == nullptr; } + /** + * Create a new Task that is immediately cancelled + * @param name The name of the Task; used for debugging + */ + static Task cancelled(std::string_view const name = "") { + auto task = Task(Handle::create(name)); + Task::cancel(task.m_handle); + return task; + } /** * Create a new Task that immediately finishes with the given * value diff --git a/loader/include/Geode/utils/function.hpp b/loader/include/Geode/utils/function.hpp new file mode 100644 index 00000000..855f1951 --- /dev/null +++ b/loader/include/Geode/utils/function.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +namespace geode::utils::function { + namespace detail { + template + struct ImplExtract; + + template + struct ImplExtract { + using Type = R(A...); + using Return = R; + using Args = std::tuple; + static constexpr std::size_t ARG_COUNT = std::tuple_size_v; + }; + template + struct ImplExtract { + using Type = R(A...); + using Return = R; + using Args = std::tuple; + static constexpr std::size_t ARG_COUNT = std::tuple_size_v; + }; + template + struct ImplExtract { + using Type = R(A...); + using Class = C; + using Return = R; + using Args = std::tuple; + static constexpr std::size_t ARG_COUNT = std::tuple_size_v; + }; + template + struct ImplExtract { + using Type = R(A...); + using Class = C; + using Return = R; + using Args = std::tuple; + static constexpr std::size_t ARG_COUNT = std::tuple_size_v; + }; + template + requires requires { &F::operator(); } + struct ImplExtract : public ImplExtract {}; + + template + using Extract = ImplExtract>; + } + + template + using FunctionInfo = detail::Extract; + + template + using Return = typename detail::Extract::Return; + + template + using Args = typename detail::Extract::Args; + + template + using Arg = std::tuple_element_t::Args>; +} diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp index d23b3c63..c174658c 100644 --- a/loader/include/Geode/utils/general.hpp +++ b/loader/include/Geode/utils/general.hpp @@ -17,13 +17,22 @@ namespace geode { using ByteVector = std::vector; + // todo in v4: remove this template + [[deprecated("Use geode::toBytes instead")]] ByteVector toByteArray(T const& a) { ByteVector out; out.resize(sizeof(T)); std::memcpy(out.data(), &a, sizeof(T)); return out; } + template + ByteVector toBytes(T const& a) { + ByteVector out; + out.resize(sizeof(T)); + std::memcpy(out.data(), &a, sizeof(T)); + return out; + } namespace utils { // helper for std::visit diff --git a/loader/resources/file.png b/loader/resources/file.png new file mode 100644 index 00000000..a1fc8d29 Binary files /dev/null and b/loader/resources/file.png differ diff --git a/loader/resources/mod.json.in b/loader/resources/mod.json.in index dcbe69de..c4915538 100644 --- a/loader/resources/mod.json.in +++ b/loader/resources/mod.json.in @@ -64,13 +64,6 @@ } }, "settings": { - "show-platform-console": { - "type": "bool", - "default": false, - "name": "Show Platform Console", - "description": "Show the native console (if one exists). This setting is meant for developers", - "platforms": ["win", "mac"] - }, "auto-check-updates": { "type": "bool", "default": true, @@ -83,6 +76,24 @@ "name": "Disable Crash Popup", "description": "Disables the popup at startup asking if you'd like to send a bug report; intended for developers" }, + "enable-geode-theme": { + "type": "bool", + "default": true, + "name": "Enable Geode-Themed Colors", + "description": "When enabled, the Geode menu has a Geode-themed color scheme. This does not affect any other menus!" + }, + "developer-title": { + "type": "title", + "name": "Developer Settings" + }, + "show-platform-console": { + "type": "bool", + "default": false, + "name": "Show Platform Console", + "description": "Show the native console (if one exists). This setting is meant for developers", + "platforms": ["win", "mac"], + "requires-restart": true + }, "server-cache-size-limit": { "type": "int", "default": 20, @@ -90,12 +101,6 @@ "max": 100, "name": "Server Cache Size Limit", "description": "Limits the size of the cache used for loading mods. Higher values result in higher memory usage." - }, - "enable-geode-theme": { - "type": "bool", - "default": true, - "name": "Enable Geode-Themed Colors", - "description": "When enabled, the Geode menu has a Geode-themed color scheme. This does not affect any other menus!" } }, "issues": { diff --git a/loader/src/cocos2d-ext/AxisLayout.cpp b/loader/src/cocos2d-ext/AxisLayout.cpp index 02d8b285..cedded25 100644 --- a/loader/src/cocos2d-ext/AxisLayout.cpp +++ b/loader/src/cocos2d-ext/AxisLayout.cpp @@ -823,6 +823,13 @@ CCSize AxisLayout::getSizeHint(CCNode* on) const { axis.crossLength = cross; } } + if (auto l = m_impl->m_autoGrowAxisMinLength) { + length = std::max(length, *l); + } + // No overflow + else { + length = std::min(length, nodeAxis(on, m_impl->m_axis, 1.f).axisLength); + } if (!m_impl->m_allowCrossAxisOverflow) { cross = nodeAxis(on, m_impl->m_axis, 1.f).crossLength; } diff --git a/loader/src/load.cpp b/loader/src/load.cpp index cfa6e0e4..5c109365 100644 --- a/loader/src/load.cpp +++ b/loader/src/load.cpp @@ -185,6 +185,7 @@ int geodeEntry(void* platformData) { log::popNest(); // download and install new loader update in the background + if (Mod::get()->getSettingValue("auto-check-updates")) { log::info("Starting loader update check"); updater::checkForLoaderUpdates(); diff --git a/loader/src/load.mm b/loader/src/load.mm index 55756495..5a9fd07d 100644 --- a/loader/src/load.mm +++ b/loader/src/load.mm @@ -3,10 +3,10 @@ #ifdef GEODE_IS_MACOS +#include #include #include #include -#include #include bool safeModeCheck() { diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 55298464..e5223914 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -9,7 +9,7 @@ Loader::Loader() : m_impl(new Impl) {} Loader::~Loader() {} Loader* Loader::get() { - static auto g_geode = new Loader; + static auto g_geode = new Loader(); return g_geode; } diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 66170f69..a4b71515 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -156,15 +156,25 @@ bool Mod::hasSetting(std::string_view const key) const { } std::optional Mod::getSettingDefinition(std::string_view const key) const { - return m_impl->getSettingDefinition(key); + return m_impl->m_settings->getLegacyDefinition(std::string(key)); } SettingValue* Mod::getSetting(std::string_view const key) const { - return m_impl->getSetting(key); + return m_impl->m_settings->getLegacy(std::string(key)).get(); +} + +std::shared_ptr Mod::getSettingV3(std::string_view const key) const { + return m_impl->m_settings->get(std::string(key)); } void Mod::registerCustomSetting(std::string_view const key, std::unique_ptr value) { - return m_impl->registerCustomSetting(key, std::move(value)); + auto reg = m_impl->m_settings->registerLegacyCustomSetting(key, std::move(value)); + if (!reg) { + log::error("Unable to register custom setting: {}", reg.unwrapErr()); + } +} +Result<> Mod::registerCustomSettingType(std::string_view type, SettingGenerator generator) { + return m_impl->m_settings->registerCustomSettingType(type, generator); } std::vector Mod::getLaunchArgumentNames() const { diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 43b3271d..12e7755c 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -53,7 +53,7 @@ Result<> Mod::Impl::setup() { // always create temp dir for all mods, even if disabled, so resources can be loaded GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp dir: {error}")); - this->setupSettings(); + m_settings = std::make_unique(m_metadata); auto loadRes = this->loadData(); if (!loadRes) { log::warn("Unable to load data for \"{}\": {}", m_metadata.getID(), loadRes.unwrapErr()); @@ -182,49 +182,11 @@ Result<> Mod::Impl::loadData() { // Check if settings exist auto settingPath = m_saveDirPath / "settings.json"; if (std::filesystem::exists(settingPath)) { - GEODE_UNWRAP_INTO(auto settingData, utils::file::readString(settingPath)); - // parse settings.json - std::string error; - auto res = matjson::parse(settingData, error); - if (error.size() > 0) { - return Err("Unable to parse settings.json: " + error); - } - auto json = res.value(); - - JsonChecker checker(json); - auto root = checker.root(fmt::format("[{}/settings.json]", this->getID())); - + GEODE_UNWRAP_INTO(auto json, utils::file::readJson(settingPath)); m_savedSettingsData = json; - - for (auto& [key, value] : root.items()) { - // check if this is a known setting - if (auto setting = this->getSetting(key)) { - // load its value - if (!setting->load(value.json())) { - log::logImpl( - Severity::Error, - m_self, - "{}: Unable to load value for setting \"{}\"", - m_metadata.getID(), - key - ); - } - } - else { - if (auto definition = this->getSettingDefinition(key)) { - // Found a definition for this setting, it's most likely a custom setting - // Don't warn it, as it's expected to be loaded by the mod - } - else { - log::logImpl( - Severity::Warning, - m_self, - "Encountered unknown setting \"{}\" while loading " - "settings", - key - ); - } - } + auto load = m_settings->load(json); + if (!load) { + log::warn("Unable to load settings: {}", load.unwrapErr()); } } @@ -253,103 +215,45 @@ Result<> Mod::Impl::saveData() { return Ok(); } - // saveData is expected to be synchronous, and always called from GD thread - ModStateEvent(m_self, ModEventType::DataSaved).post(); - // Data saving should be fully fail-safe - - std::unordered_set coveredSettings; - - // Settings - matjson::Value json = matjson::Object(); - for (auto& [key, value] : m_settings) { - coveredSettings.insert(key); - if (!value->save(json[key])) { - log::error("Unable to save setting \"{}\"", key); - } - } - - // if some settings weren't provided a custom settings handler (for example, + // If some settings weren't provided a custom settings handler (for example, // the mod was not loaded) then make sure to save their previous state in // order to not lose data - log::debug("Check covered"); if (!m_savedSettingsData.is_object()) { m_savedSettingsData = matjson::Object(); } - for (auto& [key, value] : m_savedSettingsData.as_object()) { - log::debug("Check if {} is saved", key); - if (!coveredSettings.contains(key)) { - json[key] = value; - } - } + matjson::Value json = m_savedSettingsData; + m_settings->save(json); - std::string settingsStr = json.dump(); - std::string savedStr = m_saved.dump(); - - auto res = utils::file::writeString(m_saveDirPath / "settings.json", settingsStr); + auto res = utils::file::writeString(m_saveDirPath / "settings.json", json.dump()); if (!res) { log::error("Unable to save settings: {}", res.unwrapErr()); } - - auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", savedStr); + auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump()); if (!res2) { log::error("Unable to save values: {}", res2.unwrapErr()); } + // saveData is expected to be synchronous, and always called from GD thread + ModStateEvent(m_self, ModEventType::DataSaved).post(); + return Ok(); } -void Mod::Impl::setupSettings() { - for (auto& [key, sett] : m_metadata.getSettings()) { - if (auto value = sett.createDefaultValue()) { - m_settings.emplace(key, std::move(value)); - } - } -} - -void Mod::Impl::registerCustomSetting(std::string_view const key, std::unique_ptr value) { - auto keystr = std::string(key); - if (!m_settings.count(keystr)) { - // load data - if (m_savedSettingsData.contains(key)) { - value->load(m_savedSettingsData[key]); - } - m_settings.emplace(keystr, std::move(value)); - } -} - bool Mod::Impl::hasSettings() const { - return m_metadata.getSettings().size(); + return m_metadata.getSettingsV3().size(); } std::vector Mod::Impl::getSettingKeys() const { std::vector keys; - for (auto& [key, _] : m_metadata.getSettings()) { + for (auto& [key, _] : m_metadata.getSettingsV3()) { keys.push_back(key); } return keys; } -std::optional Mod::Impl::getSettingDefinition(std::string_view const key) const { - for (auto& setting : m_metadata.getSettings()) { - if (setting.first == key) { - return setting.second; - } - } - return std::nullopt; -} - -SettingValue* Mod::Impl::getSetting(std::string_view const key) const { - auto keystr = std::string(key); - - if (m_settings.count(keystr)) { - return m_settings.at(keystr).get(); - } - return nullptr; -} - bool Mod::Impl::hasSetting(std::string_view const key) const { - for (auto& setting : m_metadata.getSettings()) { + for (auto& setting : m_metadata.getSettingsV3()) { if (setting.first == key) { return true; } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index 2346fd10..f33bc0ad 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -4,6 +4,7 @@ #include "ModPatch.hpp" #include #include +#include namespace geode { class Mod::Impl { @@ -48,9 +49,9 @@ namespace geode { */ matjson::Value m_saved = matjson::Object(); /** - * Setting values + * Setting values. This is behind unique_ptr for interior mutability */ - std::unordered_map> m_settings; + std::unique_ptr m_settings = nullptr; /** * Settings save data. Stored for efficient loading of custom settings */ @@ -74,6 +75,8 @@ namespace geode { Impl(Mod* self, ModMetadata const& metadata); ~Impl(); + Impl(Impl const&) = delete; + Impl(Impl&&) = delete; Result<> setup(); @@ -83,8 +86,6 @@ namespace geode { // called on a separate thread Result<> unzipGeodeFile(ModMetadata metadata); - void setupSettings(); - std::string getID() const; std::string getName() const; std::vector getDevelopers() const; @@ -116,9 +117,6 @@ namespace geode { bool hasSettings() const; std::vector getSettingKeys() const; bool hasSetting(std::string_view const key) const; - std::optional getSettingDefinition(std::string_view const key) const; - SettingValue* getSetting(std::string_view const key) const; - void registerCustomSetting(std::string_view const key, std::unique_ptr value); std::string getLaunchArgumentName(std::string_view const name) const; std::vector getLaunchArgumentNames() const; diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index d3ebf8e5..460398ea 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -125,45 +125,28 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } catch (...) { } - JsonChecker checker(impl->m_rawJSON); - auto root = checker.root(checkerRoot).obj(); - + auto root = checkJson(impl->m_rawJSON, checkerRoot); root.needs("geode").into(impl->m_geodeVersion); - root.addKnownKey("gd"); - - // Check GD version - // (use rawJson because i dont like JsonMaybeValue) - if (rawJson.contains("gd")) { - std::string ver; - if (rawJson["gd"].is_object()) { - auto key = PlatformID::toShortString(GEODE_PLATFORM_TARGET, true); - if (rawJson["gd"].contains(key) && rawJson["gd"][key].is_string()) - ver = rawJson["gd"][key].as_string(); - } else if (rawJson["gd"].is_string()) { + + if (auto gd = root.needs("gd")) { + // In the future when we get rid of support for string format just + // change all of this to the gd.needs(...) stuff + gd.assertIs({ matjson::Type::Object, matjson::Type::String }); + if (gd.isObject()) { + gd.needs(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH) + .mustBe("a valid gd version", [](auto const& str) { + return str == "*" || numFromString(str).isOk(); + }) + .into(impl->m_gdVersion); + } + else if (gd.isString()) { impl->m_softInvalidReason = "mod.json uses old syntax"; - goto dontCheckVersion; - } else { - return Err("[mod.json] has invalid target GD version"); } - if (ver.empty()) { - // this will show an error later on, but will at least load the rest of the metadata - ver = "0.000"; - } - if (ver != "*") { - auto res = numFromString(ver); - if (res.isErr()) { - return Err("[mod.json] has invalid target GD version"); - } - impl->m_gdVersion = ver; - } - } else { - return Err("[mod.json] is missing target GD version"); } - dontCheckVersion: + constexpr auto ID_REGEX = "[a-z0-9\\-_]+\\.[a-z0-9\\-_]+"; root.needs("id") - // todo: make this use validateID in full 2.0.0 release - .validate(MiniFunction(&ModMetadata::Impl::validateOldID)) + .mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID) .into(impl->m_id); // if (!isDeprecatedIDForm(impl->m_id)) { @@ -180,7 +163,7 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs if (root.has("developer")) { return Err("[mod.json] can not have both \"developer\" and \"developers\" specified"); } - for (auto& dev : root.needs("developers").iterate()) { + for (auto& dev : root.needs("developers").items()) { impl->m_developers.push_back(dev.template get()); } } @@ -205,11 +188,9 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs }); } - for (auto& dep : root.has("dependencies").iterate()) { - auto obj = dep.obj(); - - bool onThisPlatform = !obj.has("platforms"); - for (auto& plat : obj.has("platforms").iterate()) { + for (auto& dep : root.has("dependencies").items()) { + bool onThisPlatform = !dep.has("platforms"); + for (auto& plat : dep.has("platforms").items()) { if (PlatformID::coveredBy(plat.get(), GEODE_PLATFORM_TARGET)) { onThisPlatform = true; } @@ -219,11 +200,10 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } Dependency dependency; - // todo: make this use validateID in full 2.0.0 release - obj.needs("id").validate(MiniFunction(&ModMetadata::Impl::validateOldID)).into(dependency.id); - obj.needs("version").into(dependency.version); - obj.has("importance").into(dependency.importance); - obj.checkUnknownKeys(); + dep.needs("id").mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID).into(dependency.id); + dep.needs("version").into(dependency.version); + dep.has("importance").into(dependency.importance); + dep.checkUnknownKeys(); if ( dependency.version.getComparison() != VersionCompare::MoreEq && @@ -247,24 +227,30 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs impl->m_dependencies.push_back(dependency); } - for (auto& incompat : root.has("incompatibilities").iterate()) { - auto obj = incompat.obj(); + for (auto& incompat : root.has("incompatibilities").items()) { + bool onThisPlatform = !incompat.has("platforms"); + for (auto& plat : incompat.has("platforms").items()) { + if (PlatformID::coveredBy(plat.get(), GEODE_PLATFORM_TARGET)) { + onThisPlatform = true; + } + } + if (!onThisPlatform) { + continue; + } Incompatibility incompatibility; - obj.needs("id").validate(MiniFunction(&ModMetadata::Impl::validateOldID)).into(incompatibility.id); - obj.needs("version").into(incompatibility.version); - obj.has("importance").into(incompatibility.importance); - obj.checkUnknownKeys(); - + incompat.needs("id").mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID).into(incompatibility.id); + incompat.needs("version").into(incompatibility.version); + incompat.has("importance").into(incompatibility.importance); + incompat.checkUnknownKeys(); impl->m_incompatibilities.push_back(incompatibility); } - for (auto& [key, value] : root.has("settings").items()) { + for (auto& [key, value] : root.has("settings").properties()) { // Skip settings not on this platform - if (value.template is()) { - auto obj = value.obj(); - bool onThisPlatform = !obj.has("platforms"); - for (auto& plat : obj.has("platforms").iterate()) { + if (value.is(matjson::Type::Object)) { + bool onThisPlatform = !value.has("platforms"); + for (auto& plat : value.has("platforms").items()) { if (PlatformID::coveredBy(plat.get(), GEODE_PLATFORM_TARGET)) { onThisPlatform = true; } @@ -273,25 +259,23 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs continue; } } - - GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value)); - impl->m_settings.emplace_back(key, sett); + impl->m_settings.emplace_back(key, value.json()); } - if (auto resources = root.has("resources").obj()) { - for (auto& [key, _] : resources.has("spritesheets").items()) { + if (auto resources = root.has("resources")) { + for (auto& [key, _] : resources.has("spritesheets").properties()) { impl->m_spritesheets.push_back(impl->m_id + "/" + key); } } - if (auto issues = root.has("issues").obj()) { + if (auto issues = root.has("issues")) { IssuesInfo issuesInfo; issues.needs("info").into(issuesInfo.info); - issues.has("url").intoAs(issuesInfo.url); + issues.has("url").into(issuesInfo.url); impl->m_issues = issuesInfo; } - if (auto links = root.has("links").obj()) { + if (auto links = root.has("links")) { links.has("homepage").into(info.getLinksMut().getImpl()->m_homepage); links.has("source").into(info.getLinksMut().getImpl()->m_source); links.has("community").into(info.getLinksMut().getImpl()->m_community); @@ -299,19 +283,16 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } // Tags. Actual validation is done when interacting with the server in the UI - for (auto& tag : root.has("tags").iterate()) { + for (auto& tag : root.has("tags").items()) { impl->m_tags.insert(tag.template get()); } // with new cli, binary name is always mod id impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION; - if (checker.isError()) { - return Err(checker.getError()); - } root.checkUnknownKeys(); - return Ok(info); + return root.ok(info); } Result ModMetadata::Impl::create(ModJson const& json) { @@ -542,6 +523,18 @@ std::vector ModMetadata::getSpritesheets() const { return m_impl->m_spritesheets; } std::vector> ModMetadata::getSettings() const { + std::vector> res; + for (auto [key, sett] : m_impl->m_settings) { + auto checker = JsonChecker(sett); + auto value = checker.root(""); + auto legacy = Setting::parse(key, m_impl->m_id, value); + if (!checker.isError() && legacy.isOk()) { + res.push_back(std::make_pair(key, *legacy)); + } + } + return res; +} +std::vector> ModMetadata::getSettingsV3() const { return m_impl->m_settings; } std::unordered_set ModMetadata::getTags() const { @@ -561,7 +554,7 @@ VersionInfo ModMetadata::getGeodeVersion() const { return m_impl->m_geodeVersion; } Result<> ModMetadata::checkGameVersion() const { - if (!m_impl->m_gdVersion.empty()) { + if (!m_impl->m_gdVersion.empty() && m_impl->m_gdVersion != "*") { auto const ver = m_impl->m_gdVersion; auto res = numFromString(ver); @@ -643,6 +636,10 @@ void ModMetadata::setSpritesheets(std::vector const& value) { m_impl->m_spritesheets = value; } void ModMetadata::setSettings(std::vector> const& value) { + // intentionally no-op because no one is supposed to be using this + // without subscribing to "internals are not stable" mentality +} +void ModMetadata::setSettings(std::vector> const& value) { m_impl->m_settings = value; } void ModMetadata::setTags(std::unordered_set const& value) { diff --git a/loader/src/loader/ModMetadataImpl.hpp b/loader/src/loader/ModMetadataImpl.hpp index b47d092c..fbf97448 100644 --- a/loader/src/loader/ModMetadataImpl.hpp +++ b/loader/src/loader/ModMetadataImpl.hpp @@ -4,6 +4,7 @@ #include #include #include +#include using namespace geode::prelude; @@ -36,7 +37,7 @@ namespace geode { std::vector m_dependencies; std::vector m_incompatibilities; std::vector m_spritesheets; - std::vector> m_settings; + std::vector> m_settings; std::unordered_set m_tags; bool m_needsEarlyLoad = false; bool m_isAPI = false; diff --git a/loader/src/loader/ModSettingsManager.cpp b/loader/src/loader/ModSettingsManager.cpp new file mode 100644 index 00000000..83cd04d3 --- /dev/null +++ b/loader/src/loader/ModSettingsManager.cpp @@ -0,0 +1,253 @@ +#include +#include +#include "ModImpl.hpp" + +using namespace geode::prelude; + +// All setting type generators are put in a shared pool for two reasons: +// #1 no need to duplicate the built-in settings between all mods +// #2 easier lookup of custom settings if a mod uses another mod's custom setting type + +class SharedSettingTypesPool final { +private: + std::unordered_map m_types; + + SharedSettingTypesPool() : m_types({ + // todo in v4: remove this + { "custom", &LegacyCustomSettingV3::parse }, + { "title", &TitleSettingV3::parse }, + { "bool", &BoolSettingV3::parse }, + { "int", &IntSettingV3::parse }, + { "float", &FloatSettingV3::parse }, + { "string", &StringSettingV3::parse }, + { "file", &FileSettingV3::parse }, + { "folder", &FileSettingV3::parse }, + { "path", &FileSettingV3::parse }, + { "rgb", &Color3BSettingV3::parse }, + { "color", &Color3BSettingV3::parse }, + { "rgba", &Color4BSettingV3::parse }, + }) {} + +public: + static SharedSettingTypesPool& get() { + static auto inst = SharedSettingTypesPool(); + return inst; + } + + Result<> add(std::string_view modID, std::string_view type, SettingGenerator generator) { + // Limit type to just [a-z0-9\-]+ + if (type.empty() || !std::all_of(type.begin(), type.end(), +[](char c) { + return + ('a' <= c && c <= 'z') || + ('0' <= c && c <= '9') || + (c == '-'); + })) { + return Err("Custom setting types must match the regex [a-z0-9\\-]+"); + } + auto full = fmt::format("{}/{}", modID, type); + if (m_types.contains(full)) { + return Err("Type \"{}\" has already been registered for mod {}", type, modID); + } + m_types.emplace(full, generator); + return Ok(); + } + std::optional find(std::string_view modID, std::string_view fullType) { + // Find custom settings via namespaced lookup + if (fullType.starts_with("custom:")) { + auto full = std::string(fullType.substr(fullType.find(':') + 1)); + // If there's no mod ID in the type name, use the current mod's ID + if (full.find('/') == std::string_view::npos) { + full = fmt::format("{}/{}", modID, full); + } + if (m_types.contains(full)) { + return m_types.at(full); + } + } + // Otherwise find a built-in setting + else { + auto full = std::string(fullType); + if (m_types.contains(full)) { + return m_types.at(full); + } + } + // Return null if nothing was found + return std::nullopt; + } +}; + +class ModSettingsManager::Impl final { +public: + struct SettingInfo final { + std::string type; + matjson::Value json; + std::shared_ptr v3 = nullptr; + // todo: remove in v4 + std::shared_ptr legacy = nullptr; + }; + std::string modID; + std::unordered_map settings; + bool restartRequired = false; + + void createSettings() { + for (auto& [key, setting] : settings) { + if (setting.v3) { + continue; + } + auto gen = SharedSettingTypesPool::get().find(modID, setting.type); + // The type was not found, meaning it probably hasn't been registered yet + if (!gen) { + continue; + } + if (auto v3 = (*gen)(key, modID, setting.json)) { + setting.v3 = *v3; + } + else { + log::error( + "Unable to parse setting '{}' for mod {}: {}", + key, modID, v3.unwrapErr() + ); + } + } + } +}; + +ModSettingsManager* ModSettingsManager::from(Mod* mod) { + if (!mod) return nullptr; + return ModImpl::getImpl(mod)->m_settings.get(); +} + +ModSettingsManager::ModSettingsManager(ModMetadata const& metadata) + : m_impl(std::make_unique()) +{ + m_impl->modID = metadata.getID(); + for (auto const& [key, json] : metadata.getSettingsV3()) { + auto setting = Impl::SettingInfo(); + setting.json = json; + auto root = checkJson(json, "setting"); + root.needs("type").into(setting.type); + if (root) { + if (setting.type == "custom") { + log::warn( + "Setting \"{}\" in mod {} has the old \"custom\" type - " + "this type has been deprecated and will be removed in Geode v4.0.0. " + "Use the new \"custom:type-name-here\" syntax for defining custom " + "setting types - see more in INSERT TUTORIAL HERE", + key, m_impl->modID + ); + } + m_impl->settings.emplace(key, setting); + } + else { + log::error("Setting '{}' in mod {} is missing type", key, m_impl->modID); + } + } + m_impl->createSettings(); +} +ModSettingsManager::~ModSettingsManager() {} +ModSettingsManager::ModSettingsManager(ModSettingsManager&&) = default; + +void ModSettingsManager::markRestartRequired() { + m_impl->restartRequired = true; +} + +Result<> ModSettingsManager::registerCustomSettingType(std::string_view type, SettingGenerator generator) { + GEODE_UNWRAP(SharedSettingTypesPool::get().add(m_impl->modID, type, generator)); + m_impl->createSettings(); + return Ok(); +} +Result<> ModSettingsManager::registerLegacyCustomSetting(std::string_view key, std::unique_ptr&& ptr) { + auto id = std::string(key); + if (!m_impl->settings.count(id)) { + return Err("No such setting '{}' in mod {}", id, m_impl->modID); + } + auto& sett = m_impl->settings.at(id); + if (auto custom = typeinfo_pointer_cast(sett.v3)) { + if (!custom->getValue()) { + custom->setValue(std::move(ptr)); + } + else { + return Err("Setting '{}' in mod {} has already been registed", id, m_impl->modID); + } + } + else { + return Err("Setting '{}' in mod {} is not a legacy custom setting", id, m_impl->modID); + } + return Ok(); +} + +Result<> ModSettingsManager::load(matjson::Value const& json) { + auto root = checkJson(json, "Settings"); + for (auto const& [key, value] : root.properties()) { + if (m_impl->settings.contains(key)) { + auto& sett = m_impl->settings.at(key); + if (!sett.v3) continue; + try { + if (!sett.v3->load(value.json())) { + log::error("Unable to load setting '{}' for mod {}", key, m_impl->modID); + } + } + // matjson::JsonException doesn't catch all possible json errors + catch(std::exception const& e) { + log::error("Unable to load setting '{}' for mod {} (JSON exception): {}", key, m_impl->modID, e.what()); + } + } + } + return Ok(); +} +void ModSettingsManager::save(matjson::Value& json) { + for (auto& [key, sett] : m_impl->settings) { + if (!sett.v3) { + continue; + } + // Store the value in an intermediary so if `save` fails the existing + // value loaded from disk isn't overwritten + matjson::Value value; + try { + if (sett.v3->save(value)) { + json[key] = value; + } + else { + log::error("Unable to save setting '{}' for mod {}", key, m_impl->modID); + } + } + catch(matjson::JsonException const& e) { + log::error("Unable to save setting '{}' for mod {} (JSON exception): {}", key, m_impl->modID, e.what()); + } + } +} + +std::shared_ptr ModSettingsManager::get(std::string_view key) { + auto id = std::string(key); + return m_impl->settings.count(id) ? m_impl->settings.at(id).v3 : nullptr; +} +std::shared_ptr ModSettingsManager::getLegacy(std::string_view key) { + auto id = std::string(key); + if (!m_impl->settings.count(id)) { + return nullptr; + } + auto& info = m_impl->settings.at(id); + // If this setting has alreay been given a legacy interface, give that + if (info.legacy) { + return info.legacy; + } + // Uninitialized settings are null + if (!info.v3) { + return nullptr; + } + // Generate new legacy interface + if (auto legacy = info.v3->convertToLegacyValue()) { + info.legacy.swap(*legacy); + return info.legacy; + } + return nullptr; +} +std::optional ModSettingsManager::getLegacyDefinition(std::string_view key) { + if (auto s = this->get(key)) { + return s->convertToLegacy(); + } + return std::nullopt; +} + +bool ModSettingsManager::restartRequired() const { + return m_impl->restartRequired; +} diff --git a/loader/src/loader/Setting.cpp b/loader/src/loader/Setting.cpp index 35434110..f9562eb2 100644 --- a/loader/src/loader/Setting.cpp +++ b/loader/src/loader/Setting.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -279,10 +280,10 @@ std::string SettingValue::getModID() const { } void SettingValue::valueChanged() { - // this is actually p neat because now if the mod gets disabled this wont - // post the event so that side-effect is automatically handled :3 if (auto mod = Loader::get()->getLoadedMod(m_modID)) { - SettingChangedEvent(mod, this).post(); + if (auto sett = mod->getSettingV3(m_key)) { + sett->markChanged(); + } } } @@ -296,29 +297,49 @@ void SettingValue::valueChanged() { return type_##SettingNode::create(this, width); \ } \ template<> \ + typename GeodeSettingValue::ValueType \ + GeodeSettingValue::getValue() const { \ + using S = typename SettingTypeForValueType::SettingType; \ + if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \ + if (auto setting = typeinfo_pointer_cast(mod->getSettingV3(m_key))) {\ + return setting->getValue(); \ + } \ + } \ + return m_value; \ + } \ + template<> \ void GeodeSettingValue< \ type_##Setting \ >::setValue(ValueType const& value) { \ - m_value = this->toValid(value).first; \ - this->valueChanged(); \ + using S = typename SettingTypeForValueType::SettingType; \ + if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \ + if (auto setting = typeinfo_pointer_cast(mod->getSettingV3(m_key))) {\ + return setting->setValue(value); \ + } \ + } \ } \ template<> \ Result<> GeodeSettingValue< \ type_##Setting \ >::validate(ValueType const& value) const { \ - auto reason = this->toValid(value).second; \ - if (reason.has_value()) { \ - return Err(static_cast(reason.value())); \ - } \ - return Ok(); \ + using S = typename SettingTypeForValueType::SettingType; \ + if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \ + if (auto setting = typeinfo_pointer_cast(mod->getSettingV3(m_key))) {\ + return setting->isValid(value); \ + } \ + } \ + return Ok(); \ } \ template<> \ typename type_##Setting::ValueType SettingValueSetter< \ typename type_##Setting::ValueType \ >::get(SettingValue* setting) { \ - if (auto b = typeinfo_cast(setting)) { \ - return b->getValue(); \ - } \ + using S = typename SettingTypeForValueType::SettingType; \ + if (auto mod = Loader::get()->getInstalledMod(setting->getModID())) { \ + if (auto sett = typeinfo_pointer_cast(mod->getSettingV3(setting->getKey()))) { \ + return sett->getValue(); \ + } \ + } \ return typename type_##Setting::ValueType(); \ } \ template<> \ @@ -328,9 +349,12 @@ void SettingValue::valueChanged() { SettingValue* setting, \ typename type_##Setting::ValueType const& value \ ) { \ - if (auto b = typeinfo_cast(setting)) { \ - b->setValue(value); \ - } \ + using S = typename SettingTypeForValueType::SettingType; \ + if (auto mod = Loader::get()->getInstalledMod(setting->getModID())) { \ + if (auto sett = typeinfo_pointer_cast(mod->getSettingV3(setting->getKey()))) { \ + return sett->setValue(value); \ + } \ + } \ } #define IMPL_TO_VALID(type_) \ diff --git a/loader/src/loader/SettingNodeV3.cpp b/loader/src/loader/SettingNodeV3.cpp new file mode 100644 index 00000000..10ecfc15 --- /dev/null +++ b/loader/src/loader/SettingNodeV3.cpp @@ -0,0 +1,712 @@ +#include "SettingNodeV3.hpp" +#include +#include +#include + +class SettingNodeSizeChangeEventV3::Impl final { +public: + SettingNodeV3* node; +}; + +SettingNodeSizeChangeEventV3::SettingNodeSizeChangeEventV3(SettingNodeV3* node) + : m_impl(std::make_shared()) +{ + m_impl->node = node; +} +SettingNodeSizeChangeEventV3::~SettingNodeSizeChangeEventV3() = default; + +SettingNodeV3* SettingNodeSizeChangeEventV3::getNode() const { + return m_impl->node; +} + +class SettingNodeValueChangeEventV3::Impl final { +public: + SettingNodeV3* node; + bool commit = false; +}; + +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; +} + +class SettingNodeV3::Impl final { +public: + std::shared_ptr setting; + CCLayerColor* bg; + CCLabelBMFont* nameLabel; + CCMenu* nameMenu; + CCMenu* buttonMenu; + CCMenuItemSpriteExtra* resetButton; + CCLabelBMFont* statusLabel; + ccColor4B bgColor = ccc4(0, 0, 0, 0); + bool committed = false; +}; + +bool SettingNodeV3::init(std::shared_ptr setting, float width) { + if (!CCNode::init()) + return false; + + // note: setting may be null due to UnresolvedCustomSettingNodeV3 + + m_impl = std::make_shared(); + m_impl->setting = setting; + + m_impl->bg = CCLayerColor::create({ 0, 0, 0, 0 }); + m_impl->bg->setContentSize({ width, 0 }); + m_impl->bg->ignoreAnchorPointForPosition(false); + m_impl->bg->setAnchorPoint(ccp(.5f, .5f)); + this->addChildAtPosition(m_impl->bg, Anchor::Center); + + m_impl->nameMenu = CCMenu::create(); + m_impl->nameMenu->setContentWidth(width / 2 + 25); + + m_impl->nameLabel = CCLabelBMFont::create(setting ? setting->getDisplayName().c_str() : "", "bigFont.fnt"); + m_impl->nameLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .4f)->setScalePriority(1)); + m_impl->nameMenu->addChild(m_impl->nameLabel); + + m_impl->statusLabel = CCLabelBMFont::create("", "bigFont.fnt"); + m_impl->statusLabel->setScale(.25f); + this->addChildAtPosition(m_impl->statusLabel, Anchor::Left, ccp(10, -10), ccp(0, .5f)); + + if (setting && setting->getDescription()) { + auto descSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + descSpr->setScale(.5f); + auto descBtn = CCMenuItemSpriteExtra::create( + descSpr, this, menu_selector(SettingNodeV3::onDescription) + ); + m_impl->nameMenu->addChild(descBtn); + } + + auto resetSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr); + resetSpr->setScale(.5f); + m_impl->resetButton = CCMenuItemSpriteExtra::create( + resetSpr, this, menu_selector(SettingNodeV3::onReset) + ); + m_impl->nameMenu->addChild(m_impl->resetButton); + + m_impl->nameMenu->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::Start)); + m_impl->nameMenu->getLayout()->ignoreInvisibleChildren(true); + this->addChildAtPosition(m_impl->nameMenu, Anchor::Left, ccp(10, 0), ccp(0, .5f)); + + m_impl->buttonMenu = CCMenu::create(); + m_impl->buttonMenu->setContentSize({ width / 2 - 55, 30 }); + m_impl->buttonMenu->setLayout(AnchorLayout::create()); + this->addChildAtPosition(m_impl->buttonMenu, Anchor::Right, ccp(-10, 0), ccp(1, .5f)); + + this->setAnchorPoint({ .5f, .5f }); + this->setContentSize({ width, 30 }); + + return true; +} + +void SettingNodeV3::updateState(CCNode* invoker) { + m_impl->statusLabel->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); + + if (m_impl->setting && !m_impl->setting->shouldEnable()) { + if (auto desc = m_impl->setting->getEnableIfDescription()) { + m_impl->nameLabel->setColor(ccGRAY); + m_impl->statusLabel->setVisible(true); + m_impl->statusLabel->setColor("mod-list-errors-found"_cc3b); + m_impl->statusLabel->setString(desc->c_str()); + } + } + if (m_impl->setting && m_impl->setting->requiresRestart() && m_impl->committed) { + m_impl->statusLabel->setVisible(true); + m_impl->statusLabel->setColor("mod-list-restart-required-label"_cc3b); + m_impl->statusLabel->setString("Restart Required"); + m_impl->bg->setColor("mod-list-restart-required-label-bg"_cc3b); + m_impl->bg->setOpacity(75); + } + + m_impl->nameMenu->setContentWidth(this->getContentWidth() - m_impl->buttonMenu->getContentWidth() - 20); + m_impl->nameMenu->updateLayout(); +} + +void SettingNodeV3::onDescription(CCObject*) { + auto title = m_impl->setting->getDisplayName(); + FLAlertLayer::create( + nullptr, + title.c_str(), + m_impl->setting->getDescription().value_or("No description provided"), + "OK", nullptr, + clamp(title.size() * 16, 300, 400) + )->show(); +} +void SettingNodeV3::onReset(CCObject*) { + createQuickPopup( + "Reset", + fmt::format( + "Are you sure you want to reset {} to default?", + this->getSetting()->getDisplayName() + ), + "Cancel", "Reset", + [this](auto, bool btn2) { + if (btn2) { + this->resetToDefault(); + } + } + ); +} + +void SettingNodeV3::setDefaultBGColor(ccColor4B color) { + m_impl->bgColor = color; + this->updateState(nullptr); +} + +void SettingNodeV3::markChanged(CCNode* invoker) { + this->updateState(invoker); + SettingNodeValueChangeEventV3(this, false).post(); +} +void SettingNodeV3::commit() { + this->onCommit(); + m_impl->committed = true; + this->updateState(nullptr); + SettingNodeValueChangeEventV3(this, true).post(); +} +void SettingNodeV3::resetToDefault() { + if (!m_impl->setting) return; + m_impl->setting->reset(); + m_impl->committed = true; + this->onResetToDefault(); + this->updateState(nullptr); + SettingNodeValueChangeEventV3(this, false).post(); +} + +void SettingNodeV3::setContentSize(CCSize const& size) { + CCNode::setContentSize(size); + m_impl->bg->setContentSize(size); + this->updateLayout(); + SettingNodeSizeChangeEventV3(this).post(); +} + +CCLabelBMFont* SettingNodeV3::getNameLabel() const { + return m_impl->nameLabel; +} +CCLabelBMFont* SettingNodeV3::getStatusLabel() const { + return m_impl->statusLabel; +} +CCMenu* SettingNodeV3::getNameMenu() const { + return m_impl->nameMenu; +} +CCMenu* SettingNodeV3::getButtonMenu() const { + return m_impl->buttonMenu; +} +CCLayerColor* SettingNodeV3::getBG() const { + return m_impl->bg; +} + +std::shared_ptr SettingNodeV3::getSetting() const { + return m_impl->setting; +} + +// TitleSettingNodeV3 + +bool TitleSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingNodeV3::init(setting, width)) + return false; + + auto collapseSprBG = CCSprite::create("square02c_001.png"); + collapseSprBG->setColor(ccc3(25, 25, 25)); + collapseSprBG->setOpacity(105); + auto collapseSpr = CCSprite::createWithSpriteFrameName("edit_downBtn_001.png"); + collapseSpr->setScale(1.9f); + collapseSprBG->addChildAtPosition(collapseSpr, Anchor::Center); + collapseSprBG->setScale(.2f); + + auto uncollapseSprBG = CCSprite::create("square02c_001.png"); + uncollapseSprBG->setColor(ccc3(25, 25, 25)); + uncollapseSprBG->setOpacity(105); + auto uncollapseSpr = CCSprite::createWithSpriteFrameName("edit_delCBtn_001.png"); + uncollapseSpr->setScale(1.5f); + uncollapseSprBG->addChildAtPosition(uncollapseSpr, Anchor::Center); + uncollapseSprBG->setScale(.2f); + + m_collapseToggle = CCMenuItemToggler::create( + collapseSprBG, uncollapseSprBG, + this, menu_selector(TitleSettingNodeV3::onCollapse) + ); + m_collapseToggle->m_notClickable = true; + this->getButtonMenu()->setContentWidth(20); + this->getButtonMenu()->addChildAtPosition(m_collapseToggle, Anchor::Center); + + this->getNameLabel()->setFntFile("goldFont.fnt"); + this->getNameMenu()->updateLayout(); + this->setContentHeight(20); + this->updateState(nullptr); + + return true; +} + +void TitleSettingNodeV3::onCollapse(CCObject* sender) { + m_collapseToggle->toggle(!m_collapseToggle->isToggled()); + // This triggers popup state to update due to SettingNodeValueChangeEventV3 being posted + this->markChanged(static_cast(sender)); +} +void TitleSettingNodeV3::onCommit() {} + +bool TitleSettingNodeV3::isCollapsed() const { + return m_collapseToggle->isToggled(); +} + +bool TitleSettingNodeV3::hasUncommittedChanges() const { + return false; +} +bool TitleSettingNodeV3::hasNonDefaultValue() const { + return false; +} +void TitleSettingNodeV3::onResetToDefault() {} + +std::shared_ptr TitleSettingNodeV3::getSetting() const { + return std::static_pointer_cast(SettingNodeV3::getSetting()); +} + +TitleSettingNodeV3* TitleSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new TitleSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// BoolSettingNodeV3 + +bool BoolSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + this->getButtonMenu()->setContentWidth(20); + + m_toggle = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(BoolSettingNodeV3::onToggle), .55f + ); + m_toggle->m_onButton->setContentSize({ 25, 25 }); + m_toggle->m_onButton->getNormalImage()->setPosition(ccp(25, 25) / 2); + m_toggle->m_offButton->setContentSize({ 25, 25 }); + m_toggle->m_offButton->getNormalImage()->setPosition(ccp(25, 25) / 2); + m_toggle->m_notClickable = true; + m_toggle->toggle(setting->getValue()); + this->getButtonMenu()->addChildAtPosition(m_toggle, Anchor::Right, ccp(-10, 0)); + + this->updateState(nullptr); + + return true; +} + +void BoolSettingNodeV3::updateState(CCNode* invoker) { + SettingValueNodeV3::updateState(invoker); + auto enable = this->getSetting()->shouldEnable(); + m_toggle->toggle(this->getValue()); + 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::onToggle(CCObject*) { + this->setValue(!m_toggle->isToggled(), m_toggle); + this->markChanged(m_toggle); +} + +BoolSettingNodeV3* BoolSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new BoolSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// StringSettingNodeV3 + +bool StringSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + m_input = TextInput::create(setting->getEnumOptions() ? width / 2 - 50 : width / 2, "Text"); + m_input->setCallback([this](auto const& str) { + this->setValue(str, m_input); + }); + m_input->setScale(.7f); + m_input->setString(this->getSetting()->getValue()); + this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center); + + if (setting->getEnumOptions()) { + m_input->getBGSprite()->setVisible(false); + m_input->setEnabled(false); + m_input->getInputNode()->m_placeholderLabel->setOpacity(255); + m_input->getInputNode()->m_placeholderLabel->setColor(ccWHITE); + + m_arrowLeftSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + m_arrowLeftSpr->setFlipX(true); + m_arrowLeftSpr->setScale(.4f); + auto arrowLeftBtn = CCMenuItemSpriteExtra::create( + m_arrowLeftSpr, this, menu_selector(StringSettingNodeV3::onArrow) + ); + arrowLeftBtn->setTag(-1); + this->getButtonMenu()->addChildAtPosition(arrowLeftBtn, Anchor::Left, ccp(5, 0)); + + m_arrowRightSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + m_arrowRightSpr->setScale(.4f); + auto arrowRightBtn = CCMenuItemSpriteExtra::create( + m_arrowRightSpr, this, menu_selector(StringSettingNodeV3::onArrow) + ); + arrowRightBtn->setTag(1); + this->getButtonMenu()->addChildAtPosition(arrowRightBtn, Anchor::Right, ccp(-5, 0)); + } + + this->updateState(nullptr); + + return true; +} + +void StringSettingNodeV3::updateState(CCNode* invoker) { + SettingValueNodeV3::updateState(invoker); + + if (invoker != m_input) { + m_input->setString(this->getValue()); + } + + 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, this->getValue()).value_or(0); + if (sender->getTag() > 0) { + index = index < options.size() - 1 ? index + 1 : 0; + } + else { + index = index > 0 ? index - 1 : options.size() - 1; + } + this->setValue(options.at(index), static_cast(sender)); +} + +StringSettingNodeV3* StringSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new StringSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// FileSettingNodeV3 + +bool FileSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + auto labelBG = extension::CCScale9Sprite::create("square02b_001.png", { 0, 0, 80, 80 }); + labelBG->setScale(.25f); + labelBG->setColor({ 0, 0, 0 }); + labelBG->setOpacity(90); + labelBG->setContentSize({ 420, 80 }); + this->getButtonMenu()->addChildAtPosition(labelBG, Anchor::Center, ccp(-10, 0)); + + m_fileIcon = CCSprite::create(); + this->getButtonMenu()->addChildAtPosition(m_fileIcon, Anchor::Left, ccp(5, 0)); + + m_nameLabel = CCLabelBMFont::create("", "bigFont.fnt"); + this->getButtonMenu()->addChildAtPosition(m_nameLabel, Anchor::Left, ccp(13, 0), ccp(0, .5f)); + + 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(m_selectBtn, Anchor::Right, ccp(-5, 0)); + + this->updateState(nullptr); + + return true; +} + +void FileSettingNodeV3::updateState(CCNode* invoker) { + SettingValueNodeV3::updateState(invoker); + m_fileIcon->setDisplayFrame(CCSpriteFrameCache::get()->spriteFrameByName( + this->getSetting()->isFolder() ? "folderIcon_001.png" : "file.png"_spr + )); + limitNodeSize(m_fileIcon, ccp(10, 10), 1.f, .1f); + if (this->getValue().empty()) { + m_nameLabel->setString(this->getSetting()->isFolder() ? "No Folder Selected" : "No File Selected"); + m_nameLabel->setColor(ccGRAY); + m_nameLabel->setOpacity(155); + } + else { + m_nameLabel->setString(this->getValue().filename().string().c_str()); + m_nameLabel->setColor(ccWHITE); + 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*) { + m_pickListener.bind([this](auto* event) { + auto value = event->getValue(); + if (!value) { + return; + } + if (value->isOk()) { + this->setValue(value->unwrap(), nullptr); + } + else { + FLAlertLayer::create( + "Failed", + fmt::format("Failed to pick file: {}", value->unwrapErr()), + "Ok" + )->show(); + } + }); + std::error_code ec; + m_pickListener.setFilter(file::pick( + this->getSetting()->isFolder() ? + file::PickMode::OpenFolder : + (this->getSetting()->useSaveDialog() ? file::PickMode::SaveFile : file::PickMode::OpenFile), + { + // Prefer opening the current path directly if possible + this->getValue().empty() || !std::filesystem::exists(this->getValue().parent_path(), ec) ? + dirs::getGameDir() : + this->getValue(), + this->getSetting()->getFilters().value_or(std::vector()) + } + )); +} + +FileSettingNodeV3* FileSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new FileSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// Color3BSettingNodeV3 + +bool Color3BSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + m_colorSprite = ColorChannelSprite::create(); + m_colorSprite->setScale(.65f); + + m_colorBtn = CCMenuItemSpriteExtra::create( + m_colorSprite, this, menu_selector(Color3BSettingNodeV3::onSelectColor) + ); + this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0)); + + this->updateState(nullptr); + + return true; +} + +void Color3BSettingNodeV3::updateState(CCNode* invoker) { + SettingValueNodeV3::updateState(invoker); + m_colorSprite->setColor(this->getValue()); + + auto enable = this->getSetting()->shouldEnable(); + m_colorSprite->setOpacity(enable ? 255 : 155); + m_colorBtn->setEnabled(enable); +} + +void Color3BSettingNodeV3::onSelectColor(CCObject*) { + auto popup = ColorPickPopup::create(this->getValue()); + popup->setDelegate(this); + popup->show(); +} +void Color3BSettingNodeV3::updateColor(ccColor4B const& color) { + this->setValue(to3B(color), nullptr); +} + +Color3BSettingNodeV3* Color3BSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new Color3BSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// Color4BSettingNodeV3 + +bool Color4BSettingNodeV3::init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + m_colorSprite = ColorChannelSprite::create(); + m_colorSprite->setScale(.65f); + + m_colorBtn = CCMenuItemSpriteExtra::create( + m_colorSprite, this, menu_selector(Color4BSettingNodeV3::onSelectColor) + ); + this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0)); + + this->updateState(nullptr); + + return true; +} + +void Color4BSettingNodeV3::updateState(CCNode* invoker) { + SettingValueNodeV3::updateState(invoker); + m_colorSprite->setColor(to3B(this->getValue())); + m_colorSprite->updateOpacity(this->getValue().a / 255.f); + + auto enable = this->getSetting()->shouldEnable(); + m_colorSprite->setOpacity(enable ? 255 : 155); + m_colorBtn->setEnabled(enable); +} + +void Color4BSettingNodeV3::onSelectColor(CCObject*) { + auto popup = ColorPickPopup::create(this->getValue()); + popup->setDelegate(this); + popup->show(); +} +void Color4BSettingNodeV3::updateColor(ccColor4B const& color) { + this->setValue(color, nullptr); +} + +Color4BSettingNodeV3* Color4BSettingNodeV3::create(std::shared_ptr setting, float width) { + auto ret = new Color4BSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// UnresolvedCustomSettingNodeV3 + +bool UnresolvedCustomSettingNodeV3::init(std::string_view key, Mod* mod, float width) { + if (!SettingNodeV3::init(nullptr, width)) + return false; + + m_mod = mod; + + this->setContentHeight(30); + + auto label = CCLabelBMFont::create( + (mod && mod->isEnabled() ? + fmt::format("Missing setting '{}'", key) : + fmt::format("Enable the Mod to Edit '{}'", key) + ).c_str(), + "bigFont.fnt" + ); + label->setColor(mod && mod->isEnabled() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b); + label->limitLabelWidth(width - m_obContentSize.height, .3f, .1f); + this->addChildAtPosition(label, Anchor::Left, ccp(m_obContentSize.height / 2, 0), ccp(0, .5f)); + + return true; +} + +void UnresolvedCustomSettingNodeV3::updateState(CCNode* invoker) { + SettingNodeV3::updateState(invoker); + this->getBG()->setColor(m_mod && m_mod->isEnabled() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b); + this->getBG()->setOpacity(75); +} + +void UnresolvedCustomSettingNodeV3::onCommit() {} + +bool UnresolvedCustomSettingNodeV3::hasUncommittedChanges() const { + return false; +} +bool UnresolvedCustomSettingNodeV3::hasNonDefaultValue() const { + return false; +} +void UnresolvedCustomSettingNodeV3::onResetToDefault() {} + +UnresolvedCustomSettingNodeV3* UnresolvedCustomSettingNodeV3::create(std::string_view key, Mod* mod, float width) { + auto ret = new UnresolvedCustomSettingNodeV3(); + if (ret && ret->init(key, mod, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// LegacyCustomSettingToV3Node + +bool LegacyCustomSettingToV3Node::init(std::shared_ptr original, float width) { + if (!SettingNodeV3::init(original, width)) + return false; + + this->getNameMenu()->setVisible(false); + this->getButtonMenu()->setVisible(false); + + m_original = original->getValue()->createNode(width); + m_original->setDelegate(this); + this->setContentSize({ width, m_original->getContentHeight() }); + this->addChildAtPosition(m_original, Anchor::BottomLeft, ccp(0, 0), ccp(0, 0)); + + return true; +} + +void LegacyCustomSettingToV3Node::settingValueChanged(SettingNode*) { + SettingNodeValueChangeEventV3(this, false).post(); +} +void LegacyCustomSettingToV3Node::settingValueCommitted(SettingNode*) { + SettingNodeValueChangeEventV3(this, true).post(); +} + +void LegacyCustomSettingToV3Node::onCommit() { + m_original->commit(); +} + +bool LegacyCustomSettingToV3Node::hasUncommittedChanges() const { + return m_original->hasUncommittedChanges(); +} +bool LegacyCustomSettingToV3Node::hasNonDefaultValue() const { + return m_original->hasNonDefaultValue(); +} +void LegacyCustomSettingToV3Node::onResetToDefault() { + m_original->resetToDefault(); +} + +LegacyCustomSettingToV3Node* LegacyCustomSettingToV3Node::create(std::shared_ptr original, float width) { + auto ret = new LegacyCustomSettingToV3Node(); + if (ret && ret->init(original, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/loader/SettingNodeV3.hpp b/loader/src/loader/SettingNodeV3.hpp new file mode 100644 index 00000000..781db5f2 --- /dev/null +++ b/loader/src/loader/SettingNodeV3.hpp @@ -0,0 +1,336 @@ +#pragma once + +#include +#include +#include +#include + +using namespace geode::prelude; + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// !! If these classes are ever exposed in a public header, make sure to pimpl EVERYTHING! !! +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +class TitleSettingNodeV3 : public SettingNodeV3 { +protected: + CCMenuItemToggler* m_collapseToggle; + + bool init(std::shared_ptr setting, float width); + + void onCommit() override; + void onCollapse(CCObject*); + +public: + static TitleSettingNodeV3* create(std::shared_ptr setting, float width); + + bool isCollapsed() const; + + bool hasUncommittedChanges() const override; + bool hasNonDefaultValue() const override; + void onResetToDefault() override; + + std::shared_ptr getSetting() const; +}; + +class BoolSettingNodeV3 : public SettingValueNodeV3 { +protected: + CCMenuItemToggler* m_toggle; + + bool init(std::shared_ptr setting, float width); + void updateState(CCNode* invoker) override; + void onToggle(CCObject*); + +public: + static BoolSettingNodeV3* create(std::shared_ptr setting, float width); +}; + +template +class NumberSettingNodeV3 : public SettingValueNodeV3 { +protected: + using ValueType = typename S::ValueType; + using ValueAssignType = typename S::ValueAssignType; + + TextInput* m_input; + Slider* m_slider; + CCMenuItemSpriteExtra* m_arrowLeftBtn; + 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); + auto max = this->getSetting()->getMaxValue().value_or(+100); + auto range = max - min; + return static_cast(clamp(static_cast(value - min) / range, 0.0, 1.0)); + } + ValueType valueFromSlider(float num) { + auto min = this->getSetting()->getMinValue().value_or(-100); + auto max = this->getSetting()->getMaxValue().value_or(+100); + auto range = max - min; + auto value = static_cast(num * range + min); + auto step = this->getSetting()->getSliderSnap(); + if (step > 0) { + value = static_cast(round(value / step) * step); + } + return value; + } + + bool init(std::shared_ptr setting, float width) { + if (!SettingValueNodeV3::init(setting, width)) + return false; + + 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"); + 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( + 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)); + + m_arrowLeftBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + m_arrowLeftBtnSpr->setScale(.5f); + m_arrowLeftBtn = CCMenuItemSpriteExtra::create( + m_arrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + ); + m_arrowLeftBtn->setUserObject(ObjWrapper::create(-setting->getArrowStepSize())); + m_arrowLeftBtn->setVisible(setting->isArrowsEnabled()); + this->getButtonMenu()->addChildAtPosition(m_arrowLeftBtn, Anchor::Left, ccp(22, 0)); + + m_input = TextInput::create(this->getButtonMenu()->getContentWidth() - 40, "Num"); + m_input->setScale(.7f); + m_input->setCallback([this, setting](auto const& str) { + this->setValue(numFromString(str).unwrapOr(setting->getDefaultValue()), m_input); + }); + if (!setting->isInputEnabled()) { + m_input->getBGSprite()->setVisible(false); + m_input->setEnabled(false); + m_input->getInputNode()->m_placeholderLabel->setOpacity(255); + m_input->getInputNode()->m_placeholderLabel->setColor(ccWHITE); + } + this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center); + + m_arrowRightBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + m_arrowRightBtnSpr->setFlipX(true); + m_arrowRightBtnSpr->setScale(.5f); + m_arrowRightBtn = CCMenuItemSpriteExtra::create( + 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)); + + 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); + + 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( + m_bigArrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) + ); + m_bigArrowRightBtn->setUserObject(ObjWrapper::create(setting->getBigArrowStepSize())); + m_bigArrowRightBtn->setVisible(setting->isBigArrowsEnabled()); + this->getButtonMenu()->addChildAtPosition(m_bigArrowRightBtn, Anchor::Right, ccp(-5, 0)); + + if (setting->isSliderEnabled()) { + this->setContentHeight(45); + this->getButtonMenu()->updateAnchoredPosition(Anchor::Right, ccp(-10, 7)); + + m_slider = Slider::create(this, menu_selector(NumberSettingNodeV3::onSlider)); + m_slider->setScale(.5f); + this->getButtonMenu()->addChildAtPosition(m_slider, Anchor::Center, ccp(0, -20), ccp(0, 0)); + } + + this->setValue(setting->getValue(), nullptr); + this->updateState(nullptr); + + return true; + } + + void updateState(CCNode* invoker) override { + SettingValueNodeV3::updateState(invoker); + auto enable = this->getSetting()->shouldEnable(); + if (this->getSetting()->isInputEnabled()) { + m_input->setEnabled(enable); + } + + if (invoker != m_input) { + m_input->setString(numToString(this->getValue())); + } + + auto min = this->getSetting()->getMinValue(); + auto enableLeft = enable && (!min || this->getValue() > *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->getValue() < *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->getValue())); + m_slider->updateBar(); + 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); + } + } + + void onArrow(CCObject* sender) { + auto value = this->getValue() + static_cast*>( + static_cast(sender)->getUserObject() + )->getValue(); + if (auto min = this->getSetting()->getMinValue()) { + value = std::max(*min, value); + } + if (auto max = this->getSetting()->getMaxValue()) { + value = std::min(*max, value); + } + this->setValue(value, static_cast(sender)); + } + void onSlider(CCObject*) { + this->setValue(this->valueFromSlider(m_slider->m_touchLogic->m_thumb->getValue()), m_slider); + } + +public: + static NumberSettingNodeV3* create(std::shared_ptr setting, float width) { + auto ret = new NumberSettingNodeV3(); + if (ret && ret->init(setting, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } +}; + +using IntSettingNodeV3 = NumberSettingNodeV3; +using FloatSettingNodeV3 = NumberSettingNodeV3; + +class StringSettingNodeV3 : public SettingValueNodeV3 { +protected: + TextInput* m_input; + CCSprite* m_arrowLeftSpr = nullptr; + CCSprite* m_arrowRightSpr = nullptr; + + bool init(std::shared_ptr setting, float width); + void updateState(CCNode* invoker) override; + void onArrow(CCObject* sender); + +public: + static StringSettingNodeV3* create(std::shared_ptr setting, float width); +}; + +class FileSettingNodeV3 : public SettingValueNodeV3 { +protected: + CCSprite* m_fileIcon; + CCLabelBMFont* m_nameLabel; + EventListener>> m_pickListener; + CCMenuItemSpriteExtra* m_selectBtn; + CCSprite* m_selectBtnSpr; + + bool init(std::shared_ptr setting, float width); + void updateState(CCNode* invoker) override; + void onPickFile(CCObject*); + +public: + static FileSettingNodeV3* create(std::shared_ptr setting, float width); +}; + +class Color3BSettingNodeV3 : public SettingValueNodeV3, public ColorPickPopupDelegate { +protected: + CCMenuItemSpriteExtra* m_colorBtn; + ColorChannelSprite* m_colorSprite; + + bool init(std::shared_ptr setting, float width); + void updateState(CCNode* invoker) override; + void onSelectColor(CCObject*); + void updateColor(ccColor4B const& color) override; + +public: + static Color3BSettingNodeV3* create(std::shared_ptr setting, float width); +}; + +class Color4BSettingNodeV3 : public SettingValueNodeV3, public ColorPickPopupDelegate { +protected: + CCMenuItemSpriteExtra* m_colorBtn; + ColorChannelSprite* m_colorSprite; + + bool init(std::shared_ptr setting, float width); + void updateState(CCNode* invoker) override; + void onSelectColor(CCObject*); + void updateColor(ccColor4B const& color) override; + +public: + static Color4BSettingNodeV3* create(std::shared_ptr setting, float width); +}; + +class UnresolvedCustomSettingNodeV3 : public SettingNodeV3 { +protected: + Mod* m_mod; + + bool init(std::string_view key, Mod* mod, float width); + + void updateState(CCNode* invoker) override; + + void onCommit() override; + +public: + static UnresolvedCustomSettingNodeV3* create(std::string_view key, Mod* mod, float width); + + bool hasUncommittedChanges() const override; + bool hasNonDefaultValue() const override; + void onResetToDefault() override; +}; + +// If these classes do get exposed in headers, +// LegacyCustomSettingToV3Node SHOULD NOT BE EXPOSED!!!!!! DO NOT DO THAT!!!! + +class LegacyCustomSettingToV3Node : public SettingNodeV3, public SettingNodeDelegate { +protected: + SettingNode* m_original; + + bool init(std::shared_ptr original, float width); + + void onCommit() override; + + void settingValueChanged(SettingNode*) override; + void settingValueCommitted(SettingNode*) override; + +public: + static LegacyCustomSettingToV3Node* create(std::shared_ptr original, float width); + + bool hasUncommittedChanges() const override; + bool hasNonDefaultValue() const override; + void onResetToDefault() override; +}; diff --git a/loader/src/loader/SettingV3.cpp b/loader/src/loader/SettingV3.cpp new file mode 100644 index 00000000..4c8398f1 --- /dev/null +++ b/loader/src/loader/SettingV3.cpp @@ -0,0 +1,1220 @@ +#include +#include +#include +#include +#include +#include "SettingNodeV3.hpp" + +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 SettingChangedEventV3::Impl final { +public: + std::shared_ptr setting; +}; + +SettingChangedEventV3::SettingChangedEventV3(std::shared_ptr setting) + : m_impl(std::make_shared()) +{ + m_impl->setting = setting; +} + +std::shared_ptr SettingChangedEventV3::getSetting() const { + return m_impl->setting; +} + +class SettingChangedFilterV3::Impl final { +public: + std::string modID; + std::optional settingKey; +}; + +ListenerResult SettingChangedFilterV3::handle(utils::MiniFunction fn, SettingChangedEventV3* event) { + if ( + event->getSetting()->getModID() == m_impl->modID && + !m_impl->settingKey || event->getSetting()->getKey() == m_impl->settingKey + ) { + fn(event->getSetting()); + } + return ListenerResult::Propagate; +} + +SettingChangedFilterV3::SettingChangedFilterV3( + std::string const& modID, + std::optional const& settingKey +) : m_impl(std::make_shared()) +{ + m_impl->modID = modID; + m_impl->settingKey = settingKey; +} + +SettingChangedFilterV3::SettingChangedFilterV3(Mod* mod, std::optional const& settingKey) + : SettingChangedFilterV3(mod->getID(), settingKey) {} + +SettingChangedFilterV3::SettingChangedFilterV3(SettingChangedFilterV3 const&) = default; + +EventListener* geode::listenForAllSettingChanges( + std::function)> const& callback, + Mod* mod +) { + return new EventListener( + [callback](std::shared_ptr setting) { + callback(setting); + }, + SettingChangedFilterV3(mod->getID(), std::nullopt) + ); +} + +class SettingV3::GeodeImpl { +public: + std::string modID; + std::string key; + std::vector platforms; + std::optional name; + std::optional description; + std::optional enableIf; + std::unique_ptr enableIfTree; + std::optional enableIfDescription; + bool requiresRestart = false; +}; + +SettingV3::SettingV3() : m_impl(std::make_shared()) {} +SettingV3::~SettingV3() = default; + +void SettingV3::init(std::string const& key, std::string const& modID) { + m_impl->key = key; + m_impl->modID = modID; +} +void SettingV3::init(std::string const& key, std::string const& modID, JsonExpectedValue& json) { + this->init(key, modID); + + // Keys every setting must have + json.needs("type"); + for (auto& plat : json.has("platforms").items()) { + ranges::push(m_impl->platforms, PlatformID::getCovered(plat.template get())); + } +} + +void SettingV3::parseNameAndDescription(JsonExpectedValue& json) { + json.has("name").into(m_impl->name); + json.has("description").into(m_impl->description); +} +void SettingV3::parseEnableIf(JsonExpectedValue& json) { + json.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); + json.has("enable-if-description").into(m_impl->enableIfDescription); +} +void SettingV3::parseValueProperties(JsonExpectedValue& json) { + json.has("requires-restart").into(m_impl->requiresRestart); +} + +Result<> SettingV3::parseBaseProperties(std::string const& key, std::string const& modID, matjson::Value const& value) { + auto json = checkJson(value, "SettingV3"); + this->parseBaseProperties(key, modID, json); + return json.ok(); +} +void SettingV3::parseBaseProperties(std::string const& key, std::string const& modID, JsonExpectedValue& json) { + this->init(key, modID, json); + this->parseNameAndDescription(json); + this->parseValueProperties(json); + this->parseEnableIf(json); +} + +std::string SettingV3::getKey() const { + return m_impl->key; +} +std::string SettingV3::getModID() const { + return m_impl->modID; +} +std::optional SettingV3::getName() const { + return m_impl->name; +} +std::string SettingV3::getDisplayName() const { + return m_impl->name.value_or(m_impl->key); +} +std::optional SettingV3::getDescription() const { + return m_impl->description; +} +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; +} +std::vector SettingV3::getPlatforms() const { + return m_impl->platforms; +} +Mod* SettingV3::getMod() const { + return Loader::get()->getInstalledMod(m_impl->modID); +} + +void SettingV3::markChanged() { + auto manager = ModSettingsManager::from(this->getMod()); + if (m_impl->requiresRestart) { + manager->markRestartRequired(); + } + SettingChangedEventV3(shared_from_this()).post(); + if (manager) { + // Use ModSettingsManager rather than convertToLegacyValue since it + // caches the result and we want to have that for performance + SettingChangedEvent(this->getMod(), manager->getLegacy(this->getKey()).get()).post(); + } +} + +std::optional SettingV3::convertToLegacy() const { + return std::nullopt; +} +std::optional> SettingV3::convertToLegacyValue() const { + return std::nullopt; +} + +class TitleSettingV3::Impl final { +public: +}; + +TitleSettingV3::TitleSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> TitleSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + auto root = checkJson(json, "TitleSettingV3"); + ret->init(key, modID, root); + ret->parseNameAndDescription(root); + root.checkUnknownKeys(); + return root.ok(ret); +} + +bool TitleSettingV3::load(matjson::Value const& json) { + return true; +} +bool TitleSettingV3::save(matjson::Value&) const { + return true; +} +SettingNodeV3* TitleSettingV3::createNode(float width) { + return TitleSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} +bool TitleSettingV3::isDefaultValue() const { + return true; +} +void TitleSettingV3::reset() {} + +class LegacyCustomSettingV3::Impl final { +public: + matjson::Value json; + std::shared_ptr legacyValue = nullptr; +}; + +LegacyCustomSettingV3::LegacyCustomSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> LegacyCustomSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + ret->init(key, modID); + ret->m_impl->json = json; + return Ok(ret); +} + +std::shared_ptr LegacyCustomSettingV3::getValue() const { + return m_impl->legacyValue; +} +void LegacyCustomSettingV3::setValue(std::shared_ptr value) { + m_impl->legacyValue = value; +} + +bool LegacyCustomSettingV3::load(matjson::Value const& json) { + return true; +} +bool LegacyCustomSettingV3::save(matjson::Value& json) const { + return true; +} +SettingNodeV3* LegacyCustomSettingV3::createNode(float width) { + if (m_impl->legacyValue) { + return LegacyCustomSettingToV3Node::create( + std::static_pointer_cast(shared_from_this()), width + ); + } + return UnresolvedCustomSettingNodeV3::create(this->getKey(), this->getMod(), width); +} + +bool LegacyCustomSettingV3::isDefaultValue() const { + return true; +} +void LegacyCustomSettingV3::reset() {} + +std::optional LegacyCustomSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(CustomSetting { + .json = std::make_shared(m_impl->json) + })); +} +std::optional> LegacyCustomSettingV3::convertToLegacyValue() const { + return m_impl->legacyValue ? std::optional(m_impl->legacyValue) : std::nullopt; +} + +class BoolSettingV3::Impl final { +public: +}; + +BoolSettingV3::BoolSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> BoolSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + auto root = checkJson(json, "BoolSettingV3"); + ret->parseBaseProperties(key, modID, root); + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> BoolSettingV3::isValid(bool value) const { + return Ok(); +} + +SettingNodeV3* BoolSettingV3::createNode(float width) { + return BoolSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional BoolSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(BoolSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + })); +} +std::optional> BoolSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class IntSettingV3::Impl final { +public: + std::optional minValue; + std::optional maxValue; + + struct { + // 0 means not enabled + size_t arrowStepSize = 1; + size_t bigArrowStepSize = 5; + bool sliderEnabled = true; + int64_t sliderSnap = 1; + bool textInputEnabled = true; + } controls; +}; + +Result> IntSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + + auto root = checkJson(json, "IntSettingV3"); + ret->parseBaseProperties(key, modID, root); + + root.has("min").into(ret->m_impl->minValue); + root.has("max").into(ret->m_impl->maxValue); + if (auto controls = root.has("control")) { + controls.has("arrows"); + controls.has("big-arrows"); + controls.has("arrow-step").into(ret->m_impl->controls.arrowStepSize); + controls.has("big-arrow-step").into(ret->m_impl->controls.bigArrowStepSize); + controls.has("slider").into(ret->m_impl->controls.sliderEnabled); + controls.has("slider-step").into(ret->m_impl->controls.sliderSnap); + controls.has("input").into(ret->m_impl->controls.textInputEnabled); + controls.checkUnknownKeys(); + } + + // Disable arrows if they aren't enabled + // This silly code is because step size being 0 is what defines if they are enabled + + // Small arrows are enabled by default + if (!root.has("control").has("arrows").template get(true)) { + ret->m_impl->controls.arrowStepSize = 0; + } + if (!root.has("control").has("big-arrows").template get()) { + ret->m_impl->controls.bigArrowStepSize = 0; + } + + // Without "min" or "max" slider makes no sense + if (!ret->m_impl->minValue || !ret->m_impl->maxValue) { + if (ret->m_impl->controls.sliderEnabled && root.has("control").has("slider")) { + log::warn( + "Setting '{}' has \"controls.slider\" enabled but doesn't " + "have both \"min\" and \"max\" defined - the slider has " + "been force-disabled!", + key + ); + } + ret->m_impl->controls.sliderEnabled = false; + } + + root.checkUnknownKeys(); + return root.ok(ret); +} + +IntSettingV3::IntSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result<> IntSettingV3::isValid(int64_t value) const { + if (m_impl->minValue && value < *m_impl->minValue) { + return Err("Value must be at least {}", *m_impl->minValue); + } + if (m_impl->maxValue && value > *m_impl->maxValue) { + return Err("Value must be at most {}", *m_impl->maxValue); + } + return Ok(); +} + +std::optional IntSettingV3::getMinValue() const { + return m_impl->minValue; +} +std::optional IntSettingV3::getMaxValue() const { + return m_impl->maxValue; +} + +bool IntSettingV3::isArrowsEnabled() const { + return m_impl->controls.arrowStepSize > 0; +} +bool IntSettingV3::isBigArrowsEnabled() const { + return m_impl->controls.bigArrowStepSize > 0; +} +size_t IntSettingV3::getArrowStepSize() const { + return m_impl->controls.arrowStepSize; +} +size_t IntSettingV3::getBigArrowStepSize() const { + return m_impl->controls.bigArrowStepSize; +} +bool IntSettingV3::isSliderEnabled() const { + return m_impl->controls.sliderEnabled; +} +int64_t IntSettingV3::getSliderSnap() const { + return m_impl->controls.sliderSnap; +} +bool IntSettingV3::isInputEnabled() const { + return m_impl->controls.textInputEnabled; +} + +SettingNodeV3* IntSettingV3::createNode(float width) { + return IntSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional IntSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(IntSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + .min = this->getMinValue(), + .max = this->getMaxValue(), + .controls = { + .arrows = this->isArrowsEnabled(), + .bigArrows = this->isBigArrowsEnabled(), + .arrowStep = this->getArrowStepSize(), + .bigArrowStep = this->getBigArrowStepSize(), + .slider = this->isSliderEnabled(), + .sliderStep = this->getSliderSnap(), + .input = this->isInputEnabled(), + }, + })); +} +std::optional> IntSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class FloatSettingV3::Impl final { +public: + std::optional minValue; + std::optional maxValue; + + struct { + // 0 means not enabled + double arrowStepSize = 1; + double bigArrowStepSize = 5; + bool sliderEnabled = true; + double sliderSnap = 0.1; + bool textInputEnabled = true; + } controls; +}; + +FloatSettingV3::FloatSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> FloatSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + + auto root = checkJson(json, "FloatSettingV3"); + ret->parseBaseProperties(key, modID, root); + + root.has("min").into(ret->m_impl->minValue); + root.has("max").into(ret->m_impl->maxValue); + if (auto controls = root.has("control")) { + controls.has("arrows"); + controls.has("big-arrows"); + controls.has("arrow-step").into(ret->m_impl->controls.arrowStepSize); + controls.has("big-arrow-step").into(ret->m_impl->controls.bigArrowStepSize); + controls.has("slider").into(ret->m_impl->controls.sliderEnabled); + controls.has("slider-step").into(ret->m_impl->controls.sliderSnap); + controls.has("input").into(ret->m_impl->controls.textInputEnabled); + controls.checkUnknownKeys(); + } + + // Disable arrows if they aren't enabled + // Small arrows are enabled by default + if (!root.has("control").has("arrows").template get(true)) { + ret->m_impl->controls.arrowStepSize = 0; + } + if (!root.has("control").has("big-arrows").template get()) { + ret->m_impl->controls.bigArrowStepSize = 0; + } + + // Without "min" or "max" slider makes no sense + if (!ret->m_impl->minValue || !ret->m_impl->maxValue) { + if (ret->m_impl->controls.sliderEnabled && root.has("control").has("slider")) { + log::warn( + "Setting '{}' has \"controls.slider\" enabled but doesn't " + "have both \"min\" and \"max\" defined - the slider has " + "been force-disabled!", + key + ); + } + ret->m_impl->controls.sliderEnabled = false; + } + + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> FloatSettingV3::isValid(double value) const { + if (m_impl->minValue && value < *m_impl->minValue) { + return Err("Value must be at least {}", *m_impl->minValue); + } + if (m_impl->maxValue && value > *m_impl->maxValue) { + return Err("Value must be at most {}", *m_impl->maxValue); + } + return Ok(); +} + +std::optional FloatSettingV3::getMinValue() const { + return m_impl->minValue; +} +std::optional FloatSettingV3::getMaxValue() const { + return m_impl->maxValue; +} + +bool FloatSettingV3::isArrowsEnabled() const { + return m_impl->controls.arrowStepSize > 0; +} +bool FloatSettingV3::isBigArrowsEnabled() const { + return m_impl->controls.bigArrowStepSize > 0; +} +double FloatSettingV3::getArrowStepSize() const { + return m_impl->controls.arrowStepSize; +} +double FloatSettingV3::getBigArrowStepSize() const { + return m_impl->controls.bigArrowStepSize; +} +bool FloatSettingV3::isSliderEnabled() const { + return m_impl->controls.sliderEnabled; +} +double FloatSettingV3::getSliderSnap() const { + return m_impl->controls.sliderSnap; +} +bool FloatSettingV3::isInputEnabled() const { + return m_impl->controls.textInputEnabled; +} + +SettingNodeV3* FloatSettingV3::createNode(float width) { + return FloatSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional FloatSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(FloatSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + .min = this->getMinValue(), + .max = this->getMaxValue(), + .controls = { + .arrows = this->isArrowsEnabled(), + .bigArrows = this->isBigArrowsEnabled(), + .arrowStep = static_cast(this->getArrowStepSize()), + .bigArrowStep = static_cast(this->getBigArrowStepSize()), + .slider = this->isSliderEnabled(), + .sliderStep = this->getSliderSnap(), + .input = this->isInputEnabled(), + }, + })); +} +std::optional> FloatSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class StringSettingV3::Impl final { +public: + std::optional match; + std::optional filter; + std::optional> oneOf; +}; + +StringSettingV3::StringSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> StringSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + + auto root = checkJson(json, "StringSettingV3"); + ret->parseBaseProperties(key, modID, root); + + root.has("match").into(ret->m_impl->match); + root.has("filter").into(ret->m_impl->filter); + root.has("one-of").into(ret->m_impl->oneOf); + if (ret->m_impl->oneOf && ret->m_impl->oneOf->empty()) { + return Err("Setting '{}' in mod {} - \"one-of\" may not be empty!", key, modID); + } + + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> StringSettingV3::isValid(std::string_view value) const { + if (m_impl->match) { + if (!std::regex_match(std::string(value), std::regex(*m_impl->match))) { + return Err("Value must match regex {}", *m_impl->match); + } + } + else if (m_impl->oneOf) { + if (!ranges::contains(*m_impl->oneOf, std::string(value))) { + return Err("Value must be one of {}", fmt::join(*m_impl->oneOf, ", ")); + } + } + return Ok(); +} + +std::optional StringSettingV3::getRegexValidator() const { + return m_impl->match; +} +std::optional StringSettingV3::getAllowedCharacters() const { + return m_impl->filter; +} +std::optional> StringSettingV3::getEnumOptions() const { + return m_impl->oneOf; +} + +SettingNodeV3* StringSettingV3::createNode(float width) { + return StringSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional StringSettingV3::convertToLegacy() const { + auto setting = StringSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + setting.controls->filter = this->getAllowedCharacters(); + setting.controls->match = this->getRegexValidator(); + setting.controls->options = this->getEnumOptions(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> StringSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class FileSettingV3::Impl final { +public: + bool folder = false; + bool useSaveDialog = false; // this option makes no sense if folder = true + std::optional> filters; +}; + +FileSettingV3::FileSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> FileSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + + auto root = checkJson(json, "FileSettingV3"); + ret->parseBaseProperties(key, modID, root); + + // Replace known paths like `{gd-save-dir}/` + try { + ret->setDefaultValue(fmt::format( + fmt::runtime(ret->getDefaultValue().string()), + fmt::arg("gd_dir", dirs::getGameDir()), + fmt::arg("gd_save_dir", dirs::getSaveDir()), + fmt::arg("mod_config_dir", dirs::getModConfigDir() / modID), + fmt::arg("mod_save_dir", dirs::getModsSaveDir() / modID), + fmt::arg("temp_dir", dirs::getTempDir()) + )); + } + catch(fmt::format_error const& e) { + return Err("Invalid format string for file setting path: {}", e.what()); + } + ret->setValue(ret->getDefaultValue()); + + std::string type; + root.needs("type").into(type); + if (type == "folder") { + ret->m_impl->folder = true; + // folder-specific stuff if they ever exist + } + else if (type == "file" || type == "path") { + if (type == "path") { + log::warn( + "Setting '{}' in mod {}: the \"path\" type has been " + "deprecated, use \"type\": \"file\" or \"type\": \"folder\" instead", + key, modID + ); + } + + // Controls only make sense for files but not for folders + if (auto controls = root.has("control")) { + std::string dialogType; + controls.has("dialog").into(dialogType); + switch (hash(dialogType)) { + case hash("save"): ret->m_impl->useSaveDialog = true; break; + case hash("open"): ret->m_impl->useSaveDialog = false; break; + case hash(""): break; + default: return Err("Setting '{}' in mod {}: unknown \"dialog\" type \"{}\"", key, modID, dialogType); + } + + auto filters = std::vector(); + for (auto& item : controls.has("filters").items()) { + utils::file::FilePickOptions::Filter filter; + item.has("description").into(filter.description); + item.has("files").into(filter.files); + filters.push_back(filter); + } + if (!filters.empty()) { + ret->m_impl->filters.emplace(filters); + } + } + } + + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> FileSettingV3::isValid(std::filesystem::path const& value) const { + std::error_code ec; + if (m_impl->folder) { + if (!std::filesystem::is_directory(value, ec)) { + return Err("Value must be a folder"); + } + } + else { + if (!std::filesystem::is_regular_file(value, ec)) { + return Err("Value must be a file"); + } + } + return Ok(); +} + +bool FileSettingV3::isFolder() const { + return m_impl->folder; +} +bool FileSettingV3::useSaveDialog() const { + return m_impl->useSaveDialog; +} + +std::optional> FileSettingV3::getFilters() const { + return m_impl->filters; +} + +SettingNodeV3* FileSettingV3::createNode(float width) { + return FileSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional FileSettingV3::convertToLegacy() const { + auto setting = FileSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + setting.controls.filters = this->getFilters().value_or(std::vector()); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> FileSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class Color3BSettingV3::Impl final { +public: +}; + +Color3BSettingV3::Color3BSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> Color3BSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + auto root = checkJson(json, "Color3BSettingV3"); + ret->parseBaseProperties(key, modID, root); + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> Color3BSettingV3::isValid(ccColor3B value) const { + return Ok(); +} + +SettingNodeV3* Color3BSettingV3::createNode(float width) { + return Color3BSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional Color3BSettingV3::convertToLegacy() const { + auto setting = ColorSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> Color3BSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} + +class Color4BSettingV3::Impl final { +public: +}; + +Color4BSettingV3::Color4BSettingV3(PrivateMarker) : m_impl(std::make_shared()) {} + +Result> Color4BSettingV3::parse(std::string const& key, std::string const& modID, matjson::Value const& json) { + auto ret = std::make_shared(PrivateMarker()); + auto root = checkJson(json, "Color4BSettingV3"); + ret->parseBaseProperties(key, modID, root); + root.checkUnknownKeys(); + return root.ok(ret); +} + +Result<> Color4BSettingV3::isValid(ccColor4B value) const { + return Ok(); +} + +SettingNodeV3* Color4BSettingV3::createNode(float width) { + return Color4BSettingNodeV3::create( + std::static_pointer_cast(shared_from_this()), width + ); +} + +std::optional Color4BSettingV3::convertToLegacy() const { + auto setting = ColorAlphaSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> Color4BSettingV3::convertToLegacyValue() const { + return this->convertToLegacy()->createDefaultValue(); +} diff --git a/loader/src/platform/mac/LoaderImpl.mm b/loader/src/platform/mac/LoaderImpl.mm index 5ef6269a..11956e1c 100644 --- a/loader/src/platform/mac/LoaderImpl.mm +++ b/loader/src/platform/mac/LoaderImpl.mm @@ -1,3 +1,4 @@ +#import #include #include #include @@ -5,7 +6,6 @@ #include #include #include -#import #include #include diff --git a/loader/src/platform/windows/nfdwin.cpp b/loader/src/platform/windows/nfdwin.cpp index 872963e0..6842135d 100644 --- a/loader/src/platform/windows/nfdwin.cpp +++ b/loader/src/platform/windows/nfdwin.cpp @@ -182,6 +182,7 @@ Result<> nfdPick( } if (options.defaultPath && options.defaultPath.value().wstring().size()) { std::filesystem::path path = options.defaultPath.value(); + path.make_preferred(); if (mode == NFDMode::OpenFile || mode == NFDMode::SaveFile) { if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) { if (path.has_filename()) { diff --git a/loader/src/platform/windows/util.cpp b/loader/src/platform/windows/util.cpp index 984c4baf..b2a6e8b7 100644 --- a/loader/src/platform/windows/util.cpp +++ b/loader/src/platform/windows/util.cpp @@ -123,11 +123,13 @@ Task> file::pick(PickMode mode, FilePickOptions co Result result; auto pickresult = nfdPick(nfdMode, options, &path); if (pickresult.isErr()) { - result = Err(pickresult.err().value()); + if (pickresult.unwrapErr() == "Dialog cancelled") { + return RetTask::cancelled(); + } + return RetTask::immediate(Err(pickresult.unwrapErr())); } else { - result = Ok(path); + return RetTask::immediate(Ok(path)); } - return RetTask::immediate(std::move(result)); } Task>> file::pickMany(FilePickOptions const& options) { diff --git a/loader/src/ui/mods/GeodeStyle.cpp b/loader/src/ui/mods/GeodeStyle.cpp index 920244c9..3fcd4f96 100644 --- a/loader/src/ui/mods/GeodeStyle.cpp +++ b/loader/src/ui/mods/GeodeStyle.cpp @@ -18,6 +18,7 @@ $on_mod(Loaded) { ColorProvider::get()->define("mod-list-updates-available-bg-2"_spr, { 45, 110, 222, 255 }); ColorProvider::get()->define("mod-list-errors-found"_spr, { 235, 35, 112, 255 }); ColorProvider::get()->define("mod-list-errors-found-2"_spr, { 245, 27, 27, 255 }); + ColorProvider::get()->define("mod-list-gray"_spr, { 205, 205, 205, 255 }); ColorProvider::get()->define("mod-list-tab-deselected-bg"_spr, { 26, 24, 29, 255 }); ColorProvider::get()->define("mod-list-tab-selected-bg"_spr, { 168, 147, 185, 255 }); ColorProvider::get()->define("mod-list-tab-selected-bg-alt"_spr, { 147, 163, 185, 255 }); diff --git a/loader/src/ui/mods/GeodeStyle.hpp b/loader/src/ui/mods/GeodeStyle.hpp index 21429d72..5a9375b2 100644 --- a/loader/src/ui/mods/GeodeStyle.hpp +++ b/loader/src/ui/mods/GeodeStyle.hpp @@ -19,8 +19,8 @@ enum class GeodePopupStyle { template class GeodePopup : public Popup { protected: - bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default) { - const bool geodeTheme = Mod::get()->template getSettingValue("enable-geode-theme"); + bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default, bool forceDisableTheme = false) { + const bool geodeTheme = !forceDisableTheme && Mod::get()->template getSettingValue("enable-geode-theme"); const char* bg; switch (style) { default: diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index 1b83dbcd..ce7562f8 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -90,6 +90,11 @@ bool ModsStatusNode::init() { m_downloadListener.bind([this](auto) { this->updateState(); }); + m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3* ev) { + this->updateState(); + return ListenerResult::Propagate; + }); + this->updateState(); return true; diff --git a/loader/src/ui/mods/ModsLayer.hpp b/loader/src/ui/mods/ModsLayer.hpp index 7e7de630..35e36b97 100644 --- a/loader/src/ui/mods/ModsLayer.hpp +++ b/loader/src/ui/mods/ModsLayer.hpp @@ -12,6 +12,7 @@ #include "sources/ModListSource.hpp" #include "UpdateModListState.hpp" #include +#include using namespace geode::prelude; @@ -39,6 +40,7 @@ protected: EventListener m_updateStateListener; EventListener m_downloadListener; DownloadState m_lastState = DownloadState::None; + EventListener> m_settingNodeListener; bool init(); void updateState(); diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index 063b1a75..e35255c8 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -298,6 +298,11 @@ bool ModItem::init(ModSource&& source) { m_downloadListener.bind([this](auto) { this->updateState(); }); m_downloadListener.setFilter(server::ModDownloadFilter(m_source.getID())); + m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) { + this->updateState(); + return ListenerResult::Propagate; + }); + return true; } diff --git a/loader/src/ui/mods/list/ModItem.hpp b/loader/src/ui/mods/list/ModItem.hpp index 717c030c..f5f9fc90 100644 --- a/loader/src/ui/mods/list/ModItem.hpp +++ b/loader/src/ui/mods/list/ModItem.hpp @@ -35,6 +35,7 @@ protected: EventListener>> m_checkUpdateListener; EventListener m_downloadListener; std::optional m_availableUpdate; + EventListener> m_settingNodeListener; /** * @warning Make sure `getMetadata` and `createModLogo` are callable diff --git a/loader/src/ui/mods/list/ModList.cpp b/loader/src/ui/mods/list/ModList.cpp index 8ec95174..3335f7d7 100644 --- a/loader/src/ui/mods/list/ModList.cpp +++ b/loader/src/ui/mods/list/ModList.cpp @@ -577,6 +577,7 @@ void ModList::updateState() { filterSpr->setState(!isDefaultQuery); auto clearSpr = static_cast(m_clearFiltersBtn->getNormalImage()); + m_clearFiltersBtn->setEnabled(!isDefaultQuery); clearSpr->setColor(isDefaultQuery ? ccGRAY : ccWHITE); clearSpr->setOpacity(isDefaultQuery ? 90 : 255); clearSpr->getTopSprite()->setColor(isDefaultQuery ? ccGRAY : ccWHITE); diff --git a/loader/src/ui/mods/popups/ModPopup.cpp b/loader/src/ui/mods/popups/ModPopup.cpp index 82006b0b..03427bf3 100644 --- a/loader/src/ui/mods/popups/ModPopup.cpp +++ b/loader/src/ui/mods/popups/ModPopup.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include "ConfirmUninstallPopup.hpp" @@ -557,6 +558,13 @@ bool ModPopup::setup(ModSource&& src) { mainContainer->updateLayout(); m_mainLayer->addChildAtPosition(mainContainer, Anchor::Center); + m_settingsBG = CCScale9Sprite::create("square02b_001.png"); + m_settingsBG->setColor({ 0, 0, 0 }); + m_settingsBG->setOpacity(75); + m_settingsBG->setScale(.3f); + m_settingsBG->setContentSize(ccp(35, 30) / linksBG->getScale()); + m_buttonMenu->addChildAtPosition(m_settingsBG, Anchor::BottomLeft, ccp(28, 25)); + auto settingsSpr = createGeodeCircleButton(CCSprite::createWithSpriteFrameName("settings.png"_spr)); settingsSpr->setScale(.6f); auto settingsBtn = CCMenuItemSpriteExtra::create( @@ -601,15 +609,30 @@ bool ModPopup::setup(ModSource&& src) { m_downloadListener.bind([this](auto) { this->updateState(); }); m_downloadListener.setFilter(m_source.getID()); + m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) { + this->updateState(); + return ListenerResult::Propagate; + }); + return true; } void ModPopup::updateState() { auto asMod = m_source.asMod(); auto wantsRestart = m_source.wantsRestart(); + auto wantsRestartBecauseOfSettings = asMod && ModSettingsManager::from(asMod)->restartRequired(); - m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0)); - m_installBG->setOpacity(wantsRestart ? 40 : 75); + m_installBG->setColor((wantsRestart && !wantsRestartBecauseOfSettings) ? + to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : + ccBLACK + ); + m_installBG->setOpacity((wantsRestart && !wantsRestartBecauseOfSettings) ? 40 : 75); + m_settingsBG->setColor(wantsRestartBecauseOfSettings ? + to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : + ccBLACK + ); + m_settingsBG->setOpacity(wantsRestartBecauseOfSettings ? 40 : 75); + m_settingsBG->setVisible(wantsRestartBecauseOfSettings); m_restartRequiredLabel->setVisible(wantsRestart); if (!wantsRestart && asMod) { diff --git a/loader/src/ui/mods/popups/ModPopup.hpp b/loader/src/ui/mods/popups/ModPopup.hpp index b91c5092..a6bbf789 100644 --- a/loader/src/ui/mods/popups/ModPopup.hpp +++ b/loader/src/ui/mods/popups/ModPopup.hpp @@ -30,6 +30,7 @@ protected: CCMenuItemSpriteExtra* m_cancelBtn; CCLabelBMFont* m_installStatusLabel; CCScale9Sprite* m_installBG; + CCScale9Sprite* m_settingsBG; CCLabelBMFont* m_enabledStatusLabel; ButtonSprite* m_restartRequiredLabel; CCNode* m_rightColumn; @@ -40,6 +41,7 @@ protected: EventListener>> m_checkUpdateListener; EventListener m_updateStateListener; EventListener m_downloadListener; + EventListener> m_settingNodeListener; bool setup(ModSource&& src) override; void updateState(); diff --git a/loader/src/ui/mods/settings/ModSettingsPopup.cpp b/loader/src/ui/mods/settings/ModSettingsPopup.cpp index 573218dc..ff2d9bda 100644 --- a/loader/src/ui/mods/settings/ModSettingsPopup.cpp +++ b/loader/src/ui/mods/settings/ModSettingsPopup.cpp @@ -1,12 +1,38 @@ #include "ModSettingsPopup.hpp" - #include #include #include +#include #include #include #include -#include "GeodeSettingNode.hpp" +#include +// needed for weightedFuzzyMatch +#include + +static bool matchSearch(SettingNodeV3* node, std::string const& query) { + if (typeinfo_cast(node)) { + return true; + } + bool addToList = false; + auto setting = node->getSetting(); + double weighted = 0; + if (auto name = setting->getName()) { + addToList |= weightedFuzzyMatch(setting->getKey(), query, 0.5, weighted); + addToList |= weightedFuzzyMatch(*name, query, 1, weighted); + } + // If there's no name, give full weight to key + else { + addToList |= weightedFuzzyMatch(setting->getKey(), query, 1, weighted); + } + if (auto desc = setting->getDescription()) { + addToList |= weightedFuzzyMatch(*desc, query, 0.02, weighted); + } + if (weighted < 2) { + addToList = false; + } + return addToList; +} bool ModSettingsPopup::setup(Mod* mod) { m_noElasticity = true; @@ -20,78 +46,95 @@ bool ModSettingsPopup::setup(Mod* mod) { auto layerBG = CCLayerColor::create({ 0, 0, 0, 75 }); layerBG->setContentSize(layerSize); - m_mainLayer->addChildAtPosition(layerBG, Anchor::Center, -layerSize / 2); + layerBG->ignoreAnchorPointForPosition(false); + m_mainLayer->addChildAtPosition(layerBG, Anchor::Center); - auto layer = ScrollLayer::create(layerSize); - layer->setTouchEnabled(true); + auto searchContainer = CCMenu::create(); + searchContainer->setContentSize({ layerSize.width, 30 }); + + m_searchInput = TextInput::create((layerSize.width - 15) / .7f - 40, "Search Settings..."); + m_searchInput->setTextAlign(TextInputAlign::Left); + m_searchInput->setScale(.7f); + m_searchInput->setCallback([this](auto const&) { + this->updateState(); + m_list->moveToTop(); + }); + m_searchInput->setID("search-input"); + searchContainer->addChildAtPosition(m_searchInput, Anchor::Left, ccp(7.5f, 0), ccp(0, .5f)); + + auto searchClearSpr = GeodeSquareSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"); + searchClearSpr->setScale(.45f); + m_searchClearBtn = CCMenuItemSpriteExtra::create( + searchClearSpr, this, menu_selector(ModSettingsPopup::onClearSearch) + ); + m_searchClearBtn->setID("clear-search-button"); + searchContainer->addChildAtPosition(m_searchClearBtn, Anchor::Right, ccp(-20, 0)); + + layerBG->addChildAtPosition(searchContainer, Anchor::Top, ccp(0, 0), ccp(.5f, 1)); + + m_list = ScrollLayer::create(layerSize - ccp(0, searchContainer->getContentHeight())); + m_list->setTouchEnabled(true); - float totalHeight = .0f; - std::vector rendered; - bool hasBG = true; for (auto& key : mod->getSettingKeys()) { - SettingNode* node; - if (auto sett = mod->getSetting(key)) { + SettingNodeV3* node; + if (auto sett = mod->getSettingV3(key)) { node = sett->createNode(layerSize.width); } else { - node = CustomSettingPlaceholderNode::create(key, layerSize.width); + node = UnresolvedCustomSettingNodeV3::create(key, mod, layerSize.width); } - node->setDelegate(this); + + // auto separator = CCLayerColor::create({ 0, 0, 0, 50 }, layerSize.width, 1.f); + // separator->setOpacity(bg ? 100 : 50); + // separator->ignoreAnchorPointForPosition(false); + // bg->addChildAtPosition(separator, Anchor::Bottom, ccp(0, 0), ccp(.5f, .5f)); - totalHeight += node->getScaledContentSize().height; - - if (hasBG) { - auto bg = CCLayerColor::create({ 0, 0, 0, 50 }); - bg->setContentSize(node->getScaledContentSize()); - bg->setPosition(0.f, -totalHeight); - bg->setZOrder(-10); - layer->m_contentLayer->addChild(bg); - - rendered.push_back(bg); - - hasBG = false; - } - else { - hasBG = true; - } - - node->setPosition(0.f, -totalHeight); - layer->m_contentLayer->addChild(node); - - auto separator = CCLayerColor::create( - { 0, 0, 0, static_cast(hasBG ? 100 : 50) }, layerSize.width, 1.f - ); - separator->setPosition(0.f, -totalHeight); - layer->m_contentLayer->addChild(separator); - rendered.push_back(separator); - - rendered.push_back(node); m_settings.push_back(node); + m_list->m_contentLayer->addChild(node); } - if (totalHeight < layerSize.height) { - totalHeight = layerSize.height; - } - for (auto& node : rendered) { - node->setPositionY(node->getPositionY() + totalHeight); - } - layer->m_contentLayer->setContentSize({ layerSize.width, totalHeight }); - layer->moveToTop(); + m_list->m_contentLayer->setLayout( + ColumnLayout::create() + ->setAxisReverse(true) + ->setAutoGrowAxis(layerSize.height) + ->setCrossAxisOverflow(false) + ->setAxisAlignment(AxisAlignment::End) + ->setGap(0) + ); + m_list->moveToTop(); - layerBG->addChild(layer); + layerBG->addChildAtPosition(m_list, Anchor::BottomLeft); // layer borders - m_mainLayer->addChildAtPosition(createGeodeListBorders({layerSize.width, layerSize.height - 2}), Anchor::Center); + m_mainLayer->addChildAtPosition(createGeodeListBorders(layerSize), Anchor::Center); + + auto scrollBar = Scrollbar::create(m_list); + m_mainLayer->addChildAtPosition( + scrollBar, Anchor::Center, ccp(layerBG->getContentWidth() / 2 + 10, 0) + ); // buttons + m_applyMenu = CCMenu::create(); + m_applyMenu->setContentWidth(150); + m_applyMenu->setLayout(RowLayout::create()); + m_applyMenu->getLayout()->ignoreInvisibleChildren(true); + + auto restartBtnSpr = createGeodeButton("Restart Now", true); + restartBtnSpr->setScale(.6f); + m_restartBtn = CCMenuItemSpriteExtra::create( + restartBtnSpr, this, menu_selector(ModSettingsPopup::onRestart) + ); + m_applyMenu->addChildAtPosition(m_restartBtn, Anchor::Bottom, ccp(0, 20)); + m_applyBtnSpr = createGeodeButton("Apply", true); m_applyBtnSpr->setScale(.6f); - m_applyBtn = CCMenuItemSpriteExtra::create( m_applyBtnSpr, this, menu_selector(ModSettingsPopup::onApply) ); - m_buttonMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20)); + m_applyMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20)); + + m_mainLayer->addChildAtPosition(m_applyMenu, Anchor::Bottom, ccp(0, 20)); auto resetBtnSpr = createGeodeButton("Reset All", true); resetBtnSpr->setScale(.6f); @@ -101,15 +144,34 @@ bool ModSettingsPopup::setup(Mod* mod) { ); m_buttonMenu->addChildAtPosition(resetBtn, Anchor::BottomLeft, ccp(45, 20)); - auto openDirBtnSpr = createGeodeButton("Open Folder", true); - openDirBtnSpr->setScale(.6f); + auto configFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png"); + m_openConfigDirBtnSpr = createGeodeButton(configFolderSpr, ""); + m_openConfigDirBtnSpr->setScale(.6f); + m_openConfigDirBtnSpr->getIcon()->setScale(m_openConfigDirBtnSpr->getIcon()->getScale() * 1.4f); + auto openConfigDirBtn = CCMenuItemSpriteExtra::create( + m_openConfigDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenConfigDirectory) + ); + m_buttonMenu->addChildAtPosition(openConfigDirBtn, Anchor::BottomRight, ccp(-50, 20)); + auto settingFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png"); + auto settingFolderSprSub = CCSprite::createWithSpriteFrameName("settings.png"_spr); + settingFolderSprSub->setColor(ccBLACK); + settingFolderSprSub->setOpacity(155); + settingFolderSprSub->setScale(.55f); + settingFolderSpr->addChildAtPosition(settingFolderSprSub, Anchor::Center, ccp(0, -3)); + auto openDirBtnSpr = createGeodeButton(settingFolderSpr, ""); + openDirBtnSpr->setScale(.6f); + openDirBtnSpr->getIcon()->setScale(openDirBtnSpr->getIcon()->getScale() * 1.4f); auto openDirBtn = CCMenuItemSpriteExtra::create( openDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenSaveDirectory) ); - m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-53, 20)); + m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-20, 20)); - this->settingValueChanged(nullptr); + m_changeListener.bind([this](auto* ev) { + this->updateState(ev->getNode()); + return ListenerResult::Propagate; + }); + this->updateState(); return true; } @@ -126,7 +188,21 @@ void ModSettingsPopup::onApply(CCObject*) { FLAlertLayer::create("Info", "No changes have been made.", "OK")->show(); } } +void ModSettingsPopup::onRestart(CCObject*) { + // Update button state to let user know it's restarting but it might take a bit + m_restartBtn->setEnabled(false); + static_cast(m_restartBtn->getNormalImage())->setString("Restarting..."); + m_restartBtn->updateSprite(); + // Actually restart + Loader::get()->queueInMainThread([] { + // Delayed by 2 frames - one is needed to render the "Restarting text" + Loader::get()->queueInMainThread([] { + // the other never finishes rendering because the game actually restarts at this point + game::restart(); + }); + }); +} void ModSettingsPopup::onResetAll(CCObject*) { createQuickPopup( "Reset All", @@ -142,27 +218,89 @@ void ModSettingsPopup::onResetAll(CCObject*) { } ); } - -void ModSettingsPopup::settingValueCommitted(SettingNode*) { - if (this->hasUncommitted()) { - m_applyBtnSpr->setColor({0xff, 0xff, 0xff}); - m_applyBtn->setEnabled(true); - } - else { - m_applyBtnSpr->setColor({0x44, 0x44, 0x44}); - m_applyBtn->setEnabled(false); - } +void ModSettingsPopup::onOpenSaveDirectory(CCObject*) { + file::openFolder(m_mod->getSaveDir()); +} +void ModSettingsPopup::onOpenConfigDirectory(CCObject*) { + file::openFolder(m_mod->getConfigDir()); + this->updateState(); +} +void ModSettingsPopup::onClearSearch(CCObject*) { + m_searchInput->setString(""); + this->updateState(); + m_list->moveToTop(); } -void ModSettingsPopup::settingValueChanged(SettingNode*) { +void ModSettingsPopup::updateState(SettingNodeV3* invoker) { + auto search = m_searchInput->getString(); + auto hasSearch = !search.empty(); + + m_restartBtn->setVisible(ModSettingsManager::from(m_mod)->restartRequired()); + m_applyMenu->updateLayout(); + + auto configDirExists = std::filesystem::exists(m_mod->getConfigDir(false)); + m_openConfigDirBtnSpr->setCascadeColorEnabled(true); + m_openConfigDirBtnSpr->setCascadeOpacityEnabled(true); + m_openConfigDirBtnSpr->setColor(configDirExists ? ccWHITE : ccGRAY); + m_openConfigDirBtnSpr->setOpacity(configDirExists ? 255 : 155); + + auto listPosBefore = m_list->m_contentLayer->getPositionY(); + auto listHeightBefore = m_list->m_contentLayer->getContentHeight(); + + // Update search visibility + all settings with "enable-if" schemes + + // checkerboard BG + TitleSettingNodeV3* lastTitle = nullptr; + bool bg = false; + for (auto& sett : m_settings) { + if (auto asTitle = typeinfo_cast(sett.data())) { + lastTitle = asTitle; + } + sett->removeFromParent(); + if ( + // Show if the setting is not a title and is not subject to a collapsed title + !(lastTitle && lastTitle != sett && lastTitle->isCollapsed()) && + // Show if there's no search query or if the setting matches it + (!hasSearch || matchSearch(sett, search)) + ) { + m_list->m_contentLayer->addChild(sett); + sett->setDefaultBGColor(ccc4(0, 0, 0, bg ? 60 : 20)); + bg = !bg; + } + // Avoid infinite loops + if (sett == invoker) { + continue; + } + if (sett->getSetting() && sett->getSetting()->getEnableIf()) { + sett->updateState(nullptr); + } + } + m_list->m_contentLayer->updateLayout(); + + // Preserve relative list position if something has been collapsed + m_list->m_contentLayer->setPositionY( + listPosBefore + + (listHeightBefore - m_list->m_contentLayer->getContentHeight()) + ); + + m_applyBtnSpr->setCascadeColorEnabled(true); + m_applyBtnSpr->setCascadeOpacityEnabled(true); if (this->hasUncommitted()) { - m_applyBtnSpr->setColor({0xff, 0xff, 0xff}); + m_applyBtnSpr->setColor(ccWHITE); + m_applyBtnSpr->setOpacity(255); m_applyBtn->setEnabled(true); } else { - m_applyBtnSpr->setColor({0x44, 0x44, 0x44}); + m_applyBtnSpr->setColor(ccGRAY); + m_applyBtnSpr->setOpacity(155); m_applyBtn->setEnabled(false); } + + auto clearSpr = static_cast(m_searchClearBtn->getNormalImage()); + m_searchClearBtn->setEnabled(hasSearch); + clearSpr->setColor(hasSearch ? ccWHITE : ccGRAY); + clearSpr->setOpacity(hasSearch ? 255 : 90); + clearSpr->getTopSprite()->setColor(hasSearch ? ccWHITE : ccGRAY); + clearSpr->getTopSprite()->setOpacity(hasSearch ? 255 : 90); } bool ModSettingsPopup::hasUncommitted() const { @@ -175,28 +313,26 @@ bool ModSettingsPopup::hasUncommitted() const { } void ModSettingsPopup::onClose(CCObject* sender) { - if (sender && this->hasUncommitted()) { + if (this->hasUncommitted()) { createQuickPopup( "Unsaved Changes", "You have unsaved changes! Are you sure you " "want to exit?", "Cancel", "Discard", [this](FLAlertLayer*, bool btn2) { - if (btn2) this->onClose(nullptr); + if (btn2) { + GeodePopup::onClose(nullptr); + } } ); return; } - Popup::onClose(sender); -} - -void ModSettingsPopup::onOpenSaveDirectory(CCObject*) { - file::openFolder(m_mod->getSaveDir()); + GeodePopup::onClose(sender); } ModSettingsPopup* ModSettingsPopup::create(Mod* mod) { auto ret = new ModSettingsPopup(); - if (ret->init(440.f, 280.f, mod)) { + if (ret->init(440, 280, mod)) { ret->autorelease(); return ret; } diff --git a/loader/src/ui/mods/settings/ModSettingsPopup.hpp b/loader/src/ui/mods/settings/ModSettingsPopup.hpp index f61cb3ce..147149a2 100644 --- a/loader/src/ui/mods/settings/ModSettingsPopup.hpp +++ b/loader/src/ui/mods/settings/ModSettingsPopup.hpp @@ -1,28 +1,36 @@ #pragma once -#include +#include #include #include #include "../GeodeStyle.hpp" using namespace geode::prelude; -class ModSettingsPopup : public GeodePopup, public SettingNodeDelegate { +class ModSettingsPopup : public GeodePopup { protected: Mod* m_mod; - std::vector m_settings; + ScrollLayer* m_list; + std::vector> m_settings; + CCMenu* m_applyMenu; CCMenuItemSpriteExtra* m_applyBtn; + CCMenuItemSpriteExtra* m_restartBtn; ButtonSprite* m_applyBtnSpr; - - void settingValueChanged(SettingNode*) override; - void settingValueCommitted(SettingNode*) override; + IconButtonSprite* m_openConfigDirBtnSpr; + TextInput* m_searchInput; + CCMenuItemSpriteExtra* m_searchClearBtn; + EventListener> m_changeListener; bool setup(Mod* mod) override; + void updateState(SettingNodeV3* invoker = nullptr); bool hasUncommitted() const; void onClose(CCObject*) override; void onApply(CCObject*); + void onRestart(CCObject*); void onResetAll(CCObject*); void onOpenSaveDirectory(CCObject*); + void onOpenConfigDirectory(CCObject*); + void onClearSearch(CCObject*); public: static ModSettingsPopup* create(Mod* mod); diff --git a/loader/src/ui/mods/sources/ModListSource.cpp b/loader/src/ui/mods/sources/ModListSource.cpp index 19ced217..e19e1c43 100644 --- a/loader/src/ui/mods/sources/ModListSource.cpp +++ b/loader/src/ui/mods/sources/ModListSource.cpp @@ -1,5 +1,6 @@ #include "ModListSource.hpp" #include +#include #define FTS_FUZZY_MATCH_IMPLEMENTATION #include @@ -90,6 +91,9 @@ bool ModListSource::isRestartRequired() { if (mod->getRequestedAction() != ModRequestedAction::None) { return true; } + if (ModSettingsManager::from(mod)->restartRequired()) { + return true; + } } if (server::ModDownloadManager::get()->wantsRestart()) { return true; diff --git a/loader/src/ui/mods/sources/ModSource.cpp b/loader/src/ui/mods/sources/ModSource.cpp index 0c5ef5d3..fe7bb2e0 100644 --- a/loader/src/ui/mods/sources/ModSource.cpp +++ b/loader/src/ui/mods/sources/ModSource.cpp @@ -1,6 +1,7 @@ #include "ModSource.hpp" #include +#include #include #include #include @@ -106,7 +107,8 @@ bool ModSource::wantsRestart() const { } return std::visit(makeVisitor { [](Mod* mod) { - return mod->getRequestedAction() != ModRequestedAction::None; + return mod->getRequestedAction() != ModRequestedAction::None || + ModSettingsManager::from(mod)->restartRequired(); }, [](server::ServerModMetadata const& metdata) { return false; diff --git a/loader/src/ui/nodes/ColorPickPopup.cpp b/loader/src/ui/nodes/ColorPickPopup.cpp index 01083a6f..7a6b46c3 100644 --- a/loader/src/ui/nodes/ColorPickPopup.cpp +++ b/loader/src/ui/nodes/ColorPickPopup.cpp @@ -10,6 +10,55 @@ using namespace geode::prelude; +// class ColorPickPopupEvent::Impl final { +// public: +// ColorPickPopup* popup; +// ccColor4B color; +// bool closed = false; +// }; + +// ColorPickPopupEvent::ColorPickPopupEvent(ColorPickPopup* popup, ccColor4B const& color) +// : m_impl(std::make_shared()) +// { +// m_impl->popup = popup; +// m_impl->color = color; +// } +// ColorPickPopupEvent::~ColorPickPopupEvent() = default; + +// ColorPickPopup* ColorPickPopupEvent::getPopup() const { +// return m_impl->popup; +// } +// ccColor4B ColorPickPopupEvent::getColor() const { +// return m_impl->color; +// } +// bool ColorPickPopupEvent::isPopupClosed() const { +// return m_impl->closed; +// } + +// class ColorPickPopupEventFilter::Impl final { +// public: +// ColorPickPopup* popup; +// }; + +// ListenerResult ColorPickPopupEventFilter::handle(utils::MiniFunction fn, ColorPickPopupEvent* event) { +// if (event->getPopup() == m_impl->popup) { +// if (event->isPopupClosed()) { +// m_impl->popup = nullptr; +// } +// else { +// fn(event); +// } +// } +// return ListenerResult::Propagate; +// } +// ColorPickPopupEventFilter::ColorPickPopupEventFilter() : ColorPickPopupEventFilter(nullptr) {} +// ColorPickPopupEventFilter::ColorPickPopupEventFilter(ColorPickPopup* popup) +// : m_impl(std::make_shared()) +// { +// m_impl->popup = popup; +// } +// ColorPickPopupEventFilter::~ColorPickPopupEventFilter() = default; + bool ColorPickPopup::setup(ccColor4B const& color, bool isRGBA) { m_noElasticity = true; m_color = color; @@ -334,7 +383,9 @@ void ColorPickPopup::updateState(CCNode* except) { } m_resetBtn->setVisible(m_originalColor != m_color); m_newColorSpr->setColor(to3B(m_color)); - if (m_delegate) m_delegate->updateColor(m_color); + if (m_delegate) { + m_delegate->updateColor(m_color); + } } void ColorPickPopup::onOpacitySlider(CCObject* sender) { diff --git a/loader/src/utils/JsonValidation.cpp b/loader/src/utils/JsonValidation.cpp index 4e654041..b211ae89 100644 --- a/loader/src/utils/JsonValidation.cpp +++ b/loader/src/utils/JsonValidation.cpp @@ -2,201 +2,50 @@ using namespace geode::prelude; - matjson::Value& JsonMaybeSomething::json() { return m_json; } - JsonMaybeSomething::JsonMaybeSomething( JsonChecker& checker, matjson::Value& json, std::string const& hierarchy, bool hasValue ) : m_checker(checker), m_json(json), m_hierarchy(hierarchy), m_hasValue(hasValue) {} - bool JsonMaybeSomething::isError() const { return m_checker.isError() || !m_hasValue; } - std::string JsonMaybeSomething::getError() const { return m_checker.getError(); } - JsonMaybeSomething::operator bool() const { return !isError(); } - void JsonMaybeSomething::setError(std::string const& error) { m_checker.m_result = error; } - JsonMaybeValue::JsonMaybeValue( JsonChecker& checker, matjson::Value& json, std::string const& hierarchy, bool hasValue -) : - JsonMaybeSomething(checker, json, hierarchy, hasValue) {} - +) : JsonMaybeSomething(checker, json, hierarchy, hasValue) {} JsonMaybeSomething& JsonMaybeValue::self() { return *static_cast(this); } -// template -// template -// JsonMaybeValue& JsonMaybeValue::as() { -// if (this->isError()) return *this; -// if (!jsonConvertibleTo(self().m_json.type(), T)) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// self().m_json.type_name() + "\", expected \"" + -// jsonValueTypeToString(T) + "\"" -// ); -// } -// m_inferType = false; -// return *this; -// } - - JsonMaybeValue& JsonMaybeValue::array() { this->as(); return *this; } -// template -// template -// JsonMaybeValue JsonMaybeValue::asOneOf() { -// if (this->isError()) return *this; -// bool isOneOf = (... || jsonConvertibleTo(self().m_json.type(), T)); -// if (!isOneOf) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// self().m_json.type_name() + "\", expected one of \"" + -// (jsonValueTypeToString(T), ...) + "\"" -// ); -// } -// m_inferType = false; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::is() { -// if (this->isError()) return *this; -// self().m_hasValue = jsonConvertibleTo(self().m_json.type(), T); -// m_inferType = false; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::validate(JsonValueValidator validator) { -// if (this->isError()) return *this; -// try { -// if (!validator(self().m_json.template get())) { -// this->setError(self().m_hierarchy + ": Invalid value format"); -// } -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::inferType() { -// if (this->isError() || !m_inferType) return *this; -// return this->as()>(); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::intoRaw(T& target) { -// if (this->isError()) return *this; -// target = self().m_json; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::into(T& target) { -// return this->intoAs(target); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::into(std::optional& target) { -// return this->intoAs>(target); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::intoAs(T& target) { -// this->inferType(); -// if (this->isError()) return *this; -// try { -// target = self().m_json.template get(); -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return *this; -// } - -// template -// template -// T JsonMaybeValue::get() { -// this->inferType(); -// if (this->isError()) return T(); -// try { -// return self().m_json.template get(); -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type to get \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return T(); -// } - - JsonMaybeObject JsonMaybeValue::obj() { this->as(); return JsonMaybeObject(self().m_checker, self().m_json, self().m_hierarchy, self().m_hasValue); } -// template -// template -// struct JsonMaybeValue::Iterator { -// std::vector m_values; - -// using iterator = typename std::vector::iterator; -// using const_iterator = typename std::vector::const_iterator; - -// iterator begin() { -// return m_values.begin(); -// } -// iterator end() { -// return m_values.end(); -// } - -// const_iterator begin() const { -// return m_values.begin(); -// } -// const_iterator end() const { -// return m_values.end(); -// } -// }; - - JsonMaybeValue JsonMaybeValue::at(size_t i) { this->as(); if (this->isError()) return *this; @@ -216,7 +65,6 @@ JsonMaybeValue JsonMaybeValue::at(size_t i) { ); } - typename JsonMaybeValue::template Iterator JsonMaybeValue::iterate() { this->as(); Iterator iter; @@ -232,7 +80,6 @@ typename JsonMaybeValue::template Iterator JsonMaybeValue::itera return iter; } - typename JsonMaybeValue::template Iterator> JsonMaybeValue::items() { this->as(); Iterator> iter; @@ -248,33 +95,26 @@ typename JsonMaybeValue::template Iterator(this); } - void JsonMaybeObject::addKnownKey(std::string const& key) { m_knownKeys.insert(key); } - matjson::Value& JsonMaybeObject::json() { return self().m_json; } - JsonMaybeValue JsonMaybeObject::emptyValue() { return JsonMaybeValue(self().m_checker, self().m_json, "", false); } - JsonMaybeValue JsonMaybeObject::has(std::string const& key) { this->addKnownKey(key); if (this->isError()) return emptyValue(); @@ -284,7 +124,6 @@ JsonMaybeValue JsonMaybeObject::has(std::string const& key) { return JsonMaybeValue(self().m_checker, self().m_json[key], key, true); } - JsonMaybeValue JsonMaybeObject::needs(std::string const& key) { this->addKnownKey(key); if (this->isError()) return emptyValue(); @@ -295,7 +134,6 @@ JsonMaybeValue JsonMaybeObject::needs(std::string const& key) { return JsonMaybeValue(self().m_checker, self().m_json[key], key, true); } - // TODO: gross hack :3 (ctrl+f this comment to find the other part) extern bool s_jsonCheckerShouldCheckUnknownKeys; bool s_jsonCheckerShouldCheckUnknownKeys = true; @@ -309,20 +147,274 @@ void JsonMaybeObject::checkUnknownKeys() { } } - JsonChecker::JsonChecker(matjson::Value& json) : m_json(json), m_result(std::monostate()) {} - bool JsonChecker::isError() const { return std::holds_alternative(m_result); } - std::string JsonChecker::getError() const { return std::get(m_result); } - JsonMaybeValue JsonChecker::root(std::string const& hierarchy) { return JsonMaybeValue(*this, m_json, hierarchy, true); } + +// This is used for null JsonExpectedValues (for example when doing +// `json.has("key")` where "key" doesn't exist) +static matjson::Value NULL_SCOPED_VALUE = nullptr; + +class JsonExpectedValue::Impl final { +public: + // Values shared between JsonExpectedValues related to the same JSON + struct Shared final { + matjson::Value originalJson; + std::optional error; + std::string rootScopeName; + + Shared(matjson::Value const& json, std::string_view rootScopeName) + : originalJson(json), rootScopeName(rootScopeName) {} + }; + + // this may be null if the JsonExpectedValue is a "null" value + std::shared_ptr shared; + matjson::Value& scope; + std::string scopeName; + std::string key; + std::unordered_set knownKeys; + + Impl() + : shared(nullptr), + scope(NULL_SCOPED_VALUE) + {} + + // Create a root Impl + Impl(std::shared_ptr shared) + : shared(shared), + scope(shared->originalJson), + scopeName(shared->rootScopeName) + {} + + // Create a derived Impl + Impl(Impl* from, matjson::Value& scope, std::string_view key) + : shared(from->shared), + scope(scope), + scopeName(fmt::format("{}.{}", from->scopeName, key)), + key(key) + {} +}; + +JsonExpectedValue::JsonExpectedValue() + : m_impl(std::make_unique()) +{} +JsonExpectedValue::JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key) + : m_impl(std::make_unique(from, scope, key)) +{} +JsonExpectedValue::JsonExpectedValue(matjson::Value const& json, std::string_view rootScopeName) + : m_impl(std::make_unique(std::make_shared(json, rootScopeName))) +{} +JsonExpectedValue::~JsonExpectedValue() {} + +JsonExpectedValue::JsonExpectedValue(JsonExpectedValue&&) = default; +JsonExpectedValue& JsonExpectedValue::operator=(JsonExpectedValue&&) = default; + +const char* JsonExpectedValue::matJsonTypeToString(matjson::Type ty) { + switch (ty) { + case matjson::Type::Null: return "null"; + case matjson::Type::Bool: return "bool"; + case matjson::Type::Number: return "number"; + case matjson::Type::String: return "string"; + case matjson::Type::Array: return "array"; + case matjson::Type::Object: return "object"; + default: return "unknown"; + } +} + +matjson::Value const& JsonExpectedValue::getJSONRef() const { + return m_impl->scope; +} +matjson::Value JsonExpectedValue::json() const { + return m_impl->scope; +} +std::string JsonExpectedValue::key() const { + return m_impl->key; +} + +bool JsonExpectedValue::hasError() const { + return !m_impl->shared || m_impl->shared->error.has_value(); +} +void JsonExpectedValue::setError(std::string_view error) { + m_impl->shared->error.emplace(fmt::format("[{}]: {}", m_impl->scopeName, error)); +} + +bool JsonExpectedValue::is(matjson::Type type) const { + if (this->hasError()) return false; + return m_impl->scope.type() == type; +} +bool JsonExpectedValue::isNull() const { + return this->is(matjson::Type::Null); +} +bool JsonExpectedValue::isBool() const { + return this->is(matjson::Type::Bool); +} +bool JsonExpectedValue::isNumber() const { + return this->is(matjson::Type::Number); +} +bool JsonExpectedValue::isString() const { + return this->is(matjson::Type::String); +} +bool JsonExpectedValue::isArray() const { + return this->is(matjson::Type::Array); +} +bool JsonExpectedValue::isObject() const { + return this->is(matjson::Type::Object); +} +JsonExpectedValue& JsonExpectedValue::assertIs(matjson::Type type) { + if (this->hasError()) return *this; + if (m_impl->scope.type() != type) { + this->setError( + "invalid type {}, expected {}", + matJsonTypeToString(m_impl->scope.type()), + matJsonTypeToString(type) + ); + } + return *this; +} +JsonExpectedValue& JsonExpectedValue::assertIsNull() { + return this->assertIs(matjson::Type::Null); +} +JsonExpectedValue& JsonExpectedValue::assertIsBool() { + return this->assertIs(matjson::Type::Bool); +} +JsonExpectedValue& JsonExpectedValue::assertIsNumber() { + return this->assertIs(matjson::Type::Number); +} +JsonExpectedValue& JsonExpectedValue::assertIsString() { + return this->assertIs(matjson::Type::String); +} +JsonExpectedValue& JsonExpectedValue::assertIsArray() { + return this->assertIs(matjson::Type::Array); +} +JsonExpectedValue& JsonExpectedValue::assertIsObject() { + return this->assertIs(matjson::Type::Object); +} +JsonExpectedValue& JsonExpectedValue::assertIs(std::initializer_list types) { + if (this->hasError()) return *this; + if (!std::any_of(types.begin(), types.end(), [this](matjson::Type t) { return t == m_impl->scope.type(); })) { + this->setError( + "invalid type {}, expected either {}", + matJsonTypeToString(m_impl->scope.type()), + ranges::join(ranges::map>(types, [](matjson::Type t) { + return matJsonTypeToString(t); + }), " or ") + ); + } + return *this; +} + +JsonExpectedValue JsonExpectedValue::has(std::string_view key) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Object)) { + return JsonExpectedValue(); + } + m_impl->knownKeys.insert(std::string(key)); + if (!m_impl->scope.contains(key)) { + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key); +} +JsonExpectedValue JsonExpectedValue::needs(std::string_view key) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Object)) { + return JsonExpectedValue(); + } + m_impl->knownKeys.insert(std::string(key)); + if (!m_impl->scope.contains(key)) { + this->setError("missing required key {}", key); + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key); +} +std::vector> JsonExpectedValue::properties() { + if (this->hasError()) { + return std::vector>(); + } + if (!this->assertIs(matjson::Type::Object)) { + return std::vector>(); + } + std::vector> res; + for (auto& [k, v] : m_impl->scope.as_object()) { + res.push_back(std::make_pair(k, JsonExpectedValue(m_impl.get(), v, k))); + } + return res; +} +void JsonExpectedValue::checkUnknownKeys() { + if (this->hasError()) return; + for (auto&& [key, _] : this->properties()) { + if (!m_impl->knownKeys.contains(key)) { + log::warn("{} contains unknown key \"{}\"", m_impl->scopeName, key); + } + } +} + +size_t JsonExpectedValue::length() { + if (this->hasError()) { + return 0; + } + if (!this->assertIs(matjson::Type::Array)) { + return 0; + } + return m_impl->scope.as_array().size(); +} +JsonExpectedValue JsonExpectedValue::at(size_t index) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Array)) { + return JsonExpectedValue(); + } + auto& arr = m_impl->scope.as_array(); + if (arr.size() <= index) { + this->setError( + "array expected to have at least size {}, but its size was only {}", + index + 1, arr.size() + ); + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), arr.at(index), std::to_string(index)); +} +std::vector JsonExpectedValue::items() { + if (this->hasError()) { + return std::vector(); + } + if (!this->assertIs(matjson::Type::Array)) { + return std::vector(); + } + std::vector res; + size_t i = 0; + for (auto& v : m_impl->scope.as_array()) { + res.push_back(JsonExpectedValue(m_impl.get(), v, std::to_string(i++))); + } + return res; +} + +JsonExpectedValue::operator bool() const { + // The shared check is because null values should evaluate to false so `obj.has("key")` evaluates to false + return m_impl->shared && !this->hasError(); +} + +Result<> JsonExpectedValue::ok() { + if (m_impl->shared && m_impl->shared->error) { + return Err(*m_impl->shared->error); + } + return Ok(); +} + +JsonExpectedValue geode::checkJson(matjson::Value const& json, std::string_view rootScopeName) { + return JsonExpectedValue(json, rootScopeName); +} diff --git a/loader/src/utils/PlatformID.cpp b/loader/src/utils/PlatformID.cpp index 02ec045c..183791c3 100644 --- a/loader/src/utils/PlatformID.cpp +++ b/loader/src/utils/PlatformID.cpp @@ -5,6 +5,12 @@ using namespace geode::prelude; PlatformID PlatformID::from(const char* str) { + // todo in v4: this should just be + // "win" -> Windows + // "mac", "mac-intel", "mac-arm" -> Mac + // "ios" -> iOS + // "android", "android32", "android64" -> Android + // no linux switch (hash(str)) { case hash("win"): case hash("Windows"): @@ -33,29 +39,38 @@ PlatformID PlatformID::from(const char* str) { } bool PlatformID::coveredBy(const char* str, PlatformID t) { - switch (hash(str)) { - case hash("win"): return t == PlatformID::Windows; - - case hash("mac"): return t == PlatformID::MacIntel || t == PlatformID::MacArm; - case hash("mac-intel"): return t == PlatformID::MacIntel; - case hash("mac-arm"): return t == PlatformID::MacArm; - - case hash("ios"): return t == PlatformID::iOS; - - case hash("android"): return t == PlatformID::Android32 || t == PlatformID::Android64; - case hash("android32"): return t == PlatformID::Android32; - case hash("android64"): return t == PlatformID::Android64; - - case hash("linux"): return t == PlatformID::Linux; - - default: return false; - } + // todo in v4: this is ridiculously inefficient currently - in v4 just use a flag check! + return ranges::contains(getCovered(str), t); } bool PlatformID::coveredBy(std::string const& str, PlatformID t) { return PlatformID::coveredBy(str.c_str(), t); } +std::vector PlatformID::getCovered(std::string_view str) { + switch (hash(str)) { + case hash("desktop"): return { PlatformID::Windows, PlatformID::MacArm, PlatformID::MacIntel }; + case hash("mobile"): return { PlatformID::iOS, PlatformID::Android32, PlatformID::Android64 }; + + case hash("win"): return { PlatformID::Windows }; + + case hash("mac"): return { PlatformID::MacIntel, PlatformID::MacArm }; + case hash("mac-intel"): return { PlatformID::MacIntel }; + case hash("mac-arm"): return { PlatformID::MacArm }; + + case hash("ios"): return { PlatformID::iOS }; + + case hash("android"): return { PlatformID::Android32, PlatformID::Android64 }; + case hash("android32"): return { PlatformID::Android32 }; + case hash("android64"): return { PlatformID::Android64 }; + + // todo in v4: no linux + case hash("linux"): return { PlatformID::Linux }; + + default: return {}; + } +} + PlatformID PlatformID::from(std::string const& str) { return PlatformID::from(str.c_str()); }