From 89d1a5140f5bb15048bb50e00801c33fed42c263 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:34:33 +0300 Subject: [PATCH] new settings stuff that i need to commit because main broke --- loader/include/Geode/loader/ModMetadata.hpp | 9 +- loader/include/Geode/loader/SettingV3.hpp | 325 ++++++++ loader/include/Geode/utils/JsonValidation.hpp | 191 ++++- loader/src/loader/ModImpl.cpp | 5 +- loader/src/loader/ModImpl.hpp | 5 +- loader/src/loader/ModMetadataImpl.cpp | 126 ++- loader/src/loader/ModMetadataImpl.hpp | 3 +- loader/src/loader/ModSettingsManager.cpp | 11 + loader/src/loader/ModSettingsManager.hpp | 25 + loader/src/loader/SettingV3.cpp | 746 ++++++++++++++++++ loader/src/utils/JsonValidation.cpp | 424 ++++++---- 11 files changed, 1623 insertions(+), 247 deletions(-) create mode 100644 loader/include/Geode/loader/SettingV3.hpp create mode 100644 loader/src/loader/ModSettingsManager.cpp create mode 100644 loader/src/loader/ModSettingsManager.hpp create mode 100644 loader/src/loader/SettingV3.cpp diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index d0e3c793..9628d5ee 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -184,7 +184,12 @@ namespace geode { * Mod settings * @note Not a map because insertion order must be preserved */ - [[nodiscard]] std::vector> getSettings() const; + [[nodiscard, deprecated("Use getSettingsV3")]] std::vector> getSettings() const; + /** + * Mod settings + * @note Not a map because insertion order must be preserved + */ + [[nodiscard]] std::vector> getSettingsV3() const; /** * Get the tags for this mod */ @@ -232,7 +237,9 @@ namespace geode { void setDependencies(std::vector const& value); void setIncompatibilities(std::vector const& value); void setSpritesheets(std::vector const& value); + [[deprecated("This function does NOTHING")]] void setSettings(std::vector> const& value); + void setSettings(std::vector> const& value); void setTags(std::unordered_set const& value); void setNeedsEarlyLoad(bool const& value); void setIsAPI(bool const& value); diff --git a/loader/include/Geode/loader/SettingV3.hpp b/loader/include/Geode/loader/SettingV3.hpp new file mode 100644 index 00000000..c25afc0d --- /dev/null +++ b/loader/include/Geode/loader/SettingV3.hpp @@ -0,0 +1,325 @@ +#pragma once + +#include "../DefaultInclude.hpp" +#include +#include +// todo: remove this header in 4.0.0 +#include "Setting.hpp" + +namespace geode { + class SettingNodeV3; + class JsonExpectedValue; + + class GEODE_DLL SettingV3 { + private: + class GeodeImpl; + std::shared_ptr m_impl; + + public: + SettingV3(std::string const& key, std::string const& modID); + virtual ~SettingV3(); + + /** + * Get the key of this setting + */ + std::string getKey() const; + /** + * Get the mod ID this setting is for + */ + std::string getModID() const; + /** + * Get the mod this setting is for. Note that this may return null + * while the mod is still being initialized + */ + Mod* getMod() const; + + virtual Result<> parse(std::string const& modID, matjson::Value const& json) = 0; + virtual bool load(matjson::Value const& json) = 0; + virtual bool save(matjson::Value& json) const = 0; + virtual SettingNodeV3* createNode(float width) = 0; + + virtual bool isDefaultValue() const = 0; + /** + * Reset this setting's value back to its original value + */ + virtual void reset() = 0; + + [[deprecated("This function will be removed alongside legacy settings in 4.0.0!")]] + virtual std::optional convertToLegacy() const; + [[deprecated("This function will be removed alongside legacy settings in 4.0.0!")]] + virtual std::optional> convertToLegacyValue() const; + + static Result> parseBuiltin(std::string const& modID, matjson::Value const& json); + }; + + namespace detail { + class GEODE_DLL GeodeSettingBaseV3 : public SettingV3 { + private: + class Impl; + std::shared_ptr m_impl; + + protected: + Result<> parseShared(JsonExpectedValue& json); + + public: + std::string getName() const; + std::optional getDescription() const; + std::optional getEnableIf() const; + }; + } + + class GEODE_DLL TitleSettingV3 final : public SettingV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + std::string getTitle() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + }; + + class GEODE_DLL UnresolvedCustomSettingV3 final : public SettingV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL BoolSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + bool getValue() const; + void setValue(bool value); + Result<> isValid(bool value) const; + + bool getDefaultValue() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL IntSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + int64_t getValue() const; + void setValue(int64_t value); + Result<> isValid(int64_t value) const; + + int64_t getDefaultValue() const; + std::optional getMinValue() const; + std::optional getMaxValue() const; + + bool isArrowsEnabled() const; + bool isBigArrowsEnabled() const; + size_t getArrowStepSize() const; + size_t getBigArrowStepSize() const; + bool isSliderEnabled() const; + std::optional getSliderSnap() const; + bool isInputEnabled() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL FloatSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + double getValue() const; + void setValue(double value); + Result<> isValid(double value) const; + + double getDefaultValue() const; + std::optional getMinValue() const; + std::optional getMaxValue() const; + + bool isArrowsEnabled() const; + bool isBigArrowsEnabled() const; + size_t getArrowStepSize() const; + size_t getBigArrowStepSize() const; + bool isSliderEnabled() const; + std::optional getSliderSnap() const; + bool isInputEnabled() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL StringSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + std::string getValue() const; + void setValue(std::string_view value); + Result<> isValid(std::string_view value) const; + + std::string getDefaultValue() const; + + std::optional getRegexValidator() const; + std::optional getAllowedCharacters() const; + std::optional> getEnumOptions() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL FileSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + std::filesystem::path getValue() const; + void setValue(std::filesystem::path const& value); + Result<> isValid(std::filesystem::path value) const; + + std::filesystem::path getDefaultValue() const; + std::optional> getFilters() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL Color3BSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + cocos2d::ccColor3B getValue() const; + void setValue(cocos2d::ccColor3B value); + Result<> isValid(cocos2d::ccColor3B value) const; + + cocos2d::ccColor3B getDefaultValue() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL Color4BSettingV3 final : public detail::GeodeSettingBaseV3 { + private: + class Impl; + std::shared_ptr m_impl; + + public: + cocos2d::ccColor4B getValue() const; + void setValue(cocos2d::ccColor4B value); + Result<> isValid(cocos2d::ccColor4B value) const; + + cocos2d::ccColor4B getDefaultValue() const; + + Result<> parse(std::string const& modID, matjson::Value const& json) override; + bool load(matjson::Value const& json) override; + bool save(matjson::Value& json) const override; + SettingNodeV3* createNode(float width) override; + + bool isDefaultValue() const override; + void reset() override; + + std::optional convertToLegacy() const override; + std::optional> convertToLegacyValue() const override; + }; + + class GEODE_DLL SettingNodeV3 : public cocos2d::CCNode { + protected: + bool init(); + + /** + * Mark this setting as changed. This updates the UI for committing + * the value + */ + void markChanged(); + + /** + * When the setting value is committed (aka can't be undone), this + * function will be called. This should take care of actually saving + * the value in some sort of global manager + */ + virtual void onCommit() = 0; + + void dispatchChanged(); + void dispatchCommitted(); + + public: + virtual void commit() = 0; + virtual bool hasUncommittedChanges() = 0; + virtual bool hasNonDefaultValue() = 0; + virtual void resetToDefault() = 0; + }; +} diff --git a/loader/include/Geode/utils/JsonValidation.hpp b/loader/include/Geode/utils/JsonValidation.hpp index 12190338..6ac23168 100644 --- a/loader/include/Geode/utils/JsonValidation.hpp +++ b/loader/include/Geode/utils/JsonValidation.hpp @@ -77,7 +77,9 @@ namespace geode { struct JsonMaybeObject; struct JsonMaybeValue; - struct GEODE_DLL JsonMaybeSomething { + struct GEODE_DLL + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + JsonMaybeSomething { protected: JsonChecker& m_checker; matjson::Value& m_json; @@ -102,7 +104,9 @@ namespace geode { operator bool() const; }; - struct GEODE_DLL JsonMaybeValue : public JsonMaybeSomething { + struct GEODE_DLL + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + JsonMaybeValue : public JsonMaybeSomething { bool m_inferType = true; JsonMaybeValue( @@ -254,7 +258,9 @@ namespace geode { Iterator> items(); }; - struct GEODE_DLL JsonMaybeObject : JsonMaybeSomething { + struct + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + GEODE_DLL JsonMaybeObject : JsonMaybeSomething { std::set m_knownKeys; JsonMaybeObject( @@ -276,7 +282,9 @@ namespace geode { void checkUnknownKeys(); }; - struct GEODE_DLL JsonChecker { + struct + [[deprecated("Use JsonExpectedValue via the checkJson function instead")]] + GEODE_DLL JsonChecker { std::variant m_result; matjson::Value& m_json; @@ -289,4 +297,179 @@ namespace geode { JsonMaybeValue root(std::string const& hierarchy); }; + class GEODE_DLL JsonExpectedValue final { + protected: + class Impl; + std::unique_ptr m_impl; + + JsonExpectedValue(); + JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key); + + bool hasError() const; + void setError(std::string_view error); + + matjson::Value const& getJSONRef() const; + + template + void setError(fmt::format_string error, Args&&... args) { + this->setError(fmt::format(error, std::forward(args)...)); + } + + template + std::optional tryGet() { + if (this->hasError()) return std::nullopt; + try { + return this->getJSONRef().template as(); + } + catch(matjson::JsonException const& e) { + this->setError("invalid json type: {}", e); + } + return std::nullopt; + } + + public: + JsonExpectedValue(matjson::Value const& value, std::string_view rootScopeName); + ~JsonExpectedValue(); + + JsonExpectedValue(JsonExpectedValue&&); + JsonExpectedValue& operator=(JsonExpectedValue&&); + JsonExpectedValue(JsonExpectedValue const&) = delete; + JsonExpectedValue& operator=(JsonExpectedValue const&) = delete; + + /** + * Get a copy of the underlying raw JSON value + */ + matjson::Value json() const; + /** + * Get the key name of this JSON value. If this is an array index, + * returns the index as a string. If this is the root object, + * returns the root scope name. + */ + std::string key() const; + + /** + * Check the type of this JSON value. Does not set an error. If an + * error is already set, always returns false + */ + bool is(matjson::Type type) const; + bool isNull() const; + bool isBool() const; + bool isNumber() const; + bool isString() const; + bool isArray() const; + bool isObject() const; + /** + * Asserts that this JSON value is of the specified type. If it is + * not, an error is set and all subsequent operations are no-ops + * @returns Itself + */ + JsonExpectedValue& assertIs(matjson::Type type); + JsonExpectedValue& assertIsNull(); + JsonExpectedValue& assertIsBool(); + JsonExpectedValue& assertIsNumber(); + JsonExpectedValue& assertIsString(); + JsonExpectedValue& assertIsArray(); + JsonExpectedValue& assertIsObject(); + /** + * Asserts that this JSON value is one of a list of specified types + * @returns Itself + */ + JsonExpectedValue& assertIs(std::initializer_list type); + + // -- Dealing with values -- + + template + T get() { + if (auto v = this->template tryGet()) { + return *std::move(v); + } + return T(); + } + template + JsonExpectedValue& into(T& value) { + if (auto v = this->template tryGet()) { + value = *std::move(v); + } + return *this; + } + template + JsonExpectedValue& into(std::optional& value) { + if (auto v = this->template tryGet()) { + value.emplace(*std::move(v)); + } + return *this; + } + template + JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires { + { predicate(std::declval()) } -> std::convertible_to; + } { + if (this->hasError()) return *this; + if (auto v = this->template tryGet()) { + if (!predicate(v)) { + this->setError("json value is not {}", name); + } + } + return *this; + } + + // -- Dealing with objects -- + + /** + * Check if this object has an optional key. Asserts that this JSON + * value is an object. If the key doesn't exist, returns a + * `JsonExpectValue` that does nothing + * @returns The key, which is a no-op value if it didn't exist + */ + JsonExpectedValue has(std::string_view key); + /** + * Check if this object has an optional key. Asserts that this JSON + * value is an object. If the key doesn't exist, sets an error and + * returns a `JsonExpectValue` that does nothing + * @returns The key, which is a no-op value if it didn't exist + */ + JsonExpectedValue needs(std::string_view key); + /** + * Asserts that this JSON value is an object. Get all object + * properties + */ + std::vector> properties(); + /** + * Asserts that this JSON value is an object. Logs unknown keys to + * the console as warnings + */ + void checkUnknownKeys(); + + // -- Dealing with arrays -- + + /** + * Asserts that this JSON value is an array. Returns the length of + * the array, or 0 on error + */ + size_t length(); + /** + * Asserts that this JSON value is an array. Returns the value at + * the specified index. If there is no value at that index, sets an + * error + */ + JsonExpectedValue at(size_t index); + /** + * Asserts that this JSON value is an array. Returns the array items + * @warning The old JsonChecker used `items` for iterating object + * properties - on this new API that function is called `properties`! + */ + std::vector items(); + + operator bool() const; + + Result<> ok(); + template + Result ok(T&& value) { + auto ok = this->ok(); + if (!ok) { + return Err(ok.unwrapErr()); + } + return Ok(std::move(value)); + } + }; + GEODE_DLL JsonExpectedValue checkJson(matjson::Value const& json, std::string_view rootScopeName); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 43b3271d..be2c24a4 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -341,9 +341,10 @@ std::optional Mod::Impl::getSettingDefinition(std::string_view const ke SettingValue* Mod::Impl::getSetting(std::string_view const key) const { auto keystr = std::string(key); - if (m_settings.count(keystr)) { - return m_settings.at(keystr).get(); + if (auto value = m_settings.at(keystr)->convertToLegacyValue()) { + return value->get(); + } } return nullptr; } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index 2346fd10..9dad1934 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -4,6 +4,7 @@ #include "ModPatch.hpp" #include #include +#include "ModSettingsManager.hpp" namespace geode { class Mod::Impl { @@ -48,9 +49,9 @@ namespace geode { */ matjson::Value m_saved = matjson::Object(); /** - * Setting values + * Setting values. This is behind unique_ptr for interior mutability */ - std::unordered_map> m_settings; + std::unique_ptr m_settings = std::make_unique(); /** * Settings save data. Stored for efficient loading of custom settings */ diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index cacc4aa6..9b601b1f 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -125,45 +125,28 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } catch (...) { } - JsonChecker checker(impl->m_rawJSON); - auto root = checker.root(checkerRoot).obj(); - + auto root = checkJson(impl->m_rawJSON, checkerRoot); root.needs("geode").into(impl->m_geodeVersion); - root.addKnownKey("gd"); - - // Check GD version - // (use rawJson because i dont like JsonMaybeValue) - if (rawJson.contains("gd")) { - std::string ver; - if (rawJson["gd"].is_object()) { - auto key = PlatformID::toShortString(GEODE_PLATFORM_TARGET, true); - if (rawJson["gd"].contains(key) && rawJson["gd"][key].is_string()) - ver = rawJson["gd"][key].as_string(); - } else if (rawJson["gd"].is_string()) { + + if (auto gd = root.needs("gd")) { + // In the future when we get rid of support for string format just + // change all of this to the gd.needs(...) stuff + gd.assertIs({ matjson::Type::Object, matjson::Type::String }); + if (gd.isObject()) { + gd.needs(GEODE_PLATFORM_SHORT_IDENTIFIER) + .mustBe("a valid gd version", [](auto const& str) { + return str == "*" || numFromString(str).isOk(); + }) + .into(impl->m_gdVersion); + } + else if (gd.isString()) { impl->m_softInvalidReason = "mod.json uses old syntax"; - goto dontCheckVersion; - } else { - return Err("[mod.json] has invalid target GD version"); } - if (ver.empty()) { - // this will show an error later on, but will at least load the rest of the metadata - ver = "0.000"; - } - if (ver != "*") { - auto res = numFromString(ver); - if (res.isErr()) { - return Err("[mod.json] has invalid target GD version"); - } - impl->m_gdVersion = ver; - } - } else { - return Err("[mod.json] is missing target GD version"); } - dontCheckVersion: + constexpr auto ID_REGEX = "[a-z0-9\\-_]+\\.[a-z0-9\\-_]+"; root.needs("id") - // todo: make this use validateID in full 2.0.0 release - .validate(MiniFunction(&ModMetadata::Impl::validateOldID)) + .mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID) .into(impl->m_id); // if (!isDeprecatedIDForm(impl->m_id)) { @@ -180,7 +163,7 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs if (root.has("developer")) { return Err("[mod.json] can not have both \"developer\" and \"developers\" specified"); } - for (auto& dev : root.needs("developers").iterate()) { + for (auto& dev : root.needs("developers").items()) { impl->m_developers.push_back(dev.template get()); } } @@ -205,11 +188,9 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs }); } - for (auto& dep : root.has("dependencies").iterate()) { - auto obj = dep.obj(); - - bool onThisPlatform = !obj.has("platforms"); - for (auto& plat : obj.has("platforms").iterate()) { + for (auto& dep : root.has("dependencies").items()) { + bool onThisPlatform = !dep.has("platforms"); + for (auto& plat : dep.has("platforms").items()) { if (PlatformID::coveredBy(plat.get(), GEODE_PLATFORM_TARGET)) { onThisPlatform = true; } @@ -219,11 +200,10 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } Dependency dependency; - // todo: make this use validateID in full 2.0.0 release - obj.needs("id").validate(MiniFunction(&ModMetadata::Impl::validateOldID)).into(dependency.id); - obj.needs("version").into(dependency.version); - obj.has("importance").into(dependency.importance); - obj.checkUnknownKeys(); + dep.needs("id").mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID).into(dependency.id); + dep.needs("version").into(dependency.version); + dep.has("importance").into(dependency.importance); + dep.checkUnknownKeys(); if ( dependency.version.getComparison() != VersionCompare::MoreEq && @@ -247,24 +227,20 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs impl->m_dependencies.push_back(dependency); } - for (auto& incompat : root.has("incompatibilities").iterate()) { - auto obj = incompat.obj(); - + for (auto& incompat : root.has("incompatibilities").items()) { Incompatibility incompatibility; - obj.needs("id").validate(MiniFunction(&ModMetadata::Impl::validateOldID)).into(incompatibility.id); - obj.needs("version").into(incompatibility.version); - obj.has("importance").into(incompatibility.importance); - obj.checkUnknownKeys(); - + incompat.needs("id").mustBe(ID_REGEX, &ModMetadata::Impl::validateOldID).into(incompatibility.id); + incompat.needs("version").into(incompatibility.version); + incompat.has("importance").into(incompatibility.importance); + incompat.checkUnknownKeys(); impl->m_incompatibilities.push_back(incompatibility); } - for (auto& [key, value] : root.has("settings").items()) { + for (auto& [key, value] : root.has("settings").properties()) { // Skip settings not on this platform - if (value.template is()) { - auto obj = value.obj(); - bool onThisPlatform = !obj.has("platforms"); - for (auto& plat : obj.has("platforms").iterate()) { + if (value.is(matjson::Type::Object)) { + bool onThisPlatform = !value.has("platforms"); + for (auto& plat : value.has("platforms").items()) { if (PlatformID::coveredBy(plat.get(), GEODE_PLATFORM_TARGET)) { onThisPlatform = true; } @@ -274,24 +250,24 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } } - GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value)); + GEODE_UNWRAP_INTO(auto sett, SettingV3::parseBuiltin(impl->m_id, value.json())); impl->m_settings.emplace_back(key, sett); } - if (auto resources = root.has("resources").obj()) { - for (auto& [key, _] : resources.has("spritesheets").items()) { + if (auto resources = root.has("resources")) { + for (auto& [key, _] : resources.has("spritesheets").properties()) { impl->m_spritesheets.push_back(impl->m_id + "/" + key); } } - if (auto issues = root.has("issues").obj()) { + if (auto issues = root.has("issues")) { IssuesInfo issuesInfo; issues.needs("info").into(issuesInfo.info); - issues.has("url").intoAs(issuesInfo.url); + issues.has("url").into(issuesInfo.url); impl->m_issues = issuesInfo; } - if (auto links = root.has("links").obj()) { + if (auto links = root.has("links")) { links.has("homepage").into(info.getLinksMut().getImpl()->m_homepage); links.has("source").into(info.getLinksMut().getImpl()->m_source); links.has("community").into(info.getLinksMut().getImpl()->m_community); @@ -299,19 +275,15 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs } // Tags. Actual validation is done when interacting with the server in the UI - for (auto& tag : root.has("tags").iterate()) { + for (auto& tag : root.has("tags").items()) { impl->m_tags.insert(tag.template get()); } // with new cli, binary name is always mod id impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION; - if (checker.isError()) { - return Err(checker.getError()); - } root.checkUnknownKeys(); - - return Ok(info); + return root.ok(info); } Result ModMetadata::Impl::create(ModJson const& json) { @@ -542,6 +514,18 @@ std::vector ModMetadata::getSpritesheets() const { return m_impl->m_spritesheets; } std::vector> ModMetadata::getSettings() const { + std::vector> res; + for (auto [key, sett] : m_impl->m_settings) { + auto checker = JsonChecker(sett); + auto value = checker.root(""); + auto legacy = Setting::parse(key, m_impl->m_id, value); + if (!checker.isError() && legacy.isOk()) { + res.push_back(std::make_pair(key, *legacy)); + } + } + return res; +} +std::vector> ModMetadata::getSettingsV3() const { return m_impl->m_settings; } std::unordered_set ModMetadata::getTags() const { @@ -643,6 +627,10 @@ void ModMetadata::setSpritesheets(std::vector const& value) { m_impl->m_spritesheets = value; } void ModMetadata::setSettings(std::vector> const& value) { + // intentionally no-op because no one is supposed to be using this + // without subscribing to "internals are not stable" mentality +} +void ModMetadata::setSettings(std::vector> const& value) { m_impl->m_settings = value; } void ModMetadata::setTags(std::unordered_set const& value) { diff --git a/loader/src/loader/ModMetadataImpl.hpp b/loader/src/loader/ModMetadataImpl.hpp index b47d092c..fbf97448 100644 --- a/loader/src/loader/ModMetadataImpl.hpp +++ b/loader/src/loader/ModMetadataImpl.hpp @@ -4,6 +4,7 @@ #include #include #include +#include using namespace geode::prelude; @@ -36,7 +37,7 @@ namespace geode { std::vector m_dependencies; std::vector m_incompatibilities; std::vector m_spritesheets; - std::vector> m_settings; + std::vector> m_settings; std::unordered_set m_tags; bool m_needsEarlyLoad = false; bool m_isAPI = false; diff --git a/loader/src/loader/ModSettingsManager.cpp b/loader/src/loader/ModSettingsManager.cpp new file mode 100644 index 00000000..12eefff5 --- /dev/null +++ b/loader/src/loader/ModSettingsManager.cpp @@ -0,0 +1,11 @@ +#include "ModSettingsManager.hpp" + +SettingV3* ModSettingsManager::get(std::string const& id) {} + +SettingValue* ModSettingsManager::getLegacy(std::string const& id) { + // If this setting has alreay been given a legacy interface, give that + if (m_legacy.count(id)) { + return m_legacy.at(id).get(); + } + if (m_v3.count(id)) {} +} diff --git a/loader/src/loader/ModSettingsManager.hpp b/loader/src/loader/ModSettingsManager.hpp new file mode 100644 index 00000000..455f220b --- /dev/null +++ b/loader/src/loader/ModSettingsManager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +using namespace geode::prelude; + +// This class should NEVER be exposed in a header!!! +// It is an implementation detail!!! + +class ModSettingsManager final { +private: + struct SettingInfo final { + std::unique_ptr v3; + std::unique_ptr legacy; + }; + + std::unordered_map> m_v3; + // todo: remove in v4 + std::unordered_map> m_legacy; + +public: + SettingV3* get(std::string const& id); + SettingValue* getLegacy(std::string const& id); +}; diff --git a/loader/src/loader/SettingV3.cpp b/loader/src/loader/SettingV3.cpp new file mode 100644 index 00000000..ab6b95fb --- /dev/null +++ b/loader/src/loader/SettingV3.cpp @@ -0,0 +1,746 @@ +#include +#include + +using namespace geode::prelude; + +class SettingV3::GeodeImpl { +public: + std::string modID; + std::string key; +}; + +SettingV3::~SettingV3() = default; + +SettingV3::SettingV3(std::string const& key, std::string const& modID) + : m_impl(std::make_shared()) +{ + m_impl->key = key; + m_impl->modID = modID; +} + +std::string SettingV3::getKey() const { + return m_impl->key; +} +std::string SettingV3::getModID() const { + return m_impl->modID; +} +Mod* SettingV3::getMod() const { + return Loader::get()->getInstalledMod(m_impl->modID); +} + +Result> SettingV3::parseBuiltin(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "SettingV3"); + std::string type; + root.needs("type").into(type); + std::shared_ptr ret; + switch (hash(type)) { + case hash("bool"): ret = std::make_shared(); break; + case hash("int"): ret = std::make_shared(); break; + case hash("float"): ret = std::make_shared(); break; + case hash("string"): ret = std::make_shared(); break; + case hash("rgb"): case hash("color"): ret = std::make_shared(); break; + case hash("rgba"): ret = std::make_shared(); break; + case hash("path"): case hash("file"): ret = std::make_shared(); break; + case hash("custom"): ret = std::make_shared(); break; + case hash("title"): ret = std::make_shared(); break; + } + GEODE_UNWRAP(ret->parse(modID, json)); + return root.ok(ret); +} +std::optional SettingV3::convertToLegacy() const { + return std::nullopt; +} +std::optional> SettingV3::convertToLegacyValue() const { + return std::nullopt; +} + +class geode::detail::GeodeSettingBaseV3::Impl final { +public: + std::string name; + std::optional description; + std::optional enableIf; +}; + +std::string geode::detail::GeodeSettingBaseV3::getName() const { + return m_impl->name; +} +std::optional geode::detail::GeodeSettingBaseV3::getDescription() const { + return m_impl->description; +} +std::optional geode::detail::GeodeSettingBaseV3::getEnableIf() const { + return m_impl->enableIf; +} + +Result<> geode::detail::GeodeSettingBaseV3::parseShared(JsonExpectedValue& json) { + json.needs("name").into(m_impl->name); + json.needs("description").into(m_impl->description); + json.needs("enable-if").into(m_impl->enableIf); + return Ok(); +} + +class TitleSettingV3::Impl final { +public: + std::string title; +}; + +std::string TitleSettingV3::getTitle() const { + return m_impl->title; +} + +Result<> TitleSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "TitleSettingV3"); + root.needs("title").into(m_impl->title); + root.checkUnknownKeys(); + return root.ok(); +} +bool TitleSettingV3::load(matjson::Value const& json) { + return true; +} +bool TitleSettingV3::save(matjson::Value&) const { + return true; +} +SettingNodeV3* TitleSettingV3::createNode(float width) { + // todo +} +bool TitleSettingV3::isDefaultValue() const { + return true; +} +void TitleSettingV3::reset() {} + +class UnresolvedCustomSettingV3::Impl final { +public: + matjson::Value json; +}; + +Result<> UnresolvedCustomSettingV3::parse(std::string const& modID, matjson::Value const& json) { + m_impl->json = json; + return Ok(); +} +bool UnresolvedCustomSettingV3::load(matjson::Value const& json) { + return true; +} +bool UnresolvedCustomSettingV3::save(matjson::Value& json) const { + return true; +} +SettingNodeV3* UnresolvedCustomSettingV3::createNode(float width) { + // todo +} + +bool UnresolvedCustomSettingV3::isDefaultValue() const { + return true; +} +void UnresolvedCustomSettingV3::reset() {} + +std::optional UnresolvedCustomSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(CustomSetting { + .json = std::make_shared(m_impl->json) + })); +} +std::optional> UnresolvedCustomSettingV3::convertToLegacyValue() const { + return std::nullopt; +} + +class BoolSettingV3::Impl final { +public: + bool value; + bool defaultValue; +}; + +bool BoolSettingV3::getValue() const { + return m_impl->value; +} +void BoolSettingV3::setValue(bool value) { + m_impl->value = value; +} +Result<> BoolSettingV3::isValid(bool value) const {} +bool BoolSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} + +Result<> BoolSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "BoolSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.checkUnknownKeys(); + return root.ok(); +} +bool BoolSettingV3::load(matjson::Value const& json) { + if (json.is_bool()) { + m_impl->value = json.as_bool(); + return true; + } + return false; +} +bool BoolSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* BoolSettingV3::createNode(float width) { + // todo +} +bool BoolSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; +} +void BoolSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional BoolSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(BoolSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + })); +} +std::optional> BoolSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class IntSettingV3::Impl final { +public: + int64_t value; + int64_t defaultValue; + std::optional minValue; + std::optional maxValue; + + struct { + // 0 means not enabled + size_t arrowStepSize; + size_t bigArrowStepSize; + bool sliderEnabled; + std::optional sliderSnap; + bool textInputEnabled; + } controls; +}; + +bool IntSettingV3::isArrowsEnabled() const { + return m_impl->controls.arrowStepSize > 0; +} +bool IntSettingV3::isBigArrowsEnabled() const { + return m_impl->controls.bigArrowStepSize > 0; +} +size_t IntSettingV3::getArrowStepSize() const { + return m_impl->controls.arrowStepSize; +} +size_t IntSettingV3::getBigArrowStepSize() const { + return m_impl->controls.bigArrowStepSize; +} +bool IntSettingV3::isSliderEnabled() const { + return m_impl->controls.sliderEnabled; +} +std::optional IntSettingV3::getSliderSnap() const { + return m_impl->controls.sliderSnap; +} +bool IntSettingV3::isInputEnabled() const { + return m_impl->controls.textInputEnabled; +} + +int64_t IntSettingV3::getValue() const { + return m_impl->value; +} +void IntSettingV3::setValue(int64_t value) { + m_impl->value = clamp( + value, + m_impl->minValue.value_or(std::numeric_limits::min()), + m_impl->maxValue.value_or(std::numeric_limits::max()) + ); +} +int64_t IntSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} +std::optional IntSettingV3::getMinValue() const { + return m_impl->minValue; +} +std::optional IntSettingV3::getMaxValue() const { + return m_impl->maxValue; +} + +Result<> IntSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "IntSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.has("min").into(m_impl->minValue); + root.has("max").into(m_impl->maxValue); + if (auto controls = root.has("control")) { + controls.has("arrow-step").into(m_impl->controls.arrowStepSize); + if (!controls.has("arrows").template get()) { + m_impl->controls.arrowStepSize = 0; + } + controls.has("big-arrow-step").into(m_impl->controls.bigArrowStepSize); + if (!controls.has("big-arrows").template get()) { + m_impl->controls.bigArrowStepSize = 0; + } + controls.has("slider").into(m_impl->controls.sliderEnabled); + controls.has("slider-step").into(m_impl->controls.sliderSnap); + controls.has("input").into(m_impl->controls.textInputEnabled); + controls.checkUnknownKeys(); + } + + root.checkUnknownKeys(); + return root.ok(); +} +bool IntSettingV3::load(matjson::Value const& json) { + if (json.is_number()) { + m_impl->value = json.as_int(); + return true; + } + return false; +} +bool IntSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* IntSettingV3::createNode(float width) { + // todo +} + +bool IntSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; +} +void IntSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional IntSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(IntSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + .min = this->getMinValue(), + .max = this->getMaxValue(), + .controls = { + .arrows = this->isArrowsEnabled(), + .bigArrows = this->isBigArrowsEnabled(), + .arrowStep = this->getArrowStepSize(), + .bigArrowStep = this->getBigArrowStepSize(), + .slider = this->isSliderEnabled(), + .sliderStep = this->getSliderSnap(), + .input = this->isInputEnabled(), + }, + })); +} +std::optional> IntSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class FloatSettingV3::Impl final { +public: + double value; + double defaultValue; + std::optional minValue; + std::optional maxValue; + + struct { + // 0 means not enabled + size_t arrowStepSize; + size_t bigArrowStepSize; + bool sliderEnabled; + std::optional sliderSnap; + bool textInputEnabled; + } controls; +}; + +bool FloatSettingV3::isArrowsEnabled() const { + return m_impl->controls.arrowStepSize > 0; +} +bool FloatSettingV3::isBigArrowsEnabled() const { + return m_impl->controls.bigArrowStepSize > 0; +} +size_t FloatSettingV3::getArrowStepSize() const { + return m_impl->controls.arrowStepSize; +} +size_t FloatSettingV3::getBigArrowStepSize() const { + return m_impl->controls.bigArrowStepSize; +} +bool FloatSettingV3::isSliderEnabled() const { + return m_impl->controls.sliderEnabled; +} +std::optional FloatSettingV3::getSliderSnap() const { + return m_impl->controls.sliderSnap; +} +bool FloatSettingV3::isInputEnabled() const { + return m_impl->controls.textInputEnabled; +} + +double FloatSettingV3::getValue() const { + return m_impl->value; +} +Result<> FloatSettingV3::setValue(double value) { + if (m_impl->minValue && value < *m_impl->minValue) { + return Err("Value must be under "); + } + m_impl->value = clamp( + value, + m_impl->minValue.value_or(std::numeric_limits::min()), + m_impl->maxValue.value_or(std::numeric_limits::max()) + ); +} +double FloatSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} +std::optional FloatSettingV3::getMinValue() const { + return m_impl->minValue; +} +std::optional FloatSettingV3::getMaxValue() const { + return m_impl->maxValue; +} + +Result<> FloatSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "FloatSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.has("min").into(m_impl->minValue); + root.has("max").into(m_impl->maxValue); + if (auto controls = root.has("control")) { + controls.has("arrow-step").into(m_impl->controls.arrowStepSize); + if (!controls.has("arrows").template get()) { + m_impl->controls.arrowStepSize = 0; + } + controls.has("big-arrow-step").into(m_impl->controls.bigArrowStepSize); + if (!controls.has("big-arrows").template get()) { + m_impl->controls.bigArrowStepSize = 0; + } + controls.has("slider").into(m_impl->controls.sliderEnabled); + controls.has("slider-step").into(m_impl->controls.sliderSnap); + controls.has("input").into(m_impl->controls.textInputEnabled); + controls.checkUnknownKeys(); + } + + root.checkUnknownKeys(); + return root.ok(); +} +bool FloatSettingV3::load(matjson::Value const& json) { + if (json.is_number()) { + m_impl->value = json.as_double(); + return true; + } + return false; +} +bool FloatSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* FloatSettingV3::createNode(float width) { + // todo +} + +bool FloatSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; +} +void FloatSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional FloatSettingV3::convertToLegacy() const { + return Setting(this->getKey(), this->getModID(), SettingKind(FloatSetting { + .name = this->getName(), + .description = this->getDescription(), + .defaultValue = this->getDefaultValue(), + .min = this->getMinValue(), + .max = this->getMaxValue(), + .controls = { + .arrows = this->isArrowsEnabled(), + .bigArrows = this->isBigArrowsEnabled(), + .arrowStep = this->getArrowStepSize(), + .bigArrowStep = this->getBigArrowStepSize(), + .slider = this->isSliderEnabled(), + .sliderStep = this->getSliderSnap(), + .input = this->isInputEnabled(), + }, + })); +} +std::optional> FloatSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class StringSettingV3::Impl final { +public: + std::string value; + std::string defaultValue; + std::optional match; + std::optional filter; + std::optional> oneOf; +}; + +std::string StringSettingV3::getValue() const { + return m_impl->value; +} +Result<> StringSettingV3::setValue(std::string_view value) { + m_impl->value = value; +} +std::string StringSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} + +std::optional StringSettingV3::getRegexValidator() const { + return m_impl->match; +} +std::optional StringSettingV3::getAllowedCharacters() const { + return m_impl->filter; +} +std::optional> StringSettingV3::getEnumOptions() const { + return m_impl->oneOf; +} + +Result<> StringSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "StringSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.has("match").into(m_impl->match); + root.has("filter").into(m_impl->filter); + root.has("one-of").into(m_impl->oneOf); + + root.checkUnknownKeys(); + return root.ok(); +} +bool StringSettingV3::load(matjson::Value const& json) { + if (json.is_string()) { + m_impl->value = json.as_string(); + return true; + } + return false; +} +bool StringSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* StringSettingV3::createNode(float width) { + // todo +} + +bool StringSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; +} +void StringSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional StringSettingV3::convertToLegacy() const { + auto setting = StringSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + setting.controls->filter = this->getAllowedCharacters(); + setting.controls->match = this->getRegexValidator(); + setting.controls->options = this->getEnumOptions(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> StringSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class FileSettingV3::Impl final { +public: + std::filesystem::path value; + std::filesystem::path defaultValue; + std::optional> filters; +}; + +std::filesystem::path FileSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} +std::filesystem::path FileSettingV3::getValue() const { + return m_impl->value; +} +std::optional> FileSettingV3::getFilters() const { + return m_impl->filters; +} + +Result<> FileSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "FileSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + + root.needs("default").into(m_impl->defaultValue); + + // Replace known paths like `{gd-save-dir}/` + try { + m_impl->defaultValue = fmt::format( + fmt::runtime(m_impl->defaultValue.string()), + fmt::arg("gd-save-dir", dirs::getSaveDir()), + fmt::arg("gd-game-dir", dirs::getGameDir()), + fmt::arg("mod-config-dir", dirs::getModConfigDir() / modID), + fmt::arg("mod-save-dir", dirs::getModsSaveDir() / modID), + fmt::arg("temp-dir", dirs::getTempDir()) + ); + } + catch(fmt::format_error const&) { + return Err("Invalid format string for file setting path"); + } + m_impl->value = m_impl->defaultValue; + + if (auto controls = root.has("control")) { + auto filters = std::vector(); + for (auto& item : controls.has("filters").items()) { + utils::file::FilePickOptions::Filter filter; + item.has("description").into(filter.description); + item.has("files").into(filter.files); + filters.push_back(filter); + } + if (!filters.empty()) { + m_impl->filters.emplace(filters); + } + } + + root.checkUnknownKeys(); + return root.ok(); +} +bool FileSettingV3::load(matjson::Value const& json) { + if (json.is_string()) { + m_impl->value = json.as_string(); + return true; + } + return false; +} +bool FileSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* createNode(float width) { + // todo +} + +bool FileSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; +} +void FileSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional FileSettingV3::convertToLegacy() const { + auto setting = FileSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + setting.controls.filters = this->getFilters().value_or(std::vector()); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> FileSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class Color3BSettingV3::Impl final { +public: + ccColor3B value; + ccColor3B defaultValue; +}; + +ccColor3B Color3BSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} +ccColor3B Color3BSettingV3::getValue() const { + return m_impl->value; +} + +Result<> Color3BSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "Color3BSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.checkUnknownKeys(); + return root.ok(); +} +bool Color3BSettingV3::load(matjson::Value const& json) { + if (json.template is()) { + m_impl->value = json.template as(); + return true; + } + return false; +} +bool Color3BSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* Color3BSettingV3::createNode(float width) { + // todo +} + +bool Color3BSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; + +} +void Color3BSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional Color3BSettingV3::convertToLegacy() const { + auto setting = ColorSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> Color3BSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} + +class Color4BSettingV3::Impl final { +public: + ccColor4B value; + ccColor4B defaultValue; +}; + +ccColor4B Color4BSettingV3::getDefaultValue() const { + return m_impl->defaultValue; +} +ccColor4B Color4BSettingV3::getValue() const { + return m_impl->value; +} + +Result<> Color4BSettingV3::parse(std::string const& modID, matjson::Value const& json) { + auto root = checkJson(json, "Color4BSettingV3"); + + GEODE_UNWRAP(this->parseShared(root)); + root.needs("default").into(m_impl->defaultValue); + m_impl->value = m_impl->defaultValue; + + root.checkUnknownKeys(); + return root.ok(); +} +bool Color4BSettingV3::load(matjson::Value const& json) { + if (json.template is()) { + m_impl->value = json.template as(); + return true; + } + return false; +} +bool Color4BSettingV3::save(matjson::Value& json) const { + json = m_impl->value; + return true; +} +SettingNodeV3* Color4BSettingV3::createNode(float width) { + // todo +} + +bool Color4BSettingV3::isDefaultValue() const { + return m_impl->value == m_impl->defaultValue; + +} +void Color4BSettingV3::reset() { + m_impl->value = m_impl->defaultValue; +} + +std::optional Color4BSettingV3::convertToLegacy() const { + auto setting = ColorAlphaSetting(); + setting.name = this->getName(); + setting.description = this->getDescription(); + setting.defaultValue = this->getDefaultValue(); + return Setting(this->getKey(), this->getModID(), SettingKind(setting)); +} +std::optional> Color4BSettingV3::convertToLegacyValue() const { + return std::make_unique(this->getKey(), this->getModID(), *this->convertToLegacy()); +} diff --git a/loader/src/utils/JsonValidation.cpp b/loader/src/utils/JsonValidation.cpp index 4e654041..de775c91 100644 --- a/loader/src/utils/JsonValidation.cpp +++ b/loader/src/utils/JsonValidation.cpp @@ -2,201 +2,50 @@ using namespace geode::prelude; - matjson::Value& JsonMaybeSomething::json() { return m_json; } - JsonMaybeSomething::JsonMaybeSomething( JsonChecker& checker, matjson::Value& json, std::string const& hierarchy, bool hasValue ) : m_checker(checker), m_json(json), m_hierarchy(hierarchy), m_hasValue(hasValue) {} - bool JsonMaybeSomething::isError() const { return m_checker.isError() || !m_hasValue; } - std::string JsonMaybeSomething::getError() const { return m_checker.getError(); } - JsonMaybeSomething::operator bool() const { return !isError(); } - void JsonMaybeSomething::setError(std::string const& error) { m_checker.m_result = error; } - JsonMaybeValue::JsonMaybeValue( JsonChecker& checker, matjson::Value& json, std::string const& hierarchy, bool hasValue -) : - JsonMaybeSomething(checker, json, hierarchy, hasValue) {} - +) : JsonMaybeSomething(checker, json, hierarchy, hasValue) {} JsonMaybeSomething& JsonMaybeValue::self() { return *static_cast(this); } -// template -// template -// JsonMaybeValue& JsonMaybeValue::as() { -// if (this->isError()) return *this; -// if (!jsonConvertibleTo(self().m_json.type(), T)) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// self().m_json.type_name() + "\", expected \"" + -// jsonValueTypeToString(T) + "\"" -// ); -// } -// m_inferType = false; -// return *this; -// } - - JsonMaybeValue& JsonMaybeValue::array() { this->as(); return *this; } -// template -// template -// JsonMaybeValue JsonMaybeValue::asOneOf() { -// if (this->isError()) return *this; -// bool isOneOf = (... || jsonConvertibleTo(self().m_json.type(), T)); -// if (!isOneOf) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// self().m_json.type_name() + "\", expected one of \"" + -// (jsonValueTypeToString(T), ...) + "\"" -// ); -// } -// m_inferType = false; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::is() { -// if (this->isError()) return *this; -// self().m_hasValue = jsonConvertibleTo(self().m_json.type(), T); -// m_inferType = false; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::validate(JsonValueValidator validator) { -// if (this->isError()) return *this; -// try { -// if (!validator(self().m_json.template get())) { -// this->setError(self().m_hierarchy + ": Invalid value format"); -// } -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::inferType() { -// if (this->isError() || !m_inferType) return *this; -// return this->as()>(); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::intoRaw(T& target) { -// if (this->isError()) return *this; -// target = self().m_json; -// return *this; -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::into(T& target) { -// return this->intoAs(target); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::into(std::optional& target) { -// return this->intoAs>(target); -// } - -// template -// template -// JsonMaybeValue JsonMaybeValue::intoAs(T& target) { -// this->inferType(); -// if (this->isError()) return *this; -// try { -// target = self().m_json.template get(); -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return *this; -// } - -// template -// template -// T JsonMaybeValue::get() { -// this->inferType(); -// if (this->isError()) return T(); -// try { -// return self().m_json.template get(); -// } catch(...) { -// this->setError( -// self().m_hierarchy + ": Invalid type to get \"" + -// std::string(self().m_json.type_name()) + "\"" -// ); -// } -// return T(); -// } - - JsonMaybeObject JsonMaybeValue::obj() { this->as(); return JsonMaybeObject(self().m_checker, self().m_json, self().m_hierarchy, self().m_hasValue); } -// template -// template -// struct JsonMaybeValue::Iterator { -// std::vector m_values; - -// using iterator = typename std::vector::iterator; -// using const_iterator = typename std::vector::const_iterator; - -// iterator begin() { -// return m_values.begin(); -// } -// iterator end() { -// return m_values.end(); -// } - -// const_iterator begin() const { -// return m_values.begin(); -// } -// const_iterator end() const { -// return m_values.end(); -// } -// }; - - JsonMaybeValue JsonMaybeValue::at(size_t i) { this->as(); if (this->isError()) return *this; @@ -216,7 +65,6 @@ JsonMaybeValue JsonMaybeValue::at(size_t i) { ); } - typename JsonMaybeValue::template Iterator JsonMaybeValue::iterate() { this->as(); Iterator iter; @@ -232,7 +80,6 @@ typename JsonMaybeValue::template Iterator JsonMaybeValue::itera return iter; } - typename JsonMaybeValue::template Iterator> JsonMaybeValue::items() { this->as(); Iterator> iter; @@ -248,33 +95,26 @@ typename JsonMaybeValue::template Iterator(this); } - void JsonMaybeObject::addKnownKey(std::string const& key) { m_knownKeys.insert(key); } - matjson::Value& JsonMaybeObject::json() { return self().m_json; } - JsonMaybeValue JsonMaybeObject::emptyValue() { return JsonMaybeValue(self().m_checker, self().m_json, "", false); } - JsonMaybeValue JsonMaybeObject::has(std::string const& key) { this->addKnownKey(key); if (this->isError()) return emptyValue(); @@ -284,7 +124,6 @@ JsonMaybeValue JsonMaybeObject::has(std::string const& key) { return JsonMaybeValue(self().m_checker, self().m_json[key], key, true); } - JsonMaybeValue JsonMaybeObject::needs(std::string const& key) { this->addKnownKey(key); if (this->isError()) return emptyValue(); @@ -295,7 +134,6 @@ JsonMaybeValue JsonMaybeObject::needs(std::string const& key) { return JsonMaybeValue(self().m_checker, self().m_json[key], key, true); } - // TODO: gross hack :3 (ctrl+f this comment to find the other part) extern bool s_jsonCheckerShouldCheckUnknownKeys; bool s_jsonCheckerShouldCheckUnknownKeys = true; @@ -309,20 +147,270 @@ void JsonMaybeObject::checkUnknownKeys() { } } - JsonChecker::JsonChecker(matjson::Value& json) : m_json(json), m_result(std::monostate()) {} - bool JsonChecker::isError() const { return std::holds_alternative(m_result); } - std::string JsonChecker::getError() const { return std::get(m_result); } - JsonMaybeValue JsonChecker::root(std::string const& hierarchy) { return JsonMaybeValue(*this, m_json, hierarchy, true); } + +static const char* matJsonTypeToString(matjson::Type ty) { + switch (ty) { + case matjson::Type::Null: return "null"; + case matjson::Type::Bool: return "bool"; + case matjson::Type::Number: return "number"; + case matjson::Type::String: return "string"; + case matjson::Type::Array: return "array"; + case matjson::Type::Object: return "object"; + default: return "unknown"; + } +} + +// This is used for null JsonExpectedValues (for example when doing +// `json.has("key")` where "key" doesn't exist) +static matjson::Value NULL_SCOPED_VALUE = nullptr; + +class JsonExpectedValue::Impl final { +public: + // Values shared between JsonExpectedValues related to the same JSON + struct Shared final { + matjson::Value originalJson; + std::optional error; + std::string rootScopeName; + + Shared(matjson::Value const& json, std::string_view rootScopeName) + : originalJson(json), rootScopeName(rootScopeName) {} + }; + + // this may be null if the JsonExpectedValue is a "null" value + std::shared_ptr shared; + matjson::Value& scope; + std::string scopeName; + std::string key; + std::unordered_set knownKeys; + + Impl() + : shared(nullptr), + scope(NULL_SCOPED_VALUE) + {} + + // Create a root Impl + Impl(std::shared_ptr shared) + : shared(shared), + scope(shared->originalJson), + scopeName(shared->rootScopeName) + {} + + // Create a derived Impl + Impl(Impl* from, matjson::Value& scope, std::string_view key) + : shared(from->shared), + scope(scope), + scopeName(fmt::format("{}.{}", from->scopeName, key)), + key(key) + {} +}; + +JsonExpectedValue::JsonExpectedValue() + : m_impl(std::make_unique()) +{} +JsonExpectedValue::JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key) + : m_impl(std::make_unique(from, scope, key)) +{} +JsonExpectedValue::JsonExpectedValue(matjson::Value const& json, std::string_view rootScopeName) + : m_impl(std::make_unique(std::make_shared(json, rootScopeName))) +{} +JsonExpectedValue::~JsonExpectedValue() {} + +JsonExpectedValue::JsonExpectedValue(JsonExpectedValue&&) = default; +JsonExpectedValue& JsonExpectedValue::operator=(JsonExpectedValue&&) = default; + +matjson::Value const& JsonExpectedValue::getJSONRef() const { + return m_impl->scope; +} +matjson::Value JsonExpectedValue::json() const { + return m_impl->scope; +} +std::string JsonExpectedValue::key() const { + return m_impl->key; +} + +bool JsonExpectedValue::hasError() const { + return !m_impl->shared || m_impl->shared->error.has_value(); +} +void JsonExpectedValue::setError(std::string_view error) { + m_impl->shared->error.emplace(fmt::format("[{}]: {}", m_impl->scopeName, error)); +} + +bool JsonExpectedValue::is(matjson::Type type) const { + if (this->hasError()) return false; + return m_impl->scope.type() == type; +} +bool JsonExpectedValue::isNull() const { + return this->is(matjson::Type::Null); +} +bool JsonExpectedValue::isBool() const { + return this->is(matjson::Type::Bool); +} +bool JsonExpectedValue::isNumber() const { + return this->is(matjson::Type::Number); +} +bool JsonExpectedValue::isString() const { + return this->is(matjson::Type::String); +} +bool JsonExpectedValue::isArray() const { + return this->is(matjson::Type::Array); +} +bool JsonExpectedValue::isObject() const { + return this->is(matjson::Type::Object); +} +JsonExpectedValue& JsonExpectedValue::assertIs(matjson::Type type) { + if (this->hasError()) return *this; + if (m_impl->scope.type() != type) { + this->setError( + "invalid type {}, expected {}", + matJsonTypeToString(m_impl->scope.type()), + matJsonTypeToString(type) + ); + } + return *this; +} +JsonExpectedValue& JsonExpectedValue::assertIsNull() { + return this->assertIs(matjson::Type::Null); +} +JsonExpectedValue& JsonExpectedValue::assertIsBool() { + return this->assertIs(matjson::Type::Bool); +} +JsonExpectedValue& JsonExpectedValue::assertIsNumber() { + return this->assertIs(matjson::Type::Number); +} +JsonExpectedValue& JsonExpectedValue::assertIsString() { + return this->assertIs(matjson::Type::String); +} +JsonExpectedValue& JsonExpectedValue::assertIsArray() { + return this->assertIs(matjson::Type::Array); +} +JsonExpectedValue& JsonExpectedValue::assertIsObject() { + return this->assertIs(matjson::Type::Object); +} +JsonExpectedValue& JsonExpectedValue::assertIs(std::initializer_list types) { + if (this->hasError()) return *this; + if (!std::any_of(types.begin(), types.end(), [this](matjson::Type t) { return t == m_impl->scope.type(); })) { + this->setError( + "invalid type {}, expected either {}", + matJsonTypeToString(m_impl->scope.type()), + ranges::join(ranges::map>(types, [](matjson::Type t) { + return matJsonTypeToString(t); + }), " or ") + ); + } + return *this; +} + +JsonExpectedValue JsonExpectedValue::has(std::string_view key) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Object)) { + return JsonExpectedValue(); + } + if (!m_impl->scope.contains(key)) { + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key); +} +JsonExpectedValue JsonExpectedValue::needs(std::string_view key) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Object)) { + return JsonExpectedValue(); + } + if (!m_impl->scope.contains(key)) { + this->setError("missing required key {}", key); + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key); +} +std::vector> JsonExpectedValue::properties() { + if (this->hasError()) { + return std::vector>(); + } + if (!this->assertIs(matjson::Type::Object)) { + return std::vector>(); + } + std::vector> res; + for (auto& [k, v] : m_impl->scope.as_object()) { + res.push_back(std::make_pair(k, JsonExpectedValue(m_impl.get(), v, k))); + } + return res; +} +void JsonExpectedValue::checkUnknownKeys() { + for (auto& [key, _] : this->properties()) { + if (!m_impl->knownKeys.count(key)) { + log::warn("{} contains unknown key \"{}\"", m_impl->scopeName, key); + } + } +} + +size_t JsonExpectedValue::length() { + if (this->hasError()) { + return 0; + } + if (!this->assertIs(matjson::Type::Array)) { + return 0; + } + return m_impl->scope.as_array().size(); +} +JsonExpectedValue JsonExpectedValue::at(size_t index) { + if (this->hasError()) { + return JsonExpectedValue(); + } + if (!this->assertIs(matjson::Type::Array)) { + return JsonExpectedValue(); + } + auto& arr = m_impl->scope.as_array(); + if (arr.size() <= index) { + this->setError( + "array expected to have at least size {}, but its size was only {}", + index + 1, arr.size() + ); + return JsonExpectedValue(); + } + return JsonExpectedValue(m_impl.get(), arr.at(index), std::to_string(index)); +} +std::vector JsonExpectedValue::items() { + if (this->hasError()) { + return std::vector(); + } + if (!this->assertIs(matjson::Type::Object)) { + return std::vector(); + } + std::vector res; + size_t i = 0; + for (auto& v : m_impl->scope.as_array()) { + res.push_back(JsonExpectedValue(m_impl.get(), v, std::to_string(i++))); + } + return res; +} + +JsonExpectedValue::operator bool() const { + return !this->hasError(); +} + +Result<> JsonExpectedValue::ok() { + if (m_impl->shared && m_impl->shared->error) { + return Err(*m_impl->shared->error); + } + return Ok(); +} + +JsonExpectedValue geode::checkJson(matjson::Value const& json, std::string_view rootScopeName) { + return JsonExpectedValue(json, rootScopeName); +}