diff --git a/CMakeLists.txt b/CMakeLists.txt index 4491b65c..e3a74b7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,22 @@ cmake_minimum_required(VERSION 3.21 FATAL_ERROR) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build libraries static" FORCE) + +# Read version file(READ VERSION GEODE_VERSION) string(STRIP "${GEODE_VERSION}" GEODE_VERSION) +# Check if version has a tag like v1.0.0-alpha +string(FIND ${GEODE_VERSION} "-" GEODE_VERSION_HAS_TAG) +if (GEODE_VERSION_HAS_TAG) + string(REGEX MATCH "[a-z]+[0-9]?$" GEODE_VERSION_TAG ${GEODE_VERSION}) + string(SUBSTRING "${GEODE_VERSION}" 0 ${GEODE_VERSION_HAS_TAG} GEODE_VERSION) +else() + set(GEODE_VERSION_TAG "") +endif() + +message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG}") + project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 20) diff --git a/VERSION b/VERSION index 7ceb0404..be0aef56 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 \ No newline at end of file +1.0.0-alpha \ No newline at end of file diff --git a/loader/include/Geode/loader/Event.hpp b/loader/include/Geode/loader/Event.hpp index 7184a82b..d641df7d 100644 --- a/loader/include/Geode/loader/Event.hpp +++ b/loader/include/Geode/loader/Event.hpp @@ -89,10 +89,6 @@ namespace geode { this->enable(); } - // todo: maybe add these? - EventListener(EventListener const& other) = delete; - EventListener(EventListener&& other) = delete; - void bind(std::function fn) { m_callback = fn; } diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index c6b84ddf..7d7259a1 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -18,19 +18,6 @@ #include namespace geode { - template - struct HandleToSaved : public T { - Mod* m_mod; - std::string m_key; - - HandleToSaved(std::string const& key, Mod* mod, T const& value) : - T(value), m_key(key), m_mod(mod) {} - - HandleToSaved(HandleToSaved const&) = delete; - HandleToSaved(HandleToSaved&&) = delete; - ~HandleToSaved(); - }; - /** * @class Mod * Represents a Mod ingame. @@ -82,6 +69,14 @@ namespace geode { * Saved values */ nlohmann::json m_saved; + /** + * Setting values + */ + std::unordered_map> m_settings; + /** + * Settings save data. Stored for efficient loading of custom settings + */ + nlohmann::json m_savedSettingsData; /** * Load the platform binary @@ -90,6 +85,8 @@ namespace geode { Result<> unloadPlatformBinary(); Result<> createTempDir(); + void setupSettings(); + // no copying Mod(Mod const&) = delete; Mod operator=(Mod const&) = delete; @@ -146,25 +143,31 @@ namespace geode { ghc::filesystem::path getConfigDir(bool create = true) const; bool hasSettings() const; - decltype(ModInfo::settings) getSettings() const; + std::vector getSettingKeys() const; bool hasSetting(std::string const& key) const; - std::shared_ptr getSetting(std::string const& key) const; + std::optional getSettingDefinition(std::string const& key) const; + SettingValue* getSetting(std::string const& key) const; + void registerCustomSetting( + std::string const& key, + std::unique_ptr value + ); template T getSettingValue(std::string const& key) const { - if (this->hasSetting(key)) { - return geode::getBuiltInSettingValue(this->getSetting(key)); + if (auto sett = this->getSetting(key)) { + return SettingValueSetter::get(sett); } return T(); } template - bool setSettingValue(std::string const& key, T const& value) { - if (this->hasSetting(key)) { - geode::setBuiltInSettingValue(this->getSetting(key), value); - return true; + T setSettingValue(std::string const& key, T const& value) { + if (auto sett = this->getSetting(key)) { + auto old = this->getSettingValue(sett); + SettingValueSetter::set(sett, value); + return old; } - return false; + return T(); } template @@ -194,16 +197,6 @@ namespace geode { return defaultValue; } - template - HandleToSaved getSavedMutable(std::string const& key) { - return HandleToSaved(key, this, this->getSavedValue(key)); - } - - template - HandleToSaved getSavedMutable(std::string const& key, T const& defaultValue) { - return HandleToSaved(key, this, this->getSavedValue(key, defaultValue)); - } - /** * Set the value of an automatically saved variable. When the game is * closed, the value is automatically saved under the key @@ -389,11 +382,6 @@ namespace geode { ModJson getRuntimeInfo() const; }; - template - HandleToSaved::~HandleToSaved() { - m_mod->setSavedValue(m_key, static_cast(*this)); - } - /** * To bypass the need for cyclic dependencies, * this function does the exact same as Mod::get() diff --git a/loader/include/Geode/loader/ModInfo.hpp b/loader/include/Geode/loader/ModInfo.hpp index c834733f..3a7810fb 100644 --- a/loader/include/Geode/loader/ModInfo.hpp +++ b/loader/include/Geode/loader/ModInfo.hpp @@ -10,8 +10,6 @@ namespace geode { class Unzip; } - using ModJson = nlohmann::ordered_json; - struct GEODE_DLL Dependency { std::string id; ComparableVersionInfo version; @@ -111,8 +109,9 @@ namespace geode { std::vector spritesheets; /** * Mod settings + * @note Not a map because insertion order must be preserved */ - std::vector>> settings; + std::vector> settings; /** * Whether the mod can be disabled or not */ diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp index be641345..6b76968d 100644 --- a/loader/include/Geode/loader/Setting.hpp +++ b/loader/include/Geode/loader/Setting.hpp @@ -1,502 +1,244 @@ #pragma once +#include "Types.hpp" #include "../DefaultInclude.hpp" #include "../utils/JsonValidation.hpp" #include "../utils/Result.hpp" -#include "../external/json/json.hpp" #include "../utils/file.hpp" -#include "../utils/ranges.hpp" -#include "../utils/cocos.hpp" -#include "ModInfo.hpp" +#include "../external/json/json.hpp" #include -#include #include #pragma warning(push) #pragma warning(disable : 4275) namespace geode { - class Setting; class SettingNode; - class BoolSetting; - class IntSetting; - class FloatSetting; - class StringSetting; + class SettingValue; - struct ModInfo; + struct GEODE_DLL BoolSetting final { + using ValueType = bool; - enum class SettingType { - Bool, - Int, - Float, - String, - File, - Color, - ColorAlpha, - User, + std::optional name; + std::optional description; + bool defaultValue; + + static Result parse(JsonMaybeObject& obj); }; - /** - * Base class for all settings in Geode mods. Note that for most purposes - * you should use the built-in setting types. If you need a custom setting - * type however, inherit from this class. Do note that you are responsible - * for things like storing the default value, broadcasting value change - * events, making the setting node etc. - */ - class Setting : public std::enable_shared_from_this { + struct GEODE_DLL IntSetting final { + using ValueType = int64_t; + + std::optional name; + std::optional description; + ValueType defaultValue; + std::optional min; + std::optional max; + struct { + bool arrows = true; + bool bigArrows = false; + size_t arrowStep = 1; + size_t bigArrowStep = 5; + bool slider = true; + std::optional sliderStep = std::nullopt; + bool input = true; + } controls; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL FloatSetting final { + using ValueType = double; + + std::optional name; + std::optional description; + ValueType defaultValue; + std::optional min; + std::optional max; + struct { + bool arrows = true; + bool bigArrows = false; + size_t arrowStep = 1; + size_t bigArrowStep = 5; + bool slider = true; + std::optional sliderStep = std::nullopt; + bool input = true; + } controls; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL StringSetting final { + using ValueType = std::string; + + std::optional name; + std::optional description; + ValueType defaultValue; + std::optional match; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL FileSetting final { + using ValueType = ghc::filesystem::path; + using Filter = utils::file::FilePickOptions::Filter; + + std::optional name; + std::optional description; + ValueType defaultValue; + struct { + std::vector filters; + } controls; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL ColorSetting final { + using ValueType = cocos2d::ccColor3B; + + std::optional name; + std::optional description; + ValueType defaultValue; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL ColorAlphaSetting final { + using ValueType = cocos2d::ccColor4B; + + std::optional name; + std::optional description; + ValueType defaultValue; + + static Result parse(JsonMaybeObject& obj); + }; + + struct GEODE_DLL CustomSetting final { + ModJson json; + }; + + using SettingKind = std::variant< + BoolSetting, + IntSetting, + FloatSetting, + StringSetting, + FileSetting, + ColorSetting, + ColorAlphaSetting, + CustomSetting + >; + + struct GEODE_DLL Setting final { + private: + std::string m_key; + SettingKind m_kind; + + Setting() = default; + + public: + static Result parse( + std::string const& key, + JsonMaybeValue& obj + ); + Setting(std::string const& key, SettingKind const& kind); + + template + std::optional get() { + if (std::holds_alternative(m_kind)) { + return std::get(m_kind); + } + return std::nullopt; + } + + std::unique_ptr createDefaultValue() const; + bool isCustom() const; + std::string getDisplayName() const; + std::optional getDescription() const; + }; + + class GEODE_DLL SettingValue { protected: std::string m_key; - std::string m_modID; - - friend struct ModInfo; - - static Result> parse( - std::string const& type, std::string const& key, JsonMaybeObject& obj - ); + SettingValue(std::string const& key); + public: - GEODE_DLL virtual ~Setting() = default; - - // Load from mod.json - GEODE_DLL static Result> parse( - std::string const& key, ModJson const& json - ); - // Load value from saved settings + virtual ~SettingValue() = default; virtual bool load(nlohmann::json const& json) = 0; - // Save setting value virtual bool save(nlohmann::json& json) const = 0; - virtual SettingNode* createNode(float width) = 0; - GEODE_DLL void valueChanged(); - - GEODE_DLL std::string getKey() const; - virtual SettingType getType() const = 0; + std::string getKey() const; }; - // built-in settings' implementation details - namespace { -#define GEODE_INT_PARSE_SETTING_IMPL(obj, func, ...) \ - if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \ - auto r = std::static_pointer_cast(res)->func(obj); \ - if (!r) return Err(r.unwrapErr()); \ - } + template + class GeodeSettingValue : public SettingValue { + public: + using ValueType = typename T::ValueType; -#define GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(func, ...) \ - if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \ - auto res = static_cast(this)->func(value); \ - if (!res) { \ - return res; \ - } \ - } + protected: + ValueType m_value; + T m_definition; - template - class IMinMax; - template - class IOneOf; - template - class IMatch; + using Valid = std::pair>; - class ICArrows; - template - class ICSlider; - class ICInput; - class ICFileFilters; + GEODE_DLL Valid toValid(ValueType const& value) const; - template - class GeodeSetting : public Setting { - protected: - ValueType m_default; - ValueType m_value; - std::optional m_name; - std::optional m_description; - bool m_canResetToDefault = true; + public: + GeodeSettingValue(std::string const& key, T const& definition) + : SettingValue(key), + m_definition(definition), + m_value(definition.defaultValue) {} - friend class Setting; - - static Result> parse( - std::string const& key, JsonMaybeObject& obj - ) { - auto res = std::make_shared(); - - res->m_key = key; - obj.needs("default").into(res->m_default); - obj.has("name").intoAs(res->m_name); - obj.has("description").intoAs(res->m_description); - GEODE_INT_PARSE_SETTING_IMPL(obj, parseMinMax, IMinMax); - GEODE_INT_PARSE_SETTING_IMPL(obj, parseOneOf, IOneOf); - GEODE_INT_PARSE_SETTING_IMPL(obj, parseMatch, IMatch); - res->setValue(res->m_default); - - if (auto controls = obj.has("control").obj()) { - // every built-in setting type has a reset button - // by default - controls.has("can-reset").into(res->m_canResetToDefault); - GEODE_INT_PARSE_SETTING_IMPL(controls, parseArrows, ICArrows); - GEODE_INT_PARSE_SETTING_IMPL(controls, parseSlider, ICSlider); - GEODE_INT_PARSE_SETTING_IMPL(controls, parseInput, ICInput); - GEODE_INT_PARSE_SETTING_IMPL(controls, parseFileFilters, ICFileFilters); - } - - return Ok(res); - } - - public: - using value_t = ValueType; - - std::optional getName() const { - return m_name; - } - - std::string getDisplayName() const { - return m_name.value_or(m_key); - } - - std::optional getDescription() const { - return m_description; - } - - ValueType getDefault() const { - return m_default; - } - - ValueType getValue() const { - return m_value; - } - - void setValue(ValueType const& value) { - m_value = value; - if constexpr (std::is_base_of_v, Class>) { - (void)static_cast(this)->constrainMinMax(m_value); - } - if constexpr (std::is_base_of_v, Class>) { - (void)static_cast(this)->constrainOneOf(m_value); - } - if constexpr (std::is_base_of_v, Class>) { - (void)static_cast(this)->constrainMatch(m_value); - } - this->valueChanged(); - } - - Result<> isValidValue(ValueType value) { - GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMinMax, IMinMax); - GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainOneOf, IOneOf); - GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMatch, IMatch); - return Ok(); - } - - bool load(nlohmann::json const& json) override { - auto rawJson = json; - JsonChecker(rawJson).root("[setting value]").into(m_value); + bool load(nlohmann::json const& json) override { + try { + m_value = json.get(); return true; + } catch(...) { + return false; } - - bool save(nlohmann::json& json) const override { - json = m_value; - return true; - } - - bool canResetToDefault() const { - return m_canResetToDefault; - } - - SettingType getType() const override { - return Type; - } - }; - - template - class IMinMax { - protected: - std::optional m_min = std::nullopt; - std::optional m_max = std::nullopt; - - public: - Result<> constrainMinMax(ValueType& value) { - if (m_min && value < m_min.value()) { - value = m_min.value(); - return Err( - "Value must be between " + std::to_string(m_min.value()) + " and " + - std::to_string(m_max.value()) - ); - } - if (m_max && value > m_max.value()) { - value = m_max.value(); - return Err( - "Value must be between " + std::to_string(m_min.value()) + " and " + - std::to_string(m_max.value()) - ); - } - return Ok(); - } - - Result<> parseMinMax(JsonMaybeObject& obj) { - obj.has("min").intoAs(m_min); - obj.has("max").intoAs(m_max); - return Ok(); - } - - std::optional getMin() const { - return m_min; - } - - std::optional getMax() const { - return m_max; - } - }; - - template - class IOneOf { - protected: - std::optional> m_oneOf = std::nullopt; - - public: - Result<> constrainOneOf(ValueType& value) { - if (m_oneOf && !m_oneOf.value().count(value)) { - value = static_cast(this)->getDefault(); - return Err( - "Value must be one of " + utils::ranges::join(m_oneOf.value(), ", ") - ); - } - return Ok(); - } - - Result<> parseOneOf(JsonMaybeObject& obj) { - std::unordered_set oneOf {}; - for (auto& item : obj.has("one-of").iterate()) { - oneOf.insert(item.get()); - } - if (oneOf.size()) { - m_oneOf = oneOf; - } - return Ok(); - } - - auto getOneOf() const { - return m_oneOf; - } - }; - - template - class IMatch { - protected: - std::optional m_matchRegex = std::nullopt; - - public: - Result<> constrainMatch(ValueType& value) { - if (m_matchRegex) { - auto regex = std::regex(m_matchRegex.value()); - if (!std::regex_match(value, regex)) { - value = static_cast(this)->getDefault(); - return Err("Value must match regex " + m_matchRegex.value()); - } - } - return Ok(); - } - - Result<> parseMatch(JsonMaybeObject& obj) { - obj.has("match").intoAs(m_matchRegex); - return Ok(); - } - - std::optional getMatch() const { - return m_matchRegex; - } - }; - -#define GEODE_INT_DECL_SETTING_CONTROL(Name, name, default, json) \ - class IC##Name { \ - protected: \ - bool m_##name = default; \ - \ - public: \ - Result<> parse##Name(JsonMaybeObject& obj) { \ - obj.has(json).into(m_##name); \ - return Ok(); \ - } \ - bool has##Name() const { return m_##name; } \ - } - - class ICArrows { - protected: - bool m_hasArrows = true; - bool m_hasBigArrows = false; - size_t m_arrowStep = 1; - size_t m_bigArrowStep = 5; - - public: - Result<> parseArrows(JsonMaybeObject& obj) { - obj.has("arrows").into(m_hasArrows); - obj.has("arrow-step").into(m_arrowStep); - obj.has("big-arrows").into(m_hasBigArrows); - obj.has("big-arrow-step").into(m_bigArrowStep); - return Ok(); - } - - bool hasArrows() const { - return m_hasArrows; - } - - bool hasBigArrows() const { - return m_hasBigArrows; - } - - size_t getArrowStepSize() const { - return m_arrowStep; - } - - size_t getBigArrowStepSize() const { - return m_bigArrowStep; - } - }; - - template - class ICSlider { - protected: - bool m_hasSlider = true; - std::optional m_sliderStep = std::nullopt; - - public: - Result<> parseSlider(JsonMaybeObject& obj) { - obj.has("slider").into(m_hasSlider); - obj.has("slider-step").intoAs(m_sliderStep); - return Ok(); - } - - bool hasSlider() const { - return m_hasSlider; - } - - std::optional getSliderStepSize() const { - return m_sliderStep; - } - }; - - class ICFileFilters { - protected: - using Filter = utils::file::FilePickOptions::Filter; - - std::optional> m_filters = std::nullopt; - - public: - Result<> parseFileFilters(JsonMaybeObject& obj) { - std::vector filters {}; - for (auto& item : obj.has("filters").iterate()) { - if (auto iobj = item.obj()) { - Filter filter; - iobj.has("description").into(filter.description); - iobj.has("files").into(filter.files); - filters.push_back(filter); - } - } - if (filters.size()) { - m_filters = filters; - } - return Ok(); - } - - auto getFileFilters() const { - return m_filters; - } - }; - - GEODE_INT_DECL_SETTING_CONTROL(Input, hasInput, true, "input"); - } - - class BoolSetting : public GeodeSetting { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class IntSetting : - public GeodeSetting, - public IOneOf, - public IMinMax, - public ICArrows, - public ICSlider, - public ICInput { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class FloatSetting : - public GeodeSetting, - public IOneOf, - public IMinMax, - public ICArrows, - public ICSlider, - public ICInput { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class StringSetting : - public GeodeSetting, - public IOneOf, - public IMatch { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class FileSetting : - public GeodeSetting, - public ICFileFilters { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class ColorSetting : public GeodeSetting { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - class ColorAlphaSetting : - public GeodeSetting { - public: - GEODE_DLL SettingNode* createNode(float width) override; - }; - - // these can't be member functions because C++ is single-pass >:( - -#define GEODE_INT_BUILTIN_SETTING_IF(type, action, ...) \ - if constexpr (__VA_ARGS__) { \ - if (setting->getType() == SettingType::type) { \ - return std::static_pointer_cast(setting)->action; \ - } \ - } - - // clang-format off - - template - T getBuiltInSettingValue(const std::shared_ptr setting) { - GEODE_INT_BUILTIN_SETTING_IF(Bool, getValue(), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(Float, getValue(), std::is_floating_point_v) - else GEODE_INT_BUILTIN_SETTING_IF(Int, getValue(), std::is_integral_v) - else GEODE_INT_BUILTIN_SETTING_IF(String, getValue(), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(File, getValue(), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(Color, getValue(), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, getValue(), std::is_same_v) - else { - static_assert(!std::is_same_v, "Unsupported type for getting setting value!"); } - return T(); - } - - template - void setBuiltInSettingValue(const std::shared_ptr setting, T const& value) { - GEODE_INT_BUILTIN_SETTING_IF(Bool, setValue(value), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(Float, setValue(value), std::is_floating_point_v) - else GEODE_INT_BUILTIN_SETTING_IF(Int, setValue(value), std::is_integral_v) - else GEODE_INT_BUILTIN_SETTING_IF(String, setValue(value), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(File, setValue(value), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(Color, setValue(value), std::is_same_v) - else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, setValue(value), std::is_same_v) - else { - static_assert(!std::is_same_v, "Unsupported type for getting setting value!"); + bool save(nlohmann::json& json) const { + json = m_value; + return true; } - } - // clang-format on + GEODE_DLL SettingNode* createNode(float width) override; + T castDefinition() const { + return m_definition; + } + Setting getDefinition() const { + return Setting(m_key, m_definition); + } + + ValueType getValue() const { + return m_value; + } + void setValue(ValueType const& value) { + m_value = this->toValid(value).first; + } + Result<> validate(ValueType const& value) const { + auto reason = this->toValid(value).second; + if (reason.has_value()) { + return Err(static_cast(reason.value())); + } + return Ok(); + } + }; + + using BoolSettingValue = GeodeSettingValue; + using IntSettingValue = GeodeSettingValue; + using FloatSettingValue = GeodeSettingValue; + using StringSettingValue = GeodeSettingValue; + using FileSettingValue = GeodeSettingValue; + using ColorSettingValue = GeodeSettingValue; + using ColorAlphaSettingValue = GeodeSettingValue; + + template + struct SettingValueSetter { + static GEODE_DLL T get(SettingValue* setting); + static GEODE_DLL void set(SettingValue* setting, T const& value); + }; } #pragma warning(pop) diff --git a/loader/include/Geode/loader/SettingEvent.hpp b/loader/include/Geode/loader/SettingEvent.hpp index de30f4ed..78b1932d 100644 --- a/loader/include/Geode/loader/SettingEvent.hpp +++ b/loader/include/Geode/loader/SettingEvent.hpp @@ -7,67 +7,73 @@ #include namespace geode { - class GEODE_DLL SettingChangedEvent : public Event { - protected: - std::string m_modID; - Setting* m_setting; + struct GEODE_DLL SettingChangedEvent : public Event { + Mod* mod; + SettingValue* value; - public: - SettingChangedEvent(std::string const& modID, Setting* setting); - std::string getModID() const; - Setting* getSetting() const; + SettingChangedEvent(Mod* mod, SettingValue* value); }; - template >> - class SettingChangedFilter : public EventFilter { + class GEODE_DLL SettingChangedFilter : public EventFilter { + protected: + std::string m_modID; + std::optional m_targetKey; + public: - using Callback = void(T*); - using Event = SettingChangedEvent; + using Callback = void(SettingValue*); + + ListenerResult handle(std::function fn, SettingChangedEvent* event); + /** + * Listen to changes on a setting, or all settings + * @param modID Mod whose settings to listen to + * @param settingID Setting to listen to, or all settings if nullopt + */ + SettingChangedFilter( + std::string const& modID, + std::optional const& settingKey + ); + }; + + /** + * Listen for built-in setting changes + */ + template + class GeodeSettingChangedFilter : public SettingChangedFilter { + public: + using Callback = void(T); ListenerResult handle(std::function fn, SettingChangedEvent* event) { - if (m_modID == event->getModID() && - (!m_targetKey || m_targetKey.value() == event->getSetting()->getKey())) { - fn(static_cast(event->getSetting())); + if ( + m_modID == event->mod->getID() && + (!m_targetKey || m_targetKey.value() == event->value->getKey()) + ) { + fn(SettingValueSetter::get(event->value)); } return ListenerResult::Propagate; } - /** - * Listen to changes on a specific setting - */ - SettingChangedFilter( - std::string const& modID, std::string const& settingID - ) : - m_modID(modID), - m_targetKey(settingID) {} - - /** - * Listen to changes on all of a mods' settings - */ - SettingChangedFilter(std::string const& modID) : - m_modID(modID), m_targetKey(std::nullopt) {} - protected: - std::string m_modID; - std::optional m_targetKey; + GeodeSettingChangedFilter( + std::string const& modID, + std::string const& settingID + ) : SettingChangedFilter(modID, settingID) {} }; template - requires std::is_base_of_v std::monostate listenForSettingChanges( - std::string const& settingID, void (*callback)(T*) + std::string const& settingKey, void (*callback)(T) ) { Loader::get()->scheduleOnModLoad(getMod(), [=]() { new EventListener( - callback, SettingChangedFilter(getMod()->getID(), settingID) + callback, GeodeSettingChangedFilter(getMod()->getID(), settingKey) ); }); return std::monostate(); } - static std::monostate listenForAllSettingChanges(void (*callback)(Setting*)) { + static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) { Loader::get()->scheduleOnModLoad(getMod(), [=]() { new EventListener( - callback, SettingChangedFilter(getMod()->getID()) + callback, SettingChangedFilter(getMod()->getID(), std::nullopt) ); }); return std::monostate(); diff --git a/loader/include/Geode/loader/SettingNode.hpp b/loader/include/Geode/loader/SettingNode.hpp index 2ab1a890..5c56ce55 100644 --- a/loader/include/Geode/loader/SettingNode.hpp +++ b/loader/include/Geode/loader/SettingNode.hpp @@ -7,17 +7,19 @@ namespace geode { class SettingNode; - struct GEODE_DLL SettingNodeDelegate { - virtual void settingValueChanged(SettingNode* node); - virtual void settingValueCommitted(SettingNode* node); + struct SettingNodeDelegate { + virtual void settingValueChanged(SettingNode* node) {} + virtual void settingValueCommitted(SettingNode* node) {} }; class GEODE_DLL SettingNode : public cocos2d::CCNode { protected: - std::shared_ptr m_setting; + SettingValue* m_value; SettingNodeDelegate* m_delegate = nullptr; - bool init(std::shared_ptr setting); + bool init(SettingValue* value); + void dispatchChanged(); + void dispatchCommitted(); public: void setDelegate(SettingNodeDelegate* delegate); diff --git a/loader/include/Geode/loader/Types.hpp b/loader/include/Geode/loader/Types.hpp index d264d1e0..329146a6 100644 --- a/loader/include/Geode/loader/Types.hpp +++ b/loader/include/Geode/loader/Types.hpp @@ -2,6 +2,7 @@ #include "../DefaultInclude.hpp" #include "../platform/cplatform.h" +#include "../external/json/json.hpp" #include @@ -147,6 +148,7 @@ namespace geode { class FieldIntermediate; } + using ModJson = nlohmann::ordered_json; } /** diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp index 9de90ac1..96e79736 100644 --- a/loader/include/Geode/utils/JsonValidation.hpp +++ b/loader/include/Geode/utils/JsonValidation.hpp @@ -105,6 +105,7 @@ namespace geode { ); GEODE_DLL bool isError() const; + GEODE_DLL std::string getError() const; GEODE_DLL operator bool() const; }; diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp index 5d555863..5f737812 100644 --- a/loader/include/Geode/utils/cocos.hpp +++ b/loader/include/Geode/utils/cocos.hpp @@ -311,12 +311,14 @@ namespace geode { class EventListenerNode : public cocos2d::CCNode { protected: EventListener m_listener; + + EventListenerNode(EventListener&& listener) + : m_listener(std::move(listener)) {} public: static EventListenerNode* create(EventListener listener) { - auto ret = new EventListenerNode(); + auto ret = new EventListenerNode(std::move(listener)); if (ret && ret->init()) { - ret->m_listener = listener; ret->autorelease(); return ret; } @@ -325,9 +327,8 @@ namespace geode { } static EventListenerNode* create(typename Filter::Callback callback) { - auto ret = new EventListenerNode(); + auto ret = new EventListenerNode(EventListener(callback)); if (ret && ret->init()) { - ret->m_listener = EventListener(callback); ret->autorelease(); return ret; } @@ -343,9 +344,8 @@ namespace geode { // for some reason msvc won't let me just call EventListenerNode::create... // it claims no return value... // despite me writing return EventListenerNode::create()...... - auto ret = new EventListenerNode(); + auto ret = new EventListenerNode(EventListener(cls, callback)); if (ret && ret->init()) { - ret->m_listener.bind(cls, callback); ret->autorelease(); return ret; } diff --git a/loader/include/Geode/utils/file.hpp b/loader/include/Geode/utils/file.hpp index 749ba06b..acd167cd 100644 --- a/loader/include/Geode/utils/file.hpp +++ b/loader/include/Geode/utils/file.hpp @@ -9,6 +9,12 @@ #include #include +// allow converting ghc filesystem to json and back +namespace ghc::filesystem { + void GEODE_DLL to_json(nlohmann::json& json, path const& path); + void GEODE_DLL from_json(nlohmann::json const& json, path& path); +} + namespace geode::utils::file { GEODE_DLL Result readString(ghc::filesystem::path const& path); GEODE_DLL Result readJson(ghc::filesystem::path const& path); diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index e40d69b5..2578d5b4 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -1,6 +1,6 @@ #include "LoaderImpl.hpp" - +#include #include #include #include @@ -154,7 +154,8 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) { Result<> Loader::Impl::saveData() { // save mods' data - for (auto& [_, mod] : m_mods) { + for (auto& [id, mod] : m_mods) { + InternalMod::get()->setSavedValue("should-load-" + id, mod->isEnabled()); auto r = mod->saveData(); if (!r) { log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr()); @@ -254,10 +255,10 @@ void Loader::Impl::dispatchScheduledFunctions(Mod* mod) { } void Loader::Impl::scheduleOnModLoad(Mod* mod, ScheduledFunction func) { - std::lock_guard _(m_scheduledFunctionsMutex); if (mod) { return func(); } + std::lock_guard _(m_scheduledFunctionsMutex); m_scheduledFunctions.push_back(func); } @@ -403,9 +404,6 @@ bool Loader::Impl::didLastLaunchCrash() const { return crashlog::didLastLaunchCrash(); } - - - void Loader::Impl::reset() { this->closePlatformConsole(); @@ -417,6 +415,7 @@ void Loader::Impl::reset() { ghc::filesystem::remove_all(dirs::getModRuntimeDir()); ghc::filesystem::remove_all(dirs::getTempDir()); } + bool Loader::Impl::isReadyToHook() const { return m_readyToHook; } diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 56c129c9..cc30621f 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -17,6 +17,14 @@ Mod::Mod(ModInfo const& info) { m_info = info; m_saveDirPath = dirs::getModsSaveDir() / info.id; ghc::filesystem::create_directories(m_saveDirPath); + this->setupSettings(); + auto loadRes = this->loadData(); + if (!loadRes) { + log::warn( + "Unable to load data for \"{}\": {}", + info.id, loadRes.unwrapErr() + ); + } } Mod::~Mod() { @@ -93,14 +101,6 @@ std::vector Mod::getHooks() const { return m_hooks; } -bool Mod::hasSettings() const { - return m_info.settings.size(); -} - -decltype(ModInfo::settings) Mod::getSettings() const { - return m_info.settings; -} - // Settings and saved values Result<> Mod::loadData() { @@ -117,12 +117,20 @@ Result<> Mod::loadData() { JsonChecker checker(json); auto root = checker.root("[settings.json]"); + 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())) - return Err("Unable to load value for setting \"" + key + "\""); + if (!setting->load(value.json())) { + log::log( + Severity::Error, + this, + "{}: Unable to load value for setting \"{}\"", + m_info.id, key + ); + } } else { log::log( @@ -158,26 +166,91 @@ Result<> Mod::loadData() { Result<> Mod::saveData() { ModStateEvent(this, ModEventType::DataSaved).post(); + // Data saving should be fully fail-safe + + std::unordered_set coveredSettings; + // Settings auto json = nlohmann::json::object(); - for (auto& [key, value] : m_info.settings) { - if (!value->save(json[key])) return Err("Unable to save setting \"" + key + "\""); + for (auto& [key, value] : m_settings) { + coveredSettings.insert(key); + if (!value->save(json[key])) { + log::error("Unable to save setting \"" + key + "\""); + } } - GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4))); + // 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 + try { + log::debug("Check covered"); + for (auto& [key, value] : m_savedSettingsData.items()) { + log::debug("Check if {} is saved", key); + if (!coveredSettings.count(key)) { + json[key] = value; + } + } + } catch(...) {} - // Saved values - GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4))); + auto res = utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4)); + if (!res) { + log::error("Unable to save settings: {}", res.unwrapErr()); + } + + auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4)); + if (!res2) { + log::error("Unable to save values: {}", res2.unwrapErr()); + } return Ok(); } -std::shared_ptr Mod::getSetting(std::string const& key) const { +void Mod::setupSettings() { + for (auto& [key, sett] : m_info.settings) { + if (auto value = sett.createDefaultValue()) { + m_settings.emplace(key, std::move(value)); + } + } +} + +void Mod::registerCustomSetting( + std::string const& key, + std::unique_ptr value +) { + if (!m_settings.count(key)) { + // load data + if (m_savedSettingsData.count(key)) { + value->load(m_savedSettingsData.at(key)); + } + m_settings.emplace(key, std::move(value)); + } +} + +bool Mod::hasSettings() const { + return m_info.settings.size(); +} + +std::vector Mod::getSettingKeys() const { + std::vector keys; + for (auto& [key, _] : m_info.settings) { + keys.push_back(key); + } + return keys; +} + +std::optional Mod::getSettingDefinition(std::string const& key) const { for (auto& setting : m_info.settings) { if (setting.first == key) { return setting.second; } } + return std::nullopt; +} + +SettingValue* Mod::getSetting(std::string const& key) const { + if (m_settings.count(key)) { + return m_settings.at(key).get(); + } return nullptr; } @@ -211,11 +284,6 @@ Result<> Mod::loadBinary() { ModStateEvent(this, ModEventType::Loaded).post(); - auto loadRes = this->loadData(); - if (!loadRes) { - log::warn("Unable to load data for \"{}\": {}", m_info.id, loadRes.unwrapErr()); - } - Loader::get()->updateAllDependencies(); GEODE_UNWRAP(this->enable()); diff --git a/loader/src/loader/ModInfo.cpp b/loader/src/loader/ModInfo.cpp index ec4f51ae..644d1789 100644 --- a/loader/src/loader/ModInfo.cpp +++ b/loader/src/loader/ModInfo.cpp @@ -70,8 +70,7 @@ Result ModInfo::createFromSchemaV010(ModJson const& rawJson) { } for (auto& [key, value] : root.has("settings").items()) { - GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value.json())); - sett->m_modID = info.id; + GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value)); info.settings.push_back({ key, sett }); } diff --git a/loader/src/loader/Setting.cpp b/loader/src/loader/Setting.cpp index 20d037fc..d8e518c7 100644 --- a/loader/src/loader/Setting.cpp +++ b/loader/src/loader/Setting.cpp @@ -4,106 +4,361 @@ #include #include #include +#include USE_GEODE_NAMESPACE(); -#define PROPAGATE(err) \ - { \ - auto err__ = err; \ - if (!err__) return Err(err__.error()); \ - } +template +static void parseCommon(T& sett, JsonMaybeObject& obj) { + obj.has("name").into(sett.name); + obj.has("description").into(sett.description); + obj.has("default").into(sett.defaultValue); +} -std::string Setting::getKey() const { +Result BoolSetting::parse(JsonMaybeObject& obj) { + BoolSetting sett; + parseCommon(sett, obj); + return Ok(sett); +} + +Result IntSetting::parse(JsonMaybeObject& obj) { + IntSetting sett; + parseCommon(sett, obj); + obj.has("min").into(sett.min); + obj.has("max").into(sett.max); + if (auto controls = obj.has("control").obj()) { + controls.has("arrows").into(sett.controls.arrows); + controls.has("big-arrows").into(sett.controls.bigArrows); + controls.has("arrow-step").into(sett.controls.arrowStep); + controls.has("big-arrow-step").into(sett.controls.bigArrowStep); + controls.has("slider").into(sett.controls.slider); + controls.has("slider-step").into(sett.controls.sliderStep); + controls.has("input").into(sett.controls.input); + } + return Ok(sett); +} + +Result FloatSetting::parse(JsonMaybeObject& obj) { + FloatSetting sett; + parseCommon(sett, obj); + obj.has("min").into(sett.min); + obj.has("max").into(sett.max); + if (auto controls = obj.has("control").obj()) { + controls.has("arrows").into(sett.controls.arrows); + controls.has("big-arrows").into(sett.controls.bigArrows); + controls.has("arrow-step").into(sett.controls.arrowStep); + controls.has("big-arrow-step").into(sett.controls.bigArrowStep); + controls.has("slider").into(sett.controls.slider); + controls.has("slider-step").into(sett.controls.sliderStep); + controls.has("input").into(sett.controls.input); + } + return Ok(sett); +} + +Result StringSetting::parse(JsonMaybeObject& obj) { + StringSetting sett; + parseCommon(sett, obj); + obj.has("match").into(sett.match); + return Ok(sett); +} + +Result FileSetting::parse(JsonMaybeObject& obj) { + FileSetting sett; + parseCommon(sett, obj); + if (auto controls = obj.has("control").obj()) { + for (auto& item : controls.has("filters").iterate()) { + if (auto iobj = item.obj()) { + Filter filter; + iobj.has("description").into(filter.description); + iobj.has("files").into(filter.files); + sett.controls.filters.push_back(filter); + } + } + } + return Ok(sett); +} + +Result ColorSetting::parse(JsonMaybeObject& obj) { + ColorSetting sett; + parseCommon(sett, obj); + return Ok(sett); +} + +Result ColorAlphaSetting::parse(JsonMaybeObject& obj) { + ColorAlphaSetting sett; + parseCommon(sett, obj); + return Ok(sett); +} + +Result Setting::parse( + std::string const& key, + JsonMaybeValue& value +) { + auto sett = Setting(); + sett.m_key = key; + if (auto obj = value.obj()) { + std::string type; + obj.needs("type").into(type); + if (type.size()) { + switch (hash(type.c_str())) { + case hash("bool"): { + GEODE_UNWRAP_INTO(sett.m_kind, BoolSetting::parse(obj)); + } break; + + case hash("int"): { + GEODE_UNWRAP_INTO(sett.m_kind, IntSetting::parse(obj)); + } break; + + case hash("float"): { + GEODE_UNWRAP_INTO(sett.m_kind, FloatSetting::parse(obj)); + } break; + + case hash("string"): { + GEODE_UNWRAP_INTO(sett.m_kind, StringSetting::parse(obj)); + } break; + + case hash("rgb"): case hash("color"): { + GEODE_UNWRAP_INTO(sett.m_kind, ColorSetting::parse(obj)); + } break; + + case hash("rgba"): { + GEODE_UNWRAP_INTO(sett.m_kind, ColorAlphaSetting::parse(obj)); + } break; + + case hash("path"): case hash("file"): { + GEODE_UNWRAP_INTO(sett.m_kind, FileSetting::parse(obj)); + } break; + + case hash("custom"): { + sett.m_kind = CustomSetting { + .json = obj.json() + }; + // skip checking unknown keys + return Ok(sett); + } break; + + default: return Err("Unknown setting type \"" + type + "\""); + } + } + obj.checkUnknownKeys(); + } + // if the type wasn't an object or a string, the JsonChecker that gave the + // JsonMaybeValue will fail eventually so we can continue on + return Ok(sett); +} + +Setting::Setting(std::string const& key, SettingKind const& kind) + : m_key(key), m_kind(kind) {} + +std::unique_ptr Setting::createDefaultValue() const { + return std::visit(makeVisitor { + [&](BoolSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](IntSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](FloatSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](StringSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](FileSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](ColorSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](ColorAlphaSetting const& sett) -> std::unique_ptr { + return std::make_unique(m_key, sett); + }, + [&](auto const& sett) -> std::unique_ptr { + return nullptr; + }, + }, m_kind); +} + +bool Setting::isCustom() const { + return std::holds_alternative(m_kind); +} + +std::string Setting::getDisplayName() const { + return std::visit(makeVisitor { + [&](CustomSetting const& sett) { + return std::string(); + }, + [&](auto const& sett) { + return sett.name.value_or(m_key); + }, + }, m_kind); +} + +std::optional Setting::getDescription() const { + return std::visit(makeVisitor { + [&](CustomSetting const& sett) -> std::optional { + return std::nullopt; + }, + [&](auto const& sett) { + return sett.description; + }, + }, m_kind); +} + +// SettingValue + +SettingValue::SettingValue(std::string const& key) : m_key(key) {} + +std::string SettingValue::getKey() const { return m_key; } -Result> Setting::parse( - std::string const& type, std::string const& key, JsonMaybeObject& obj -) { - switch (hash(type.c_str())) { - case hash("bool"): return BoolSetting::parse(key, obj); - case hash("int"): return IntSetting::parse(key, obj); - case hash("float"): return FloatSetting::parse(key, obj); - case hash("string"): return StringSetting::parse(key, obj); - case hash("rgb"): - case hash("color"): return ColorSetting::parse(key, obj); - case hash("rgba"): return ColorAlphaSetting::parse(key, obj); - case hash("path"): - case hash("file"): return FileSetting::parse(key, obj); - default: return Err("Setting \"" + key + "\" has unknown type \"" + type + "\""); +// GeodeSettingValue & SettingValueSetter specializations + +#define IMPL_NODE_AND_SETTERS(type_) \ + template<> \ + SettingNode* GeodeSettingValue< \ + type_##Setting \ + >::createNode(float width) { \ + return type_##SettingNode::create(this, width); \ + } \ + typename type_##Setting::ValueType SettingValueSetter< \ + typename type_##Setting::ValueType \ + >::get(SettingValue* setting) { \ + if (auto b = typeinfo_cast(setting)) { \ + return b->getValue(); \ + } \ + return typename type_##Setting::ValueType(); \ + } \ + void SettingValueSetter< \ + typename type_##Setting::ValueType \ + >::set( \ + SettingValue* setting, \ + typename type_##Setting::ValueType const& value \ + ) { \ + if (auto b = typeinfo_cast(setting)) { \ + b->setValue(value); \ + } \ } + +#define IMPL_TO_VALID(type_) \ + template<> \ + typename GeodeSettingValue::Valid \ + GeodeSettingValue::toValid( \ + typename type_##Setting::ValueType const& value \ + ) const + +// instantiate value setters + +template struct SettingValueSetter; +template struct SettingValueSetter; +template struct SettingValueSetter; +template struct SettingValueSetter; +template struct SettingValueSetter; +template struct SettingValueSetter; +template struct SettingValueSetter; + +// instantiate values + +template class GeodeSettingValue; +template class GeodeSettingValue; +template class GeodeSettingValue; +template class GeodeSettingValue; +template class GeodeSettingValue; +template class GeodeSettingValue; +template class GeodeSettingValue; + +IMPL_NODE_AND_SETTERS(Bool); +IMPL_NODE_AND_SETTERS(Int); +IMPL_NODE_AND_SETTERS(Float); +IMPL_NODE_AND_SETTERS(String); +IMPL_NODE_AND_SETTERS(File); +IMPL_NODE_AND_SETTERS(Color); +IMPL_NODE_AND_SETTERS(ColorAlpha); + +IMPL_TO_VALID(Bool) { + return { value, std::nullopt }; } -Result> Setting::parse(std::string const& key, ModJson const& rawJson) { - if (rawJson.is_object()) { - auto json = rawJson; - JsonChecker checker(json); - auto root = checker.root("[setting \"" + key + "\"]").obj(); +IMPL_TO_VALID(Int) { + if (m_definition.min && value < m_definition.min) { + return { m_definition.min.value(), fmt::format( + "Value must be more than or equal to {}", + m_definition.min.value() + ) }; + } + if (m_definition.max && value > m_definition.max) { + return { m_definition.max.value(), fmt::format( + "Value must be less than or equal to {}", + m_definition.max.value() + ) }; + } + return { value, std::nullopt }; +} - auto res = Setting::parse(root.needs("type").get(), key, root); - root.checkUnknownKeys(); - if (checker.isError()) { - return Err(checker.getError()); +IMPL_TO_VALID(Float) { + if (m_definition.min && value < m_definition.min) { + return { m_definition.min.value(), fmt::format( + "Value must be more than or equal to {}", + m_definition.min.value() + ) }; + } + if (m_definition.max && value > m_definition.max) { + return { m_definition.max.value(), fmt::format( + "Value must be less than or equal to {}", + m_definition.max.value() + ) }; + } + return { value, std::nullopt }; +} + +IMPL_TO_VALID(String) { + if (m_definition.match) { + auto regex = std::regex(m_definition.match.value()); + if (!std::regex_match(value, regex)) { + return { + m_definition.defaultValue, + fmt::format( + "Value must match regex {}", + m_definition.match.value() + ) + }; } - return res; } - return Err("Setting value is not an object"); + return { value, std::nullopt }; } -void Setting::valueChanged() { - SettingChangedEvent(m_modID, this).post(); +IMPL_TO_VALID(File) { + return { value, std::nullopt }; } -SettingNode* BoolSetting::createNode(float width) { - return BoolSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); +IMPL_TO_VALID(Color) { + return { value, std::nullopt }; } -SettingNode* IntSetting::createNode(float width) { - return IntSettingNode::create(std::static_pointer_cast(shared_from_this()), width); +IMPL_TO_VALID(ColorAlpha) { + return { value, std::nullopt }; } -SettingNode* FloatSetting::createNode(float width) { - return FloatSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); +// SettingChangedEvent + +SettingChangedEvent::SettingChangedEvent(Mod* mod, SettingValue* value) + : mod(mod), value(value) {} + +// SettingChangedFilter + +ListenerResult SettingChangedFilter::handle( + std::function fn, SettingChangedEvent* event +) { + if (m_modID == event->mod->getID() && + (!m_targetKey || m_targetKey.value() == event->value->getKey()) + ) { + fn(event->value); + } + return ListenerResult::Propagate; } -SettingNode* StringSetting::createNode(float width) { - return StringSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); -} - -SettingNode* FileSetting::createNode(float width) { - return FileSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); -} - -SettingNode* ColorSetting::createNode(float width) { - return ColorSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); -} - -SettingNode* ColorAlphaSetting::createNode(float width) { - return ColorAlphaSettingNode::create( - std::static_pointer_cast(shared_from_this()), width - ); -} - -SettingChangedEvent::SettingChangedEvent( - std::string const& modID, Setting* setting -) : - m_modID(modID), - m_setting(setting) {} - -std::string SettingChangedEvent::getModID() const { - return m_modID; -} - -Setting* SettingChangedEvent::getSetting() const { - return m_setting; -} +SettingChangedFilter::SettingChangedFilter( + std::string const& modID, + std::optional const& settingKey +) : m_modID(modID), m_targetKey(settingKey) {} diff --git a/loader/src/loader/SettingNode.cpp b/loader/src/loader/SettingNode.cpp index e97f97ab..ca53c63b 100644 --- a/loader/src/loader/SettingNode.cpp +++ b/loader/src/loader/SettingNode.cpp @@ -3,12 +3,20 @@ USE_GEODE_NAMESPACE(); -void SettingNodeDelegate::settingValueChanged(SettingNode* node) {} +void SettingNode::dispatchChanged() { + if (m_delegate) { + m_delegate->settingValueChanged(this); + } +} -void SettingNodeDelegate::settingValueCommitted(SettingNode* node) {} +void SettingNode::dispatchCommitted() { + if (m_delegate) { + m_delegate->settingValueCommitted(this); + } +} -bool SettingNode::init(std::shared_ptr setting) { - m_setting = setting; +bool SettingNode::init(SettingValue* value) { + m_value = value; return true; } diff --git a/loader/src/main.cpp b/loader/src/main.cpp index 9694f989..55aa3144 100644 --- a/loader/src/main.cpp +++ b/loader/src/main.cpp @@ -99,50 +99,48 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) { return TRUE; } #endif - -#define $_ GEODE_CONCAT(unnamedVar_, __LINE__) - -static auto $_ = - listenForSettingChanges("show-platform-console", [](BoolSetting* setting) { - if (setting->getValue()) { +$execute { + listenForSettingChanges("show-platform-console", +[](bool value) { + if (value) { Loader::get()->openPlatformConsole(); } else { Loader::get()->closePlatformConsole(); } }); + + listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json { + return "Hello from Geode!"; + }); -static auto $_ = listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json { - return "Hello from Geode!"; -}); + listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json { + return Loader::get()->getInternalMod()->getModInfo(); + }); -static auto $_ = listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json { - return Loader::get()->getInternalMod()->getModInfo(); -}); + listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json { + std::vector res; -static auto $_ = listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json { - std::vector res; + auto args = event->messageData; + JsonChecker checker(args); + auto root = checker.root("").obj(); - auto args = event->messageData; - JsonChecker checker(args); - auto root = checker.root("").obj(); + auto includeRunTimeInfo = root.has("include-runtime-info").template get(); + auto dontIncludeLoader = root.has("dont-include-loader").template get(); - auto includeRunTimeInfo = root.has("include-runtime-info").template get(); - auto dontIncludeLoader = root.has("dont-include-loader").template get(); + if (!dontIncludeLoader) { + res.push_back( + includeRunTimeInfo ? Loader::get()->getInternalMod()->getRuntimeInfo() : + Loader::get()->getInternalMod()->getModInfo().toJSON() + ); + } - if (!dontIncludeLoader) { - res.push_back( - includeRunTimeInfo ? Loader::get()->getInternalMod()->getRuntimeInfo() : - Loader::get()->getInternalMod()->getModInfo().toJSON() - ); - } + for (auto& mod : Loader::get()->getAllMods()) { + res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON()); + } - for (auto& mod : Loader::get()->getAllMods()) { - res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON()); - } - - return res; -}); + return res; + }); +} int geodeEntry(void* platformData) { // setup internals diff --git a/loader/src/ui/internal/credits/link.cpp b/loader/src/ui/internal/credits/link.cpp deleted file mode 100644 index a0080636..00000000 --- a/loader/src/ui/internal/credits/link.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include - -USE_GEODE_NAMESPACE(); - -std::string expandKnownLink(std::string const& link) { - switch (hash(utils::string::toLower(link).c_str())) { - case hash("github"): - if (!utils::string::contains(link, "/")) { - return "https://github.com/" + link; - } - break; - - case hash("youtube"): - if (!utils::string::contains(link, "/")) { - return "https://youtube.com/channel/" + link; - } - break; - - case hash("twitter"): - if (!utils::string::contains(link, "/")) { - return "https://twitter.com/" + link; - } - break; - - case hash("newgrounds"): - if (!utils::string::contains(link, "/")) { - return "https://" + link + ".newgrounds.com"; - } - break; - } - return link; -} diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 5ba9386e..e9e15aec 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #define FTS_FUZZY_MATCH_IMPLEMENTATION #include diff --git a/loader/src/ui/internal/settings/GeodeSettingNode.cpp b/loader/src/ui/internal/settings/GeodeSettingNode.cpp index 491545a1..70b14542 100644 --- a/loader/src/ui/internal/settings/GeodeSettingNode.cpp +++ b/loader/src/ui/internal/settings/GeodeSettingNode.cpp @@ -8,6 +8,107 @@ #include #include +// Helpers + +template +Num parseNumForInput(std::string const& str) { + try { + if constexpr (std::is_same_v) { + return std::stoll(str); + } + else if constexpr (std::is_same_v) { + return std::stod(str); + } + else { + static_assert(!std::is_same_v, "Impl Num for parseNumForInput"); + } + } + catch (...) { + return 0; + } +} + +template +static float valueToSlider(T const& setting, typename T::ValueType value) { + auto min = setting.min ? setting.min.value() : -100; + auto max = setting.max ? setting.max.value() : +100; + auto range = max - min; + return static_cast(clamp(static_cast(value - min) / range, 0.0, 1.0)); +} + +template +static typename T::ValueType valueFromSlider(T const& setting, float num) { + auto min = setting.min ? setting.min.value() : -100; + auto max = setting.max ? setting.max.value() : +100; + auto range = max - min; + auto value = static_cast(num * range + min); + if (auto step = setting.controls.sliderStep) { + value = static_cast( + round(value / step.value()) * step.value() + ); + } + return value; +} + +template +InputNode* createInput(C* node, GeodeSettingValue* setting, float width) { + auto input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt"); + input->setPosition({ + -(width / 2 - 70.f) / 2, + setting->castDefinition().controls.slider ? 5.f : 0.f + }); + input->setScale(.65f); + input->getInput()->setDelegate(node); + return input; +} + +template +Slider* createSlider(C* node, GeodeSettingValue* setting, float width) { + auto slider = Slider::create( + node, menu_selector(C::onSlider), .5f + ); + slider->setPosition(-50.f, -15.f); + node->updateSlider(); + return slider; +} + +template +std::pair< + CCMenuItemSpriteExtra*, CCMenuItemSpriteExtra* +> createArrows(C* node, GeodeSettingValue* setting, float width, bool big) { + auto yPos = setting->castDefinition().controls.slider ? 5.f : 0.f; + auto decArrowSpr = CCSprite::createWithSpriteFrameName( + big ? "double-nav.png"_spr : "navArrowBtn_001.png" + ); + decArrowSpr->setFlipX(true); + decArrowSpr->setScale(.3f); + + auto decArrow = CCMenuItemSpriteExtra::create( + decArrowSpr, node, menu_selector(C::onArrow) + ); + decArrow->setPosition(-width / 2 + (big ? 65.f : 80.f), yPos); + decArrow->setTag(big ? + -setting->castDefinition().controls.bigArrowStep : + -setting->castDefinition().controls.arrowStep + ); + + auto incArrowSpr = CCSprite::createWithSpriteFrameName( + big ? "double-nav.png"_spr : "navArrowBtn_001.png" + ); + incArrowSpr->setScale(.3f); + + auto incArrow = CCMenuItemSpriteExtra::create( + incArrowSpr, node, menu_selector(C::onArrow) + ); + incArrow->setTag(big ? + setting->castDefinition().controls.bigArrowStep : + setting->castDefinition().controls.arrowStep + ); + incArrow->setPosition(big ? 5.f : -10.f, yPos); + + return { decArrow, incArrow }; +} + // BoolSettingNode void BoolSettingNode::valueChanged(bool updateText) { @@ -21,7 +122,7 @@ void BoolSettingNode::onToggle(CCObject*) { m_toggle->toggle(!m_uncommittedValue); } -bool BoolSettingNode::setup(std::shared_ptr setting, float width) { +bool BoolSettingNode::setup(BoolSettingValue* setting, float width) { m_toggle = CCMenuItemToggler::createWithStandardSprites( this, menu_selector(BoolSettingNode::onToggle), .6f ); @@ -34,42 +135,172 @@ bool BoolSettingNode::setup(std::shared_ptr setting, float width) { // IntSettingNode -float IntSettingNode::setupHeight(std::shared_ptr setting) const { - return setting->hasSlider() ? 55.f : 40.f; +float IntSettingNode::setupHeight(IntSettingValue* setting) const { + return setting->castDefinition().controls.slider ? 55.f : 40.f; } -bool IntSettingNode::setup(std::shared_ptr setting, float width) { - this->setupArrows(setting, width); - this->setupInput(setting, width); - this->setupSlider(setting, width); - return true; +void IntSettingNode::onSlider(CCObject* slider) { + m_uncommittedValue = valueFromSlider( + setting()->castDefinition(), + static_cast(slider)->getValue() + ); + this->valueChanged(true); } void IntSettingNode::valueChanged(bool updateText) { GeodeSettingNode::valueChanged(updateText); - this->updateLabel(updateText); + if (updateText) { + this->updateLabel(); + } this->updateSlider(); } +void IntSettingNode::updateSlider() { + if (m_slider) { + m_slider->setValue(valueToSlider( + setting()->castDefinition(), + m_uncommittedValue + )); + m_slider->updateBar(); + } +} + +void IntSettingNode::updateLabel() { + if (m_input) { + // hacky way to make setString not called textChanged + m_input->getInput()->setDelegate(nullptr); + m_input->setString(numToString(m_uncommittedValue)); + m_input->getInput()->setDelegate(this); + } + else { + m_label->setString(numToString(m_uncommittedValue).c_str()); + m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f); + } +} + +void IntSettingNode::onArrow(CCObject* sender) { + m_uncommittedValue += sender->getTag(); + this->valueChanged(true); +} + +void IntSettingNode::textChanged(CCTextInputNode* input) { + m_uncommittedValue = parseNumForInput(input->getString()); + this->valueChanged(false); +} + +bool IntSettingNode::setup(IntSettingValue* setting, float width) { + if (setting->castDefinition().controls.input) { + m_menu->addChild(m_input = createInput(this, setting, width)); + } + else { + m_label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label->setPosition({ + -(width / 2 - 70.f) / 2, + setting->castDefinition().controls.slider ? 5.f : 0.f + }); + m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f); + m_menu->addChild(m_label); + } + if (setting->castDefinition().controls.slider) { + m_menu->addChild(m_slider = createSlider(this, setting, width)); + } + if (setting->castDefinition().controls.arrows) { + auto [dec, inc] = createArrows(this, setting, width, false); + m_menu->addChild(m_decArrow = dec); + m_menu->addChild(m_incArrow = inc); + } + if (setting->castDefinition().controls.bigArrows) { + auto [dec, inc] = createArrows(this, setting, width, true); + m_menu->addChild(m_bigDecArrow = dec); + m_menu->addChild(m_bigIncArrow = inc); + } + return true; +} + // FloatSettingNode -float FloatSettingNode::setupHeight(std::shared_ptr setting) const { - return setting->hasSlider() ? 55.f : 40.f; +float FloatSettingNode::setupHeight(FloatSettingValue* setting) const { + return setting->castDefinition().controls.slider ? 55.f : 40.f; } -bool FloatSettingNode::setup(std::shared_ptr setting, float width) { - this->setupArrows(setting, width); - this->setupInput(setting, width); - this->setupSlider(setting, width); - return true; +void FloatSettingNode::onSlider(CCObject* slider) { + m_uncommittedValue = valueFromSlider( + setting()->castDefinition(), + static_cast(slider)->getValue() + ); + this->valueChanged(true); } void FloatSettingNode::valueChanged(bool updateText) { GeodeSettingNode::valueChanged(updateText); - this->updateLabel(updateText); + if (updateText) { + this->updateLabel(); + } this->updateSlider(); } +void FloatSettingNode::updateSlider() { + if (m_slider) { + m_slider->setValue(valueToSlider( + setting()->castDefinition(), + m_uncommittedValue + )); + m_slider->updateBar(); + } +} + +void FloatSettingNode::updateLabel() { + if (m_input) { + // hacky way to make setString not called textChanged + m_input->getInput()->setDelegate(nullptr); + m_input->setString(numToString(m_uncommittedValue)); + m_input->getInput()->setDelegate(this); + } + else { + m_label->setString(numToString(m_uncommittedValue).c_str()); + m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f); + } +} + +void FloatSettingNode::onArrow(CCObject* sender) { + m_uncommittedValue += sender->getTag(); + this->valueChanged(true); +} + +void FloatSettingNode::textChanged(CCTextInputNode* input) { + m_uncommittedValue = parseNumForInput(input->getString()); + this->valueChanged(false); +} + +bool FloatSettingNode::setup(FloatSettingValue* setting, float width) { + if (setting->castDefinition().controls.input) { + m_menu->addChild(m_input = createInput(this, setting, width)); + } + else { + m_label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label->setPosition({ + -(width / 2 - 70.f) / 2, + setting->castDefinition().controls.slider ? 5.f : 0.f + }); + m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f); + m_menu->addChild(m_label); + } + if (setting->castDefinition().controls.slider) { + m_menu->addChild(m_slider = createSlider(this, setting, width)); + } + if (setting->castDefinition().controls.arrows) { + auto [dec, inc] = createArrows(this, setting, width, false); + m_menu->addChild(m_decArrow = dec); + m_menu->addChild(m_incArrow = inc); + } + if (setting->castDefinition().controls.bigArrows) { + auto [dec, inc] = createArrows(this, setting, width, true); + m_menu->addChild(m_bigDecArrow = dec); + m_menu->addChild(m_bigIncArrow = inc); + } + return true; +} + // StringSettingNode void StringSettingNode::updateLabel() { @@ -89,7 +320,7 @@ void StringSettingNode::valueChanged(bool updateText) { this->updateLabel(); } -bool StringSettingNode::setup(std::shared_ptr setting, float width) { +bool StringSettingNode::setup(StringSettingValue* setting, float width) { m_input = InputNode::create(width / 2 - 10.f, "Text", "chatFont.fnt"); m_input->setPosition({ -(width / 2 - 70.f) / 2, .0f }); m_input->setScale(.65f); @@ -119,20 +350,19 @@ void FileSettingNode::valueChanged(bool updateText) { } void FileSettingNode::onPickFile(CCObject*) { - auto setting = std::static_pointer_cast(m_setting); if (auto path = file::pickFile( - file::PickMode::OpenFile, - { - dirs::getGameDir(), - setting->getFileFilters().value_or(std::vector()) - } - )) { + file::PickMode::OpenFile, + { + dirs::getGameDir(), + setting()->castDefinition().controls.filters + } + )) { m_uncommittedValue = path.unwrap(); this->valueChanged(true); } } -bool FileSettingNode::setup(std::shared_ptr setting, float width) { +bool FileSettingNode::setup(FileSettingValue* setting, float width) { m_input = InputNode::create(width / 2 - 30.f, "Path to File", "chatFont.fnt"); m_input->setPosition({ -(width / 2 - 80.f) / 2 - 15.f, .0f }); m_input->setScale(.65f); @@ -142,8 +372,9 @@ bool FileSettingNode::setup(std::shared_ptr setting, float width) { auto fileBtnSpr = CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png"); fileBtnSpr->setScale(.5f); - auto fileBtn = - CCMenuItemSpriteExtra::create(fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile)); + auto fileBtn = CCMenuItemSpriteExtra::create( + fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile) + ); fileBtn->setPosition(.0f, .0f); m_menu->addChild(fileBtn); @@ -169,7 +400,7 @@ void ColorSettingNode::onSelectColor(CCObject*) { popup->show(); } -bool ColorSettingNode::setup(std::shared_ptr setting, float width) { +bool ColorSettingNode::setup(ColorSettingValue* setting, float width) { m_colorSpr = ColorChannelSprite::create(); m_colorSpr->setColor(m_uncommittedValue); m_colorSpr->setScale(.65f); @@ -203,7 +434,7 @@ void ColorAlphaSettingNode::onSelectColor(CCObject*) { popup->show(); } -bool ColorAlphaSettingNode::setup(std::shared_ptr setting, float width) { +bool ColorAlphaSettingNode::setup(ColorAlphaSettingValue* setting, float width) { m_colorSpr = ColorChannelSprite::create(); m_colorSpr->setColor(to3B(m_uncommittedValue)); m_colorSpr->updateOpacity(m_uncommittedValue.a / 255.f); @@ -217,3 +448,53 @@ bool ColorAlphaSettingNode::setup(std::shared_ptr setting, fl return true; } + +// CustomSettingPlaceholderNode + +void CustomSettingPlaceholderNode::commit() { + +} + +bool CustomSettingPlaceholderNode::hasUncommittedChanges() { + return false; +} + +bool CustomSettingPlaceholderNode::hasNonDefaultValue() { + return false; +} + +void CustomSettingPlaceholderNode::resetToDefault() { + +} + +bool CustomSettingPlaceholderNode::init(std::string const& key, float width) { + if (!SettingNode::init(nullptr)) + return false; + + constexpr auto sidePad = 40.f; + + this->setContentSize({ width, 40.f }); + + auto info = CCLabelBMFont::create( + "You need to enable the mod to modify this setting.", + "bigFont.fnt" + ); + info->setAnchorPoint({ .0f, .5f }); + info->limitLabelWidth(width - sidePad, .5f, .1f); + info->setPosition({ sidePad / 2, m_obContentSize.height / 2 }); + this->addChild(info); + + return true; +} + +CustomSettingPlaceholderNode* CustomSettingPlaceholderNode::create( + std::string const& key, float width +) { + auto ret = new CustomSettingPlaceholderNode; + if (ret && ret->init(key, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/internal/settings/GeodeSettingNode.hpp b/loader/src/ui/internal/settings/GeodeSettingNode.hpp index 0b5bc499..604406d9 100644 --- a/loader/src/ui/internal/settings/GeodeSettingNode.hpp +++ b/loader/src/ui/internal/settings/GeodeSettingNode.hpp @@ -15,435 +15,256 @@ USE_GEODE_NAMESPACE(); -namespace { - template - Num parseNumForInput(std::string const& str) { - try { - if constexpr (std::is_same_v) { - return std::stoll(str); - } - else if constexpr (std::is_same_v) { - return std::stod(str); - } - } - catch (...) { - return 0; - } +#define IMPL_SETT_CREATE(type_) \ + static type_##SettingNode* create( \ + type_##SettingValue* value, float width \ + ) { \ + auto ret = new type_##SettingNode; \ + if (ret && ret->init(value, width)) { \ + ret->autorelease(); \ + return ret; \ + } \ + CC_SAFE_DELETE(ret); \ + return nullptr; \ } - template - class GeodeSettingNode : public SettingNode { - public: - using value_t = typename T::value_t; +template +class GeodeSettingNode : public SettingNode { +public: + using ValueType = typename T::ValueType; - protected: - float m_width; - float m_height; - value_t m_uncommittedValue; - CCMenu* m_menu; - CCLabelBMFont* m_nameLabel; - CCLabelBMFont* m_errorLabel; - CCMenuItemSpriteExtra* m_resetBtn; +protected: + float m_width; + float m_height; + ValueType m_uncommittedValue; + CCMenu* m_menu; + CCLabelBMFont* m_nameLabel; + CCLabelBMFont* m_errorLabel; + CCMenuItemSpriteExtra* m_resetBtn; - bool init(std::shared_ptr setting, float width) { - if (!SettingNode::init(std::static_pointer_cast(setting))) return false; + bool init(GeodeSettingValue* setting, float width) { + if (!SettingNode::init(setting)) + return false; - m_width = width; - m_height = this->setupHeight(setting); - this->setContentSize({ width, m_height }); + m_width = width; + m_height = this->setupHeight(setting); + this->setContentSize({ width, m_height }); - constexpr auto sidePad = 40.f; + constexpr auto sidePad = 40.f; - m_uncommittedValue = setting->getValue(); + m_uncommittedValue = setting->getValue(); - auto name = setting->getDisplayName(); + auto name = setting->getDefinition().getDisplayName(); - m_nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); - m_nameLabel->setAnchorPoint({ .0f, .5f }); - m_nameLabel->limitLabelWidth(width / 2 - 50.f, .5f, .1f); - m_nameLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 }); - this->addChild(m_nameLabel); + m_nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); + m_nameLabel->setAnchorPoint({ .0f, .5f }); + m_nameLabel->limitLabelWidth(width / 2 - 50.f, .5f, .1f); + m_nameLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 }); + this->addChild(m_nameLabel); - m_errorLabel = CCLabelBMFont::create("", "bigFont.fnt"); - m_errorLabel->setAnchorPoint({ .0f, .5f }); - m_errorLabel->limitLabelWidth(width / 2 - 50.f, .25f, .1f); - m_errorLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 - 14.f }); - m_errorLabel->setColor({ 255, 100, 100 }); - m_errorLabel->setZOrder(5); - this->addChild(m_errorLabel); + m_errorLabel = CCLabelBMFont::create("", "bigFont.fnt"); + m_errorLabel->setAnchorPoint({ .0f, .5f }); + m_errorLabel->limitLabelWidth(width / 2 - 50.f, .25f, .1f); + m_errorLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 - 14.f }); + m_errorLabel->setColor({ 255, 100, 100 }); + m_errorLabel->setZOrder(5); + this->addChild(m_errorLabel); - m_menu = CCMenu::create(); - m_menu->setPosition({ m_obContentSize.width - sidePad / 2, m_obContentSize.height / 2 } + m_menu = CCMenu::create(); + m_menu->setPosition({ m_obContentSize.width - sidePad / 2, m_obContentSize.height / 2 } + ); + this->addChild(m_menu); + + float btnPos = 15.f; + + if (setting->castDefinition().description) { + auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + infoSpr->setScale(.6f); + + auto infoBtn = CCMenuItemSpriteExtra::create( + infoSpr, this, menu_selector(GeodeSettingNode::onDescription) ); - this->addChild(m_menu); + infoBtn->setPosition( + -m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width + + btnPos, + 0.f + ); + m_menu->addChild(infoBtn); - float btnPos = 15.f; - - if (setting->getDescription()) { - auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); - infoSpr->setScale(.6f); - - auto infoBtn = CCMenuItemSpriteExtra::create( - infoSpr, this, menu_selector(GeodeSettingNode::onDescription) - ); - infoBtn->setPosition( - -m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width + - btnPos, - 0.f - ); - m_menu->addChild(infoBtn); - - btnPos += 20.f; - } - - if (setting->canResetToDefault()) { - auto resetBtnSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr); - resetBtnSpr->setScale(.5f); - - m_resetBtn = CCMenuItemSpriteExtra::create( - resetBtnSpr, this, menu_selector(GeodeSettingNode::onReset) - ); - m_resetBtn->setPosition( - -m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width + - btnPos, - .0f - ); - m_menu->addChild(m_resetBtn); - } - - CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); - m_menu->registerWithTouchDispatcher(); - - if (!this->setup(setting, width)) return false; - - this->valueChanged(); - - return true; + btnPos += 20.f; } - void onDescription(CCObject*) { - auto setting = std::static_pointer_cast(m_setting); - FLAlertLayer::create( - setting->getDisplayName().c_str(), setting->getDescription().value(), "OK" - ) - ->show(); - } + auto resetBtnSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr); + resetBtnSpr->setScale(.5f); - void onReset(CCObject*) { - auto setting = std::static_pointer_cast(m_setting); - createQuickPopup( - "Reset", - "Are you sure you want to reset " + setting->getDisplayName() + - " to default?", - "Cancel", "Reset", - [this](auto, bool btn2) { - if (btn2) this->resetToDefault(); + m_resetBtn = CCMenuItemSpriteExtra::create( + resetBtnSpr, this, menu_selector(GeodeSettingNode::onReset) + ); + m_resetBtn->setPosition( + -m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width + + btnPos, + .0f + ); + m_menu->addChild(m_resetBtn); + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + m_menu->registerWithTouchDispatcher(); + + if (!this->setup(setting, width)) return false; + + this->valueChanged(); + + return true; + } + + void onDescription(CCObject*) { + FLAlertLayer::create( + setting()->getDefinition().getDisplayName().c_str(), + setting()->castDefinition().description.value(), + "OK" + )->show(); + } + + void onReset(CCObject*) { + createQuickPopup( + "Reset", + "Are you sure you want to reset " + + setting()->getDefinition().getDisplayName() + + " to default?", + "Cancel", "Reset", + [this](auto, bool btn2) { + if (btn2) { + this->resetToDefault(); } - ); + } + ); + } + + virtual float setupHeight(GeodeSettingValue* setting) const { + return 40.f; + } + + virtual bool setup(GeodeSettingValue* setting, float width) = 0; + + virtual void valueChanged(bool updateText = true) { + if (this->hasUncommittedChanges()) { + m_nameLabel->setColor(cc3x(0x1d0)); } - - virtual float setupHeight(std::shared_ptr setting) const { - return 40.f; + else { + m_nameLabel->setColor(cc3x(0xfff)); } - - virtual bool setup(std::shared_ptr setting, float width) = 0; - - virtual void valueChanged(bool updateText = true) { - if (m_delegate) m_delegate->settingValueChanged(this); - if (this->hasUncommittedChanges()) { - m_nameLabel->setColor(cc3x(0x1d0)); - } - else { - m_nameLabel->setColor(cc3x(0xfff)); - } - if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue()); - auto isValid = std::static_pointer_cast(m_setting)->isValidValue(m_uncommittedValue); - if (!isValid) { - m_errorLabel->setVisible(true); - m_errorLabel->setString(isValid.unwrapErr().c_str()); - } - else { - m_errorLabel->setVisible(false); - } + if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue()); + auto isValid = setting()->validate(m_uncommittedValue); + if (!isValid) { + m_errorLabel->setVisible(true); + m_errorLabel->setString(isValid.unwrapErr().c_str()); } - - public: - static N* create(std::shared_ptr setting, float width) { - auto ret = new N(); - if (ret && ret->init(setting, width)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; + else { + m_errorLabel->setVisible(false); } + this->dispatchChanged(); + } - void commit() override { - std::static_pointer_cast(m_setting)->setValue(m_uncommittedValue); - m_uncommittedValue = std::static_pointer_cast(m_setting)->getValue(); - this->valueChanged(); - if (m_delegate) m_delegate->settingValueCommitted(this); - } + GeodeSettingValue* setting() { + return static_cast*>(m_value); + } - bool hasUncommittedChanges() override { - return m_uncommittedValue != std::static_pointer_cast(m_setting)->getValue(); - } +public: + void commit() override { + setting()->setValue(m_uncommittedValue); + m_uncommittedValue = setting()->getValue(); + this->valueChanged(); + this->dispatchCommitted(); + } - bool hasNonDefaultValue() override { - return m_uncommittedValue != std::static_pointer_cast(m_setting)->getDefault(); - } + bool hasUncommittedChanges() override { + return m_uncommittedValue != setting()->getValue(); + } - void resetToDefault() override { - m_uncommittedValue = std::static_pointer_cast(m_setting)->getDefault(); - this->valueChanged(); - } - }; + bool hasNonDefaultValue() override { + return m_uncommittedValue != setting()->castDefinition().defaultValue; + } - template - class ImplInput : public TextInputDelegate { - protected: - InputNode* m_input = nullptr; - CCLabelBMFont* m_label = nullptr; + void resetToDefault() override { + m_uncommittedValue = setting()->castDefinition().defaultValue; + this->valueChanged(); + } +}; - C* self() { - return static_cast(this); - } - - void setupInput(std::shared_ptr setting, float width) { - if (setting->hasInput()) { - m_input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt"); - m_input->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f }); - m_input->setScale(.65f); - m_input->getInput()->setDelegate(this); - self()->m_menu->addChild(m_input); - } - else { - m_label = CCLabelBMFont::create("", "bigFont.fnt"); - m_label->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f }); - m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f); - self()->m_menu->addChild(m_label); - } - } - - void updateLabel(bool updateText) { - if (!updateText) return; - if (m_input) { - // hacky way to make setString not called textChanged - m_input->getInput()->setDelegate(nullptr); - m_input->setString(numToString(self()->m_uncommittedValue)); - m_input->getInput()->setDelegate(this); - } - else { - m_label->setString(numToString(self()->m_uncommittedValue).c_str()); - m_label->limitLabelWidth(self()->m_width / 2 - 70.f, .5f, .1f); - } - } - - void textChanged(CCTextInputNode* input) override { - try { - self()->m_uncommittedValue = - parseNumForInput(input->getString()); - self()->valueChanged(false); - } - catch (...) { - } - } - }; - - template - class ImplArrows { - protected: - CCMenuItemSpriteExtra* m_decArrow = nullptr; - CCMenuItemSpriteExtra* m_incArrow = nullptr; - CCMenuItemSpriteExtra* m_bigDecArrow = nullptr; - CCMenuItemSpriteExtra* m_bigIncArrow = nullptr; - - C* self() { - return static_cast(this); - } - - void setupArrows(std::shared_ptr setting, float width) { - auto yPos = setting->hasSlider() ? 5.f : 0.f; - - if (setting->hasArrows()) { - auto decArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - decArrowSpr->setFlipX(true); - decArrowSpr->setScale(.3f); - - m_decArrow = CCMenuItemSpriteExtra::create( - decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onDecrement) - ); - m_decArrow->setPosition(-width / 2 + 80.f, yPos); - self()->m_menu->addChild(m_decArrow); - - auto incArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - incArrowSpr->setScale(.3f); - - m_incArrow = CCMenuItemSpriteExtra::create( - incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onIncrement) - ); - m_incArrow->setPosition(-10.f, yPos); - self()->m_menu->addChild(m_incArrow); - } - - if (setting->hasBigArrows()) { - auto decArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr); - decArrowSpr->setFlipX(true); - decArrowSpr->setScale(.3f); - - m_bigDecArrow = CCMenuItemSpriteExtra::create( - decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigDecrement) - ); - m_bigDecArrow->setPosition(-width / 2 + 65.f, yPos); - self()->m_menu->addChild(m_bigDecArrow); - - auto incArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr); - incArrowSpr->setScale(.3f); - - m_bigIncArrow = CCMenuItemSpriteExtra::create( - incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigIncrement) - ); - m_bigIncArrow->setPosition(5.f, yPos); - self()->m_menu->addChild(m_bigIncArrow); - } - } - - // intentionally a subclass to make it relatively safe to - // use as callbacks for the child class, since we give the - // child class as the target to CCMenuItemSpriteExtra - struct Callbacks : public C { - void onIncrement(CCObject*) { - this->m_uncommittedValue += - std::static_pointer_cast(this->m_setting)->getArrowStepSize(); - this->valueChanged(true); - } - - void onDecrement(CCObject*) { - this->m_uncommittedValue -= - std::static_pointer_cast(this->m_setting)->getArrowStepSize(); - this->valueChanged(true); - } - - void onBigIncrement(CCObject*) { - this->m_uncommittedValue += - std::static_pointer_cast(this->m_setting)->getBigArrowStepSize(); - this->valueChanged(true); - } - - void onBigDecrement(CCObject*) { - this->m_uncommittedValue -= - std::static_pointer_cast(this->m_setting)->getBigArrowStepSize(); - this->valueChanged(true); - } - }; - }; - - template - class ImplSlider { - protected: - Slider* m_slider = nullptr; - - C* self() { - return static_cast(this); - } - - static float valueToSlider(std::shared_ptr setting, typename T::value_t num) { - auto min = setting->getMin() ? setting->getMin().value() : -100; - auto max = setting->getMax() ? setting->getMax().value() : +100; - auto range = max - min; - return static_cast(clamp(static_cast(num - min) / range, 0.0, 1.0)); - } - - static typename T::value_t valueFromSlider(std::shared_ptr setting, float num) { - auto min = setting->getMin() ? setting->getMin().value() : -100; - auto max = setting->getMax() ? setting->getMax().value() : +100; - auto range = max - min; - auto value = static_cast(num * range + min); - if (auto step = setting->getSliderStepSize()) { - value = - static_cast(round(value / step.value()) * step.value()); - } - return value; - } - - void setupSlider(std::shared_ptr setting, float width) { - if (setting->hasSlider()) { - m_slider = - Slider::create(self(), menu_selector(ImplSlider::Callbacks::onSlider), .5f); - m_slider->setPosition(-50.f, -15.f); - self()->m_menu->addChild(m_slider); - - this->updateSlider(); - } - } - - void updateSlider() { - if (m_slider) { - auto setting = std::static_pointer_cast(self()->m_setting); - m_slider->setValue(valueToSlider(setting, self()->m_uncommittedValue)); - m_slider->updateBar(); - } - } - - // intentionally a subclass to make it relatively safe to - // use as callbacks for the child class, since we give the - // child class as the target to CCMenuItemSpriteExtra - struct Callbacks : public C { - void onSlider(CCObject* slider) { - auto setting = std::static_pointer_cast(this->m_setting); - - this->m_uncommittedValue = - valueFromSlider(setting, static_cast(slider)->getValue()); - this->valueChanged(true); - } - }; - }; -} - -class BoolSettingNode : public GeodeSettingNode { +class BoolSettingNode : public GeodeSettingNode { protected: CCMenuItemToggler* m_toggle; void onToggle(CCObject*); void valueChanged(bool updateText) override; - bool setup(std::shared_ptr setting, float width) override; + bool setup(BoolSettingValue* setting, float width) override; + +public: + IMPL_SETT_CREATE(Bool); }; class IntSettingNode : - public GeodeSettingNode, - public ImplArrows, - public ImplInput, - public ImplSlider { + public GeodeSettingNode, + public TextInputDelegate +{ protected: - friend class ImplArrows; - friend class ImplInput; - friend class ImplSlider; + InputNode* m_input = nullptr; + CCLabelBMFont* m_label = nullptr; + Slider* m_slider = nullptr; + CCMenuItemSpriteExtra* m_decArrow = nullptr; + CCMenuItemSpriteExtra* m_incArrow = nullptr; + CCMenuItemSpriteExtra* m_bigDecArrow = nullptr; + CCMenuItemSpriteExtra* m_bigIncArrow = nullptr; void valueChanged(bool updateText) override; + void textChanged(CCTextInputNode* input) override; - float setupHeight(std::shared_ptr setting) const override; - bool setup(std::shared_ptr setting, float width) override; + float setupHeight(IntSettingValue* setting) const override; + bool setup(IntSettingValue* setting, float width) override; + +public: + void updateSlider(); + void updateLabel(); + + void onSlider(CCObject* slider); + void onArrow(CCObject* sender); + + IMPL_SETT_CREATE(Int); }; class FloatSettingNode : - public GeodeSettingNode, - public ImplArrows, - public ImplInput, - public ImplSlider { + public GeodeSettingNode, + public TextInputDelegate +{ protected: - friend class ImplArrows; - friend class ImplInput; - friend class ImplSlider; + InputNode* m_input = nullptr; + CCLabelBMFont* m_label = nullptr; + Slider* m_slider = nullptr; + CCMenuItemSpriteExtra* m_decArrow = nullptr; + CCMenuItemSpriteExtra* m_incArrow = nullptr; + CCMenuItemSpriteExtra* m_bigDecArrow = nullptr; + CCMenuItemSpriteExtra* m_bigIncArrow = nullptr; void valueChanged(bool updateText) override; + void textChanged(CCTextInputNode* input) override; - float setupHeight(std::shared_ptr setting) const override; - bool setup(std::shared_ptr setting, float width) override; + float setupHeight(FloatSettingValue* setting) const override; + bool setup(FloatSettingValue* setting, float width) override; + +public: + void updateSlider(); + void updateLabel(); + + void onSlider(CCObject* slider); + void onArrow(CCObject* sender); + + IMPL_SETT_CREATE(Float); }; class StringSettingNode : - public GeodeSettingNode, - public TextInputDelegate { + public GeodeSettingNode, + public TextInputDelegate +{ protected: InputNode* m_input; @@ -451,12 +272,16 @@ protected: void valueChanged(bool updateText) override; void updateLabel(); - bool setup(std::shared_ptr setting, float width) override; + bool setup(StringSettingValue* setting, float width) override; + +public: + IMPL_SETT_CREATE(String); }; class FileSettingNode : - public GeodeSettingNode, - public TextInputDelegate { + public GeodeSettingNode, + public TextInputDelegate +{ protected: InputNode* m_input; @@ -466,11 +291,14 @@ protected: void onPickFile(CCObject*); - bool setup(std::shared_ptr setting, float width) override; + bool setup(FileSettingValue* setting, float width) override; + +public: + IMPL_SETT_CREATE(File); }; class ColorSettingNode : - public GeodeSettingNode, + public GeodeSettingNode, public ColorPickPopupDelegate { protected: ColorChannelSprite* m_colorSpr; @@ -480,11 +308,14 @@ protected: void onSelectColor(CCObject*); - bool setup(std::shared_ptr setting, float width) override; + bool setup(ColorSettingValue* setting, float width) override; + +public: + IMPL_SETT_CREATE(Color); }; class ColorAlphaSettingNode : - public GeodeSettingNode, + public GeodeSettingNode, public ColorPickPopupDelegate { protected: ColorChannelSprite* m_colorSpr; @@ -494,5 +325,23 @@ protected: void onSelectColor(CCObject*); - bool setup(std::shared_ptr setting, float width) override; + bool setup(ColorAlphaSettingValue* setting, float width) override; + +public: + IMPL_SETT_CREATE(ColorAlpha); +}; + +class CustomSettingPlaceholderNode : public SettingNode { +protected: + void commit() override; + bool hasUncommittedChanges() override; + bool hasNonDefaultValue() override; + void resetToDefault() override; + + bool init(std::string const& key, float width); + +public: + static CustomSettingPlaceholderNode* create( + std::string const& key, float width + ); }; diff --git a/loader/src/ui/internal/settings/ModSettingsPopup.cpp b/loader/src/ui/internal/settings/ModSettingsPopup.cpp index 931572fc..f3852c44 100644 --- a/loader/src/ui/internal/settings/ModSettingsPopup.cpp +++ b/loader/src/ui/internal/settings/ModSettingsPopup.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "GeodeSettingNode.hpp" bool ModSettingsPopup::setup(Mod* mod) { m_noElasticity = true; @@ -31,8 +32,14 @@ bool ModSettingsPopup::setup(Mod* mod) { float totalHeight = .0f; std::vector rendered; bool hasBG = true; - for (auto& [_, sett] : mod->getSettings()) { - auto node = sett->createNode(layerSize.width); + for (auto& key : mod->getSettingKeys()) { + SettingNode* node; + if (auto sett = mod->getSetting(key)) { + node = sett->createNode(layerSize.width); + } + else { + node = CustomSettingPlaceholderNode::create(key, layerSize.width); + } node->setDelegate(this); totalHeight += node->getScaledContentSize().height; @@ -144,6 +151,17 @@ void ModSettingsPopup::onResetAll(CCObject*) { ); } +void ModSettingsPopup::settingValueCommitted(SettingNode*) { + if (this->hasUncommitted()) { + m_applyBtnSpr->setColor(cc3x(0xf)); + m_applyBtn->setEnabled(true); + } + else { + m_applyBtnSpr->setColor(cc3x(0x4)); + m_applyBtn->setEnabled(false); + } +} + void ModSettingsPopup::settingValueChanged(SettingNode*) { if (this->hasUncommitted()) { m_applyBtnSpr->setColor(cc3x(0xf)); diff --git a/loader/src/ui/internal/settings/ModSettingsPopup.hpp b/loader/src/ui/internal/settings/ModSettingsPopup.hpp index 82d9eef6..56358952 100644 --- a/loader/src/ui/internal/settings/ModSettingsPopup.hpp +++ b/loader/src/ui/internal/settings/ModSettingsPopup.hpp @@ -13,7 +13,8 @@ protected: CCMenuItemSpriteExtra* m_applyBtn; ButtonSprite* m_applyBtnSpr; - void settingValueChanged(SettingNode* node) override; + void settingValueChanged(SettingNode*) override; + void settingValueCommitted(SettingNode*) override; bool setup(Mod* mod) override; bool hasUncommitted() const; diff --git a/loader/src/utils/JsonValidation.cpp b/loader/src/utils/JsonValidation.cpp index 3303e564..6cc3fa99 100644 --- a/loader/src/utils/JsonValidation.cpp +++ b/loader/src/utils/JsonValidation.cpp @@ -19,6 +19,11 @@ bool JsonMaybeSomething::isError() const { return m_checker.isError() || !m_hasValue; } +template +std::string JsonMaybeSomething::getError() const { + return m_checker.getError(); +} + template JsonMaybeSomething::operator bool() const { return !isError(); diff --git a/loader/src/utils/file.cpp b/loader/src/utils/file.cpp index d73d6537..c11c9727 100644 --- a/loader/src/utils/file.cpp +++ b/loader/src/utils/file.cpp @@ -11,6 +11,14 @@ USE_GEODE_NAMESPACE(); using namespace geode::utils::file; +void ghc::filesystem::to_json(nlohmann::json& json, path const& path) { + json = path.string(); +} + +void ghc::filesystem::from_json(nlohmann::json const& json, path& path) { + path = json.get(); +} + Result utils::file::readString(ghc::filesystem::path const& path) { #if _WIN32 std::ifstream in(path.wstring(), std::ios::in | std::ios::binary); diff --git a/loader/test/dependency/CMakeLists.txt b/loader/test/dependency/CMakeLists.txt index c66eb7e1..45c708f4 100644 --- a/loader/test/dependency/CMakeLists.txt +++ b/loader/test/dependency/CMakeLists.txt @@ -18,4 +18,4 @@ target_link_libraries( geode-sdk ) -create_geode_file(${PROJECT_NAME} DONT_INSTALL) +create_geode_file(${PROJECT_NAME}) diff --git a/loader/test/dependency/main.cpp b/loader/test/dependency/main.cpp index 3904d2ee..1c407338 100644 --- a/loader/test/dependency/main.cpp +++ b/loader/test/dependency/main.cpp @@ -3,6 +3,134 @@ USE_GEODE_NAMESPACE(); #include +#include + +enum class Icon { + Steve, + Mike, + LazarithTheDestroyerOfForsakenSouls, + Geoff, +}; +constexpr Icon DEFAULT_ICON = Icon::Steve; + +class MySettingValue; + +class MySettingValue : public SettingValue { +protected: + Icon m_icon; + +public: + MySettingValue(std::string const& key, Icon icon) + : SettingValue(key), m_icon(icon) {} + + bool load(nlohmann::json const& json) override { + try { + m_icon = static_cast(json.get()); + return true; + } catch(...) { + return false; + } + } + bool save(nlohmann::json& json) const override { + json = static_cast(m_icon); + return true; + } + SettingNode* createNode(float width) override; + + void setIcon(Icon icon) { + m_icon = icon; + } + Icon getIcon() const { + return m_icon; + } +}; + +class MySettingNode : public SettingNode { +protected: + Icon m_currentIcon; + std::vector m_sprites; + + bool init(MySettingValue* value, float width) { + if (!SettingNode::init(value)) + return false; + + m_currentIcon = value->getIcon(); + this->setContentSize({ width, 40.f }); + + auto menu = CCMenu::create(); + menu->setPosition(width / 2, 20.f); + + float x = -75.f; + + for (auto& [spr, icon] : { + std::pair { "player_01_001.png", Icon::Steve, }, + std::pair { "player_02_001.png", Icon::Mike, }, + std::pair { "player_03_001.png", Icon::LazarithTheDestroyerOfForsakenSouls, }, + std::pair { "player_04_001.png", Icon::Geoff, }, + }) { + auto btnSpr = CCSprite::createWithSpriteFrameName(spr); + btnSpr->setScale(.7f); + m_sprites.push_back(btnSpr); + if (icon == m_currentIcon) { + btnSpr->setColor({ 0, 255, 0 }); + } else { + btnSpr->setColor({ 200, 200, 200 }); + } + auto btn = CCMenuItemSpriteExtra::create( + btnSpr, this, menu_selector(MySettingNode::onSelect) + ); + btn->setTag(static_cast(icon)); + btn->setPosition(x, 0); + menu->addChild(btn); + + x += 50.f; + } + + this->addChild(menu); + + return true; + } + + void onSelect(CCObject* sender) { + for (auto& spr : m_sprites) { + spr->setColor({ 200, 200, 200 }); + } + m_currentIcon = static_cast(sender->getTag()); + static_cast( + static_cast(sender)->getNormalImage() + )->setColor({ 0, 255, 0 }); + this->dispatchChanged(); + } + +public: + void commit() override { + static_cast(m_value)->setIcon(m_currentIcon); + this->dispatchCommitted(); + } + bool hasUncommittedChanges() override { + return m_currentIcon != static_cast(m_value)->getIcon(); + } + bool hasNonDefaultValue() override { + return m_currentIcon != DEFAULT_ICON; + } + void resetToDefault() override { + m_currentIcon = DEFAULT_ICON; + } + + static MySettingNode* create(MySettingValue* value, float width) { + auto ret = new MySettingNode; + if (ret && ret->init(value, width)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; + } +}; + +SettingNode* MySettingValue::createNode(float width) { + return MySettingNode::create(this, width); +} struct MyMenuLayer : Modify { void onMoreGames(CCObject*) { @@ -10,13 +138,21 @@ struct MyMenuLayer : Modify { FLAlertLayer::create("Damn", ":(", "OK")->show(); } else { - FLAlertLayer::create("Yay", "The weather report said it wouldn't rain today :)", "OK") - ->show(); + FLAlertLayer::create( + "Yay", + "The weather report said it wouldn't rain today :)", + "OK" + )->show(); } } }; -GEODE_API bool GEODE_CALL geode_load(Mod*) { +$on_mod(Loaded) { + Mod::get()->registerCustomSetting( + "overcast-skies", + std::make_unique("overcast-skies", DEFAULT_ICON) + ); + // Dispatcher::get()->addFunction("test-garage-open", [](GJGarageLayer* // gl) { auto label = CCLabelBMFont::create("Dispatcher works!", "bigFont.fnt"); // label->setPosition(100, 80); @@ -24,5 +160,4 @@ GEODE_API bool GEODE_CALL geode_load(Mod*) { // label->setZOrder(99999); // gl->addChild(label); // }); - return true; } diff --git a/loader/test/dependency/mod.json b/loader/test/dependency/mod.json index 0b373132..63083aa8 100644 --- a/loader/test/dependency/mod.json +++ b/loader/test/dependency/mod.json @@ -76,6 +76,9 @@ } ] } + }, + "overcast-skies": { + "type": "custom" } } } diff --git a/loader/test/main/CMakeLists.txt b/loader/test/main/CMakeLists.txt index 84b8cdfb..9a11d192 100644 --- a/loader/test/main/CMakeLists.txt +++ b/loader/test/main/CMakeLists.txt @@ -16,4 +16,4 @@ target_link_libraries( geode-sdk ) -create_geode_file(${PROJECT_NAME} DONT_INSTALL) +create_geode_file(${PROJECT_NAME}) diff --git a/loader/test/main/mod.json b/loader/test/main/mod.json index 9ccd1f6d..39a806ff 100644 --- a/loader/test/main/mod.json +++ b/loader/test/main/mod.json @@ -8,7 +8,7 @@ "dependencies": [ { "id": "geode.testdep", - "version": "1.0.*", + "version": ">=1.0.0", "required": true } ]