Merge pull request #1073 from geode-sdk/settings

Settings Rework
This commit is contained in:
HJfod 2024-09-10 20:27:20 +03:00 committed by GitHub
commit 67814ece83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4612 additions and 548 deletions

View file

@ -1,5 +1,18 @@
# Geode Changelog
## v3.6.0
* Major rework of the entire settings system with lots of new features; see the [docs page](https://docs.geode-sdk.org/mods/settings) for more
* Rework JSON validation; now uses the `JsonExpectedValue` class with the `checkJson` helper (89d1a51)
* Add `Task::cancelled` for creating immediately cancelled Tasks (1a82d12)
* Add function type utilities in `utils/function.hpp` (659c168)
* Add `typeinfo_pointer_cast` for casting `std::shared_ptr`s (28cc6fd)
* Add `GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH` (1032d9a)
* Add `PlatformID::getCovered` (d5718be)
* Rename `toByteArray` to `toBytes` (6eb0797)
* Improve `AxisLayout::getSizeHint` (85e7b5e)
* Fix issues with file dialogs on Windows (62b6241, 971e3fb)
* Mod incompatibilities may now be platform-specific (9f1c70a)
## v3.5.0
* Move CCLighting to cocos headers (#1036)
* Add new `gd::string` constructor (bae22b4)

View file

@ -1 +1 @@
3.5.0
3.6.0

View file

@ -10,6 +10,7 @@
#include "Hook.hpp"
#include "ModMetadata.hpp"
#include "Setting.hpp"
#include "SettingV3.hpp"
#include "Types.hpp"
#include "Loader.hpp"
@ -23,6 +24,8 @@
#include <vector>
namespace geode {
class SettingV3;
template <class T>
struct HandleToSaved : public T {
Mod* m_mod;
@ -170,12 +173,32 @@ namespace geode {
*/
std::filesystem::path getConfigDir(bool create = true) const;
/**
* Returns true if this mod has any settings
*/
bool hasSettings() const;
/**
* Get a list of all this mod's setting keys (in the order they were
* declared in `mod.json`)
*/
std::vector<std::string> getSettingKeys() const;
bool hasSetting(std::string_view const key) const;
// todo in v4: remove these
[[deprecated("Use Mod::getSettingV3")]]
std::optional<Setting> getSettingDefinition(std::string_view const key) const;
[[deprecated("Use Mod::getSettingV3")]]
SettingValue* getSetting(std::string_view const key) const;
// todo in v4: possibly rename this to getSetting?
/**
* Get the definition of a setting, or null if the setting was not found,
* or if it's a custom setting that has not yet been registered using
* `Mod::registerCustomSettingType`
* @param key The key of the setting as defined in `mod.json`
*/
std::shared_ptr<SettingV3> getSettingV3(std::string_view const key) const;
/**
* Register a custom setting's value class. See Mod::addCustomSetting
* for a convenience wrapper that creates the value in-place to avoid
@ -186,6 +209,7 @@ namespace geode {
* @param value The SettingValue class that shall handle this setting
* @see addCustomSetting
*/
[[deprecated("Use Mod::registerCustomSettingType")]]
void registerCustomSetting(std::string_view const key, std::unique_ptr<SettingValue> value);
/**
* Register a custom setting's value class. The new SettingValue class
@ -200,10 +224,21 @@ namespace geode {
* }
*/
template <class T, class V>
[[deprecated("Use Mod::registerCustomSettingType")]]
void addCustomSetting(std::string_view const key, V const& value) {
this->registerCustomSetting(key, std::make_unique<T>(std::string(key), this->getID(), value));
}
/**
* Register a custom setting type. See
* [the setting docs](https://docs.geode-sdk.org/mods/settings) for more
* @param type The type of the setting. This should **not** include the
* `custom:` prefix!
* @param generator A pointer to a function that, when called, returns a
* newly-created instance of the setting type
*/
Result<> registerCustomSettingType(std::string_view type, SettingGenerator generator);
/**
* Returns a prefixed launch argument name. See `Mod::getLaunchArgument`
* for details about mod-specific launch arguments.
@ -246,19 +281,27 @@ namespace geode {
matjson::Value& getSaveContainer();
matjson::Value& getSavedSettingsData();
/**
* Get the value of a [setting](https://docs.geode-sdk.org/mods/settings).
* To use this for custom settings, first specialize the
* `SettingTypeForValueType` class, and then make sure your custom
* setting type has a `getValue` function which returns the value
*/
template <class T>
T getSettingValue(std::string_view const key) const {
if (auto sett = this->getSetting(key)) {
return SettingValueSetter<T>::get(sett);
using S = typename SettingTypeForValueType<T>::SettingType;
if (auto sett = cast::typeinfo_pointer_cast<S>(this->getSettingV3(key))) {
return sett->getValue();
}
return T();
}
template <class T>
T setSettingValue(std::string_view const key, T const& value) {
if (auto sett = this->getSetting(key)) {
auto old = this->getSettingValue<T>(key);
SettingValueSetter<T>::set(sett, value);
using S = typename SettingTypeForValueType<T>::SettingType;
if (auto sett = cast::typeinfo_pointer_cast<S>(this->getSettingV3(key))) {
auto old = sett->getValue();
sett->setValue(value);
return old;
}
return T();

View file

@ -184,7 +184,12 @@ namespace geode {
* Mod settings
* @note Not a map because insertion order must be preserved
*/
[[nodiscard]] std::vector<std::pair<std::string, Setting>> getSettings() const;
[[nodiscard, deprecated("Use getSettingsV3")]] std::vector<std::pair<std::string, Setting>> getSettings() const;
/**
* Mod settings
* @note Not a map because insertion order must be preserved
*/
[[nodiscard]] std::vector<std::pair<std::string, matjson::Value>> getSettingsV3() const;
/**
* Get the tags for this mod
*/
@ -232,7 +237,9 @@ namespace geode {
void setDependencies(std::vector<Dependency> const& value);
void setIncompatibilities(std::vector<Incompatibility> const& value);
void setSpritesheets(std::vector<std::string> const& value);
[[deprecated("This function does NOTHING")]]
void setSettings(std::vector<std::pair<std::string, Setting>> const& value);
void setSettings(std::vector<std::pair<std::string, matjson::Value>> const& value);
void setTags(std::unordered_set<std::string> const& value);
void setNeedsEarlyLoad(bool const& value);
void setIsAPI(bool const& value);

View file

@ -0,0 +1,56 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include "SettingV3.hpp"
namespace geode {
class GEODE_DLL ModSettingsManager final {
private:
class Impl;
std::unique_ptr<Impl> m_impl;
friend class ::geode::SettingV3;
void markRestartRequired();
public:
static ModSettingsManager* from(Mod* mod);
ModSettingsManager(ModMetadata const& metadata);
~ModSettingsManager();
ModSettingsManager(ModSettingsManager&&);
ModSettingsManager(ModSettingsManager const&) = delete;
/**
* Load setting values from savedata.
* The format of the savedata should be an object with the keys being
* setting IDs and then the values the values of the saved settings
* @returns Ok if no horrible errors happened. Note that a setting value
* missing is not considered a horrible error, but will instead just log a
* warning into the console!
*/
Result<> load(matjson::Value const& json);
/**
* Save setting values to savedata.
* The format of the savedata will be an object with the keys being
* setting IDs and then the values the values of the saved settings
* @note If saving a setting fails, it will log a warning to the console
*/
void save(matjson::Value& json);
Result<> registerCustomSettingType(std::string_view type, SettingGenerator generator);
// todo in v4: remove this
Result<> registerLegacyCustomSetting(std::string_view key, std::unique_ptr<SettingValue>&& ptr);
std::shared_ptr<SettingV3> get(std::string_view key);
std::shared_ptr<SettingValue> getLegacy(std::string_view key);
std::optional<Setting> getLegacyDefinition(std::string_view key);
/**
* Returns true if any setting with the `"restart-required"` attribute
* has been altered
*/
bool restartRequired() const;
};
}

View file

@ -286,9 +286,7 @@ namespace geode {
return Setting(m_key, m_modID, m_definition);
}
ValueType getValue() const {
return m_value;
}
GEODE_DLL ValueType getValue() const;
GEODE_DLL void setValue(ValueType const& value);
GEODE_DLL Result<> validate(ValueType const& value) const;
};
@ -301,8 +299,10 @@ namespace geode {
using ColorSettingValue = GeodeSettingValue<ColorSetting>;
using ColorAlphaSettingValue = GeodeSettingValue<ColorAlphaSetting>;
// todo: remove in v3
template<class T>
struct GEODE_DLL SettingValueSetter {
struct [[deprecated("Use SettingTypeForValueType from SettingV3 instead")]] GEODE_DLL SettingValueSetter {
static T get(SettingValue* setting);
static void set(SettingValue* setting, T const& value);
};

View file

@ -4,18 +4,18 @@
#include "Loader.hpp"
#include "Setting.hpp"
#include "Mod.hpp"
#include "SettingV3.hpp"
#include <optional>
namespace geode {
struct GEODE_DLL SettingChangedEvent : public Event {
struct GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedEvent : public Event {
Mod* mod;
SettingValue* value;
SettingChangedEvent(Mod* mod, SettingValue* value);
};
class GEODE_DLL SettingChangedFilter : public EventFilter<SettingChangedEvent> {
class GEODE_DLL [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] SettingChangedFilter : public EventFilter<SettingChangedEvent> {
protected:
std::string m_modID;
std::optional<std::string> m_targetKey;
@ -40,7 +40,7 @@ namespace geode {
* Listen for built-in setting changes
*/
template<class T>
class GeodeSettingChangedFilter : public SettingChangedFilter {
class [[deprecated("Use SettingChangedEventV3 from SettingV3.hpp instead")]] GeodeSettingChangedFilter : public SettingChangedFilter {
public:
using Callback = void(T);
@ -60,21 +60,4 @@ namespace geode {
) : SettingChangedFilter(modID, settingID) {}
GeodeSettingChangedFilter(GeodeSettingChangedFilter const&) = default;
};
template <class T>
std::monostate listenForSettingChanges(
std::string const& settingKey, void (*callback)(T)
) {
(void)new EventListener(
callback, GeodeSettingChangedFilter<T>(getMod()->getID(), settingKey)
);
return std::monostate();
}
static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) {
(void)new EventListener(
callback, SettingChangedFilter(getMod()->getID(), std::nullopt)
);
return std::monostate();
}
}

View file

@ -0,0 +1,822 @@
#pragma once
#include "../DefaultInclude.hpp"
#include <optional>
#include <cocos2d.h>
// todo: remove this header in 4.0.0
#include "Setting.hpp"
#include "../utils/cocos.hpp"
// this unfortunately has to be included because of C++ templates
#include "../utils/JsonValidation.hpp"
#include "../utils/function.hpp"
// todo in v4: this can be removed as well as the friend decl in LegacyCustomSettingV3
class LegacyCustomSettingToV3Node;
class ModSettingsPopup;
namespace geode {
class ModSettingsManager;
class SettingNodeV3;
// todo in v4: remove this
class SettingValue;
class GEODE_DLL SettingV3 : public std::enable_shared_from_this<SettingV3> {
private:
class GeodeImpl;
std::shared_ptr<GeodeImpl> m_impl;
protected:
/**
* Only call this function if you aren't going to call
* `parseBaseProperties`, which will call it for you!
* If you don't want to call `parseBaseProperties`, at the very least
* you **must** call this!
* Select which properties you want to parse using the `parseX`
* functions
* @param key The setting's key as defined in `mod.json`
* @param modID The ID of the mod this settings is being parsed for
* @param json The current JSON checking instance being used. This
* should be the JSON object that defines the setting. If you aren't
* using Geode's JSON checking utilities, you can use the other
* overload of `init`
*/
void init(std::string const& key, std::string const& modID, JsonExpectedValue& json);
/**
* Only call this function if you aren't going to call
* `parseBaseProperties`, which will call it for you!
* If you don't want to call `parseBaseProperties`, at the very least
* you **must** call this!
* Select which properties you want to parse using the `parseX`
* functions
* @param key The setting's key as defined in `mod.json`
* @param modID The ID of the mod this settings is being parsed for
* @note If you are using Geode's JSON checking utilities
* (`checkJson` / `JsonExpectedValue`), you should be using the other
* overload that takes a `JsonExpectedValue&`!
*/
void init(std::string const& key, std::string const& modID);
/**
* Parses the `"name"` and `"description"` keys from the setting's
* definition in `mod.json` (if they exist), so their values can be
* accessed via `getName` and `getDescription`.
* @param json The current JSON checking instance being used. This
* should be the JSON object that defines the setting
* @warning In most cases, you should be using `parseBaseProperties`
* instead to do all of this in one go!
* If you do need the fine-grained control however, make sure to call
* `init` before calling these parsing functions!
*/
void parseNameAndDescription(JsonExpectedValue& json);
/**
* Parses the `"enable-if"` and `"enable-if-description"` keys from
* the setting's definition in `mod.json` (if they exist), so
* `shouldEnable` and `getEnableIfDescription` work.
* @param json The current JSON checking instance being used. This
* should be the JSON object that defines the setting
* @warning In most cases, you should be using `parseBaseProperties`
* instead to do all of this in one go!
* If you do need the fine-grained control however, make sure to call
* `init` before calling these parsing functions!
*/
void parseEnableIf(JsonExpectedValue& json);
/**
* Parses the `"requires-restart"` key from the setting's definition in
* `mod.json` (if they exist), so `requiresRestart` works.
* @param json The current JSON checking instance being used. This
* should be the JSON object that defines the setting
* @warning In most cases, you should be using `parseBaseProperties`
* instead to do all of this in one go!
* If you do need the fine-grained control however, make sure to call
* `init` before calling these parsing functions!
*/
void parseValueProperties(JsonExpectedValue& json);
/**
* Parse all of the base properties such as `"name"` and `"description"`
* for this setting
* @param key The setting's key as defined in `mod.json`
* @param modID The ID of the mod this settings is being parsed for
* @param json The current JSON checking instance being used. If you
* aren't using Geode's JSON checking utilities, use the other overload
* of this function
* @note If you don't want to parse some of the base properties, such as
* `"requires-restart"` (because you're doing a cosmetic setting), then
* you can call `init` instead and then the specific `parseX` functions
*/
void parseBaseProperties(std::string const& key, std::string const& modID, JsonExpectedValue& json);
/**
* Parse all of the base properties such as `"name"` and `"description"`
* for this setting
* @param key The setting's key as defined in `mod.json`
* @param modID The ID of the mod this settings is being parsed for
* @param json The JSON value. If you are using Geode's JSON checking
* utilities (`checkJson` / `JsonExpectedValue`), you should use the
* other overload directly!
* @note If you don't want to parse some of the base properties, such as
* `"requires-restart"` (because you're doing a cosmetic setting), then
* you can call `init` instead and then the specific `parseX` functions
*/
Result<> parseBaseProperties(std::string const& key, std::string const& modID, matjson::Value const& json);
/**
* Mark that the value of this setting has changed. This should be
* ALWAYS called on every setter that can modify the setting's state!
*/
void markChanged();
friend class ::geode::SettingValue;
public:
SettingV3();
virtual ~SettingV3();
/**
* Get the key of this setting
*/
std::string getKey() const;
/**
* Get the mod ID this setting is for
*/
std::string getModID() const;
/**
* Get the mod this setting is for. Note that this may return null
* while the mod is still being initialized
*/
Mod* getMod() const;
/**
* Get the name of this setting
*/
std::optional<std::string> getName() const;
/**
* Get the name of this setting, or its key if it has no name
*/
std::string getDisplayName() const;
/**
* Get the description of this setting
*/
std::optional<std::string> getDescription() const;
/**
* Get the "enable-if" scheme for this setting
*/
std::optional<std::string> getEnableIf() const;
/**
* Check if this setting should be enabled based on the "enable-if" scheme
*/
bool shouldEnable() const;
std::optional<std::string> getEnableIfDescription() const;
/**
* Whether this setting requires a restart on change
*/
bool requiresRestart() const;
/**
* Get the platforms this setting is available on
*/
std::vector<PlatformID> getPlatforms() const;
virtual bool load(matjson::Value const& json) = 0;
virtual bool save(matjson::Value& json) const = 0;
virtual SettingNodeV3* createNode(float width) = 0;
virtual bool isDefaultValue() const = 0;
/**
* Reset this setting's value back to its original value
*/
virtual void reset() = 0;
[[deprecated(
"This function will be removed alongside legacy settings in 4.0.0! "
"You should NOT be implementing it for your own custom setting classes"
)]]
virtual std::optional<Setting> convertToLegacy() const;
[[deprecated(
"This function will be removed alongside legacy settings in 4.0.0! "
"You should NOT be implementing it for your own custom setting classes"
)]]
virtual std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const;
};
using SettingGenerator = std::function<Result<std::shared_ptr<SettingV3>>(
std::string const& key,
std::string const& modID,
matjson::Value const& json
)>;
/**
* A helper class for creating a basic setting with a simple value.
* Override the virtual function `isValid` to
* @tparam T The type of the setting's value. This type must be JSON-
* serializable and deserializable!
* @tparam V The type used for the `setValue` function, if it differs from T
*/
template <class T, class V = T>
class SettingBaseValueV3 : public SettingV3 {
private:
class Impl final {
private:
T defaultValue;
T value;
friend class SettingBaseValueV3;
};
std::shared_ptr<Impl> m_impl;
protected:
/**
* Parses the `"default"` key from the setting's definition in
* `mod.json`. The key may also be defined per-platform, i.e.
* `"default": { "win": ..., "android": ... }`
* @param json The current JSON checking instance being used. This
* should be the JSON object that defines the setting
* @warning In most cases, you should be using `parseBaseProperties`
* instead to do all of this in one go!
* If you do need the fine-grained control however, make sure to call
* `init` before calling these parsing functions!
*/
void parseDefaultValue(JsonExpectedValue& json) {
auto root = json.needs("default");
// Check if this is a platform-specific default value
if (root.isObject() && root.has(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH)) {
root.needs(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH).into(m_impl->defaultValue);
}
else {
root.into(m_impl->defaultValue);
}
m_impl->value = m_impl->defaultValue;
}
/**
* Parse shared value, including the default value for this setting
* @param key The key of the setting
* @param modID The ID of the mod this setting is being parsed for
* @param json The current JSON checking instance being used. If you
* aren't using Geode's JSON checking utilities, use the other overload
* of this function
*/
void parseBaseProperties(std::string const& key, std::string const& modID, JsonExpectedValue& json) {
SettingV3::parseBaseProperties(key, modID, json);
this->parseDefaultValue(json);
}
/**
* Parse shared value, including the default value for this setting
* @param key The key of the setting
* @param modID The ID of the mod this setting is being parsed for
* @param json The JSON value. If you are using Geode's JSON checking
* utilities (`checkJson` / `JsonExpectedValue`), you should use the
* other overload directly!
*/
Result<> parseBaseProperties(std::string const& key, std::string const& modID, matjson::Value const& json) {
auto root = checkJson(json, "SettingBaseValueV3");
this->parseBaseProperties(key, modID, root);
return root.ok();
}
/**
* Set the default value. This does not check that the value is
* actually valid!
*/
void setDefaultValue(V value) {
m_impl->defaultValue = value;
}
public:
SettingBaseValueV3() : m_impl(std::make_shared<Impl>()) {}
using ValueType = T;
using ValueAssignType = V;
/**
* Get the default value for this setting
*/
T getDefaultValue() const {
return m_impl->defaultValue;
}
/**
* Get the current value of this setting
*/
T getValue() const {
return m_impl->value;
}
/**
* Set the value of this setting. This will broadcast a new
* SettingChangedEventV3, letting any listeners now the value has changed
* @param value The new value for the setting. If the value is not a
* valid value for this setting (as determined by `isValue`), then the
* setting's value is reset to the default value
*/
void setValue(V value) {
m_impl->value = this->isValid(value) ? value : m_impl->defaultValue;
this->markChanged();
}
/**
* Check if a given value is valid for this setting. If not, an error
* describing why the value isn't valid is returned
*/
virtual Result<> isValid(V value) const {
return Ok();
}
bool isDefaultValue() const override {
return m_impl->value == m_impl->defaultValue;
}
void reset() override {
this->setValue(m_impl->defaultValue);
}
bool load(matjson::Value const& json) override {
if (json.template is<T>()) {
m_impl->value = json.template as<T>();
return true;
}
return false;
}
bool save(matjson::Value& json) const override {
json = m_impl->value;
return true;
}
};
class GEODE_DLL TitleSettingV3 final : public SettingV3 {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
TitleSettingV3(PrivateMarker);
static Result<std::shared_ptr<TitleSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
bool load(matjson::Value const& json) override;
bool save(matjson::Value& json) const override;
SettingNodeV3* createNode(float width) override;
bool isDefaultValue() const override;
void reset() override;
};
// todo in v4: remove this class completely
class GEODE_DLL LegacyCustomSettingV3 final : public SettingV3 {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
friend class ::geode::ModSettingsManager;
friend class ::LegacyCustomSettingToV3Node;
private:
class PrivateMarker {};
friend class SettingV3;
public:
LegacyCustomSettingV3(PrivateMarker);
static Result<std::shared_ptr<LegacyCustomSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
std::shared_ptr<SettingValue> getValue() const;
void setValue(std::shared_ptr<SettingValue> value);
bool load(matjson::Value const& json) override;
bool save(matjson::Value& json) const override;
SettingNodeV3* createNode(float width) override;
bool isDefaultValue() const override;
void reset() override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL BoolSettingV3 final : public SettingBaseValueV3<bool> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
BoolSettingV3(PrivateMarker);
static Result<std::shared_ptr<BoolSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(bool value) const override;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL IntSettingV3 final : public SettingBaseValueV3<int64_t> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
IntSettingV3(PrivateMarker);
static Result<std::shared_ptr<IntSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(int64_t value) const override;
std::optional<int64_t> getMinValue() const;
std::optional<int64_t> getMaxValue() const;
bool isArrowsEnabled() const;
bool isBigArrowsEnabled() const;
size_t getArrowStepSize() const;
size_t getBigArrowStepSize() const;
bool isSliderEnabled() const;
int64_t getSliderSnap() const;
bool isInputEnabled() const;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL FloatSettingV3 final : public SettingBaseValueV3<double> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
FloatSettingV3(PrivateMarker);
static Result<std::shared_ptr<FloatSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(double value) const override;
std::optional<double> getMinValue() const;
std::optional<double> getMaxValue() const;
bool isArrowsEnabled() const;
bool isBigArrowsEnabled() const;
double getArrowStepSize() const;
double getBigArrowStepSize() const;
bool isSliderEnabled() const;
double getSliderSnap() const;
bool isInputEnabled() const;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL StringSettingV3 final : public SettingBaseValueV3<std::string, std::string_view> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
StringSettingV3(PrivateMarker);
static Result<std::shared_ptr<StringSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(std::string_view value) const override;
std::optional<std::string> getRegexValidator() const;
std::optional<std::string> getAllowedCharacters() const;
std::optional<std::vector<std::string>> getEnumOptions() const;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL FileSettingV3 final : public SettingBaseValueV3<std::filesystem::path, std::filesystem::path const&> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
FileSettingV3(PrivateMarker);
static Result<std::shared_ptr<FileSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(std::filesystem::path const& value) const override;
bool isFolder() const;
bool useSaveDialog() const;
std::optional<std::vector<utils::file::FilePickOptions::Filter>> getFilters() const;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL Color3BSettingV3 final : public SettingBaseValueV3<cocos2d::ccColor3B> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
Color3BSettingV3(PrivateMarker);
static Result<std::shared_ptr<Color3BSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(cocos2d::ccColor3B value) const override;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL Color4BSettingV3 final : public SettingBaseValueV3<cocos2d::ccColor4B> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
private:
class PrivateMarker {};
friend class SettingV3;
public:
Color4BSettingV3(PrivateMarker);
static Result<std::shared_ptr<Color4BSettingV3>> parse(std::string const& key, std::string const& modID, matjson::Value const& json);
Result<> isValid(cocos2d::ccColor4B value) const override;
SettingNodeV3* createNode(float width) override;
std::optional<Setting> convertToLegacy() const override;
std::optional<std::shared_ptr<SettingValue>> convertToLegacyValue() const override;
};
class GEODE_DLL SettingNodeV3 : public cocos2d::CCNode {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
friend class ::ModSettingsPopup;
protected:
bool init(std::shared_ptr<SettingV3> setting, float width);
/**
* Update the state of this setting node, bringing all inputs
* up-to-date with the current value. Derivatives of `SettingNodeV3`
* should set update the state (such as visibility, value, etc.) of all
* its controls, except for the one that's passed as the `invoker`
* argument. Derivatives should remember to **always call the base
* class's `updateState` function**, as it updates the built-in title
* label as well as the description and reset buttons!
* @param invoker The button or other interactive element that caused
* this state update. If that element is for example a text input, it
* may wish to ignore the state update, as it itself is the source of
* truth for the node's value at that moment. May be nullptr to mark
* that no specific node requested this state update
*/
virtual void updateState(cocos2d::CCNode* invoker);
/**
* Mark this setting as changed. This updates the UI for committing
* the value, as well as posts a `SettingNodeValueChangeEventV3`
* @param invoker The node to be passed onto `updateState`
*/
void markChanged(cocos2d::CCNode* invoker);
/**
* When the setting value is committed (aka can't be undone), this
* function will be called. This should take care of actually saving
* the value in some sort of global manager
*/
virtual void onCommit() = 0;
virtual void onResetToDefault() = 0;
void onDescription(CCObject*);
void onReset(CCObject*);
public:
void commit();
void resetToDefault();
virtual bool hasUncommittedChanges() const = 0;
virtual bool hasNonDefaultValue() const = 0;
// Can be overridden by the setting itself
// Can / should be used to do alternating BG
void setDefaultBGColor(cocos2d::ccColor4B color);
cocos2d::CCLabelBMFont* getNameLabel() const;
cocos2d::CCLabelBMFont* getStatusLabel() const;
cocos2d::CCMenu* getNameMenu() const;
cocos2d::CCMenu* getButtonMenu() const;
cocos2d::CCLayerColor* getBG() const;
void setContentSize(cocos2d::CCSize const& size) override;
std::shared_ptr<SettingV3> getSetting() const;
};
/**
* Helper class for creating `SettingNode`s for simple settings that
* implement `SettingBaseValueV3`
*/
template <class S>
class SettingValueNodeV3 : public SettingNodeV3 {
protected:
private:
class Impl final {
private:
typename S::ValueType currentValue;
friend class SettingValueNodeV3;
};
std::shared_ptr<Impl> m_impl;
protected:
bool init(std::shared_ptr<S> setting, float width) {
if (!SettingNodeV3::init(setting, width))
return false;
m_impl = std::make_shared<Impl>();
m_impl->currentValue = setting->getValue();
return true;
}
void updateState(cocos2d::CCNode* invoker) {
SettingNodeV3::updateState(invoker);
auto validate = this->getSetting()->isValid(m_impl->currentValue);
if (!validate) {
this->getStatusLabel()->setVisible(true);
this->getStatusLabel()->setString(validate.unwrapErr().c_str());
this->getStatusLabel()->setColor(cocos2d::ccc3(235, 35, 52));
}
}
void onCommit() override {
this->getSetting()->setValue(m_impl->currentValue);
// The value may be different, if the current value was an invalid
// value for the setting
this->setValue(this->getSetting()->getValue(), nullptr);
}
bool hasUncommittedChanges() const override {
return m_impl->currentValue != this->getSetting()->getValue();
}
bool hasNonDefaultValue() const override {
return m_impl->currentValue != this->getSetting()->getDefaultValue();
}
void onResetToDefault() override {
this->setValue(this->getSetting()->getDefaultValue(), nullptr);
}
public:
/**
* Get the **uncommitted** value for this node
*/
typename S::ValueType getValue() const {
return m_impl->currentValue;
}
/**
* Set the **uncommitted** value for this node
* @param value The value to set
* @param invoker The node that invoked this value change; see the docs
* for `SettingNodeV3::updateState` to know more
*/
void setValue(typename S::ValueAssignType value, cocos2d::CCNode* invoker) {
m_impl->currentValue = value;
this->markChanged(invoker);
}
std::shared_ptr<S> getSetting() const {
return std::static_pointer_cast<S>(SettingNodeV3::getSetting());
}
};
class GEODE_DLL SettingChangedEventV3 final : public Event {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
SettingChangedEventV3(std::shared_ptr<SettingV3> setting);
std::shared_ptr<SettingV3> getSetting() const;
};
class GEODE_DLL SettingChangedFilterV3 final : public EventFilter<SettingChangedEventV3> {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
using Callback = void(std::shared_ptr<SettingV3>);
ListenerResult handle(utils::MiniFunction<Callback> fn, SettingChangedEventV3* event);
/**
* Listen to changes on a setting, or all settings
* @param modID Mod whose settings to listen to
* @param settingKey Setting to listen to, or all settings if nullopt
*/
SettingChangedFilterV3(
std::string const& modID,
std::optional<std::string> const& settingKey
);
SettingChangedFilterV3(Mod* mod, std::optional<std::string> const& settingKey);
SettingChangedFilterV3(SettingChangedFilterV3 const&);
};
class GEODE_DLL SettingNodeSizeChangeEventV3 : public Event {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
SettingNodeSizeChangeEventV3(SettingNodeV3* node);
virtual ~SettingNodeSizeChangeEventV3();
SettingNodeV3* getNode() const;
};
class GEODE_DLL SettingNodeValueChangeEventV3 : public Event {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
SettingNodeValueChangeEventV3(SettingNodeV3* node, bool commit);
virtual ~SettingNodeValueChangeEventV3();
SettingNodeV3* getNode() const;
bool isCommit() const;
};
template <class T>
struct SettingTypeForValueType {
static_assert(
!std::is_same_v<T, T>,
"specialize the SettingTypeForValueType class to use Mod::getSettingValue for custom settings"
);
};
template <>
struct SettingTypeForValueType<bool> {
using SettingType = BoolSettingV3;
};
template <>
struct SettingTypeForValueType<int64_t> {
using SettingType = IntSettingV3;
};
template <>
struct SettingTypeForValueType<double> {
using SettingType = FloatSettingV3;
};
template <>
struct SettingTypeForValueType<std::string> {
using SettingType = StringSettingV3;
};
template <>
struct SettingTypeForValueType<std::filesystem::path> {
using SettingType = FileSettingV3;
};
template <>
struct SettingTypeForValueType<cocos2d::ccColor3B> {
using SettingType = Color3BSettingV3;
};
template <>
struct SettingTypeForValueType<cocos2d::ccColor4B> {
using SettingType = Color4BSettingV3;
};
template <class T>
EventListener<SettingChangedFilterV3>* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) {
using Ty = typename SettingTypeForValueType<T>::SettingType;
return new EventListener(
[callback = std::move(callback)](std::shared_ptr<SettingV3> setting) {
if (auto ty = geode::cast::typeinfo_pointer_cast<Ty>(setting)) {
callback(ty->getValue());
}
},
SettingChangedFilterV3(mod, std::string(settingKey))
);
}
EventListener<SettingChangedFilterV3>* listenForSettingChanges(std::string_view settingKey, auto&& callback, Mod* mod = getMod()) {
using T = std::remove_cvref_t<utils::function::Arg<0, decltype(callback)>>;
return listenForSettingChanges<T>(settingKey, std::move(callback), mod);
}
GEODE_DLL EventListener<SettingChangedFilterV3>* listenForAllSettingChanges(
std::function<void(std::shared_ptr<SettingV3>)> const& callback,
Mod* mod = getMod()
);
}

View file

@ -16,6 +16,7 @@
#define GEODE_PLATFORM_NAME "Windows"
#define GEODE_PLATFORM_EXTENSION ".dll"
#define GEODE_PLATFORM_SHORT_IDENTIFIER "win"
#define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "win"
#define CC_TARGET_OS_WIN32
#if defined(WIN64) || defined(_WIN64) || defined(__WIN64) && !defined(__CYGWIN__)
@ -47,6 +48,7 @@
#define GEODE_PLATFORM_NAME "iOS"
#define GEODE_PLATFORM_EXTENSION ".ios.dylib"
#define GEODE_PLATFORM_SHORT_IDENTIFIER "ios"
#define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "ios"
#define CC_TARGET_OS_IPHONE
#else
#define GEODE_IOS(...)
@ -54,6 +56,7 @@
#define GEODE_IS_MACOS
#define GEODE_IS_DESKTOP
#define GEODE_PLATFORM_EXTENSION ".dylib"
#define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "mac"
#define CC_TARGET_OS_MAC
#if TARGET_CPU_ARM64
@ -85,6 +88,7 @@
#define GEODE_IS_MOBILE
#define GEODE_CALL
#define CC_TARGET_OS_ANDROID
#define GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH "android"
#if defined(__arm__)
#define GEODE_ANDROID32(...) __VA_ARGS__

View file

@ -3,6 +3,7 @@
#include "cplatform.h"
#include <string>
#include <functional>
#include <memory>
#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
#define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__)
@ -113,15 +114,16 @@
namespace geode {
class PlatformID {
public:
// todo in v4: make these flags and add archless Mac and Android as well as Desktop and Mobile and remove Linux
enum {
Unknown = -1,
Windows,
MacIntel,
MacArm,
iOS,
Android32,
Android64,
Linux,
Windows = 0,
MacIntel = 1,
MacArm = 2,
iOS = 3,
Android32 = 4,
Android64 = 5,
Linux = 6,
};
using Type = decltype(Unknown);
@ -171,7 +173,14 @@ namespace geode {
*/
static GEODE_DLL bool coveredBy(const char* str, PlatformID t);
static GEODE_DLL bool coveredBy(std::string const& str, PlatformID t);
/**
* Returns the list of platforms covered by this string name. For
* example, "android" would return both Android32 and Android64
* todo in v4: deprecate this as the flagged version deals with this
*/
static GEODE_DLL std::vector<PlatformID> getCovered(std::string_view str);
// todo in v4: this does not need to be constexpr in the header. dllexport it
static constexpr char const* toString(Type lp) {
switch (lp) {
case Unknown: return "Unknown";
@ -187,6 +196,7 @@ namespace geode {
return "Undefined";
}
// todo in v4: this does not need to be constexpr in the header. dllexport it
static constexpr char const* toShortString(Type lp, bool ignoreArch = false) {
switch (lp) {
case Unknown: return "unknown";
@ -242,3 +252,13 @@ namespace std {
#elif defined(GEODE_IS_ANDROID64)
#define GEODE_PLATFORM_TARGET PlatformID::Android64
#endif
// this is cross-platform so not duplicating it across the typeinfo_cast definitions
namespace geode::cast {
template<class T, class U>
std::shared_ptr<T> typeinfo_pointer_cast(std::shared_ptr<U> const& r) noexcept {
// https://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast
auto p = typeinfo_cast<typename std::shared_ptr<T>::element_type*>(r.get());
return std::shared_ptr<T>(r, p);
}
}

View file

@ -8,6 +8,7 @@
#include <cstring>
#include <type_traits>
#include <typeinfo>
#include <memory>
namespace geode {
struct PlatformInfo {

View file

@ -2,16 +2,23 @@
#include "Popup.hpp"
#include "TextInput.hpp"
#include "Popup.hpp"
#include "../loader/Event.hpp"
#include <Geode/binding/TextInputDelegate.hpp>
namespace geode {
class ColorPickPopup;
class GEODE_DLL ColorPickPopupDelegate {
public:
virtual void updateColor(cocos2d::ccColor4B const& color) {}
};
// todo in v4: make this pimpl and maybe use events over the delegate?
// thing with events is that if you just filter via ColorPickPopup* it
// won't work unless you automatically detach the filter when closing the
// popup (otherwise opening another popup really quickly will just be
// allocated into the same memory and now the old filter is catching the
// new popup too)
class GEODE_DLL ColorPickPopup :
public Popup<cocos2d::ccColor4B const&, bool>,
public cocos2d::extension::ColorPickerDelegate,

View file

@ -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<std::pair<std::string, JsonMaybeValue>> items();
};
struct GEODE_DLL JsonMaybeObject : JsonMaybeSomething {
struct
[[deprecated("Use JsonExpectedValue via the checkJson function instead")]]
GEODE_DLL JsonMaybeObject : JsonMaybeSomething {
std::set<std::string> 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<std::monostate, std::string> m_result;
matjson::Value& m_json;
@ -289,4 +297,208 @@ namespace geode {
JsonMaybeValue root(std::string const& hierarchy);
};
class GEODE_DLL JsonExpectedValue final {
protected:
class Impl;
std::unique_ptr<Impl> m_impl;
JsonExpectedValue();
JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key);
static const char* matJsonTypeToString(matjson::Type ty);
bool hasError() const;
void setError(std::string_view error);
matjson::Value const& getJSONRef() const;
template <class... Args>
void setError(fmt::format_string<Args...> error, Args&&... args) {
this->setError(fmt::format(error, std::forward<Args>(args)...));
}
template <class T>
std::optional<T> tryGet() {
if (this->hasError()) return std::nullopt;
if constexpr (std::is_same_v<T, matjson::Value>) {
return this->getJSONRef();
}
else {
try {
if (this->getJSONRef().template is<T>()) {
return this->getJSONRef().template as<T>();
}
else {
this->setError(
"unexpected type {}",
this->matJsonTypeToString(this->getJSONRef().type())
);
}
}
// matjson can throw variant exceptions too so you need to do this
catch(std::exception const& e) {
this->setError("unable to parse json: {}", e);
}
}
return std::nullopt;
}
public:
JsonExpectedValue(matjson::Value const& value, std::string_view rootScopeName);
~JsonExpectedValue();
JsonExpectedValue(JsonExpectedValue&&);
JsonExpectedValue& operator=(JsonExpectedValue&&);
JsonExpectedValue(JsonExpectedValue const&) = delete;
JsonExpectedValue& operator=(JsonExpectedValue const&) = delete;
/**
* Get a copy of the underlying raw JSON value
*/
matjson::Value json() const;
/**
* Get the key name of this JSON value. If this is an array index,
* returns the index as a string. If this is the root object,
* returns the root scope name.
*/
std::string key() const;
/**
* Check the type of this JSON value. Does not set an error. If an
* error is already set, always returns false
*/
bool is(matjson::Type type) const;
bool isNull() const;
bool isBool() const;
bool isNumber() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
/**
* Asserts that this JSON value is of the specified type. If it is
* not, an error is set and all subsequent operations are no-ops
* @returns Itself
*/
JsonExpectedValue& assertIs(matjson::Type type);
JsonExpectedValue& assertIsNull();
JsonExpectedValue& assertIsBool();
JsonExpectedValue& assertIsNumber();
JsonExpectedValue& assertIsString();
JsonExpectedValue& assertIsArray();
JsonExpectedValue& assertIsObject();
/**
* Asserts that this JSON value is one of a list of specified types
* @returns Itself
*/
JsonExpectedValue& assertIs(std::initializer_list<matjson::Type> type);
// -- Dealing with values --
template <class T>
T get(T const& defaultValue = T()) {
if (auto v = this->template tryGet<T>()) {
return *std::move(v);
}
return defaultValue;
}
template <class T>
JsonExpectedValue& into(T& value) {
if (auto v = this->template tryGet<T>()) {
value = *std::move(v);
}
return *this;
}
template <class T>
JsonExpectedValue& into(std::optional<T>& value) {
if (auto v = this->template tryGet<T>()) {
value.emplace(*std::move(v));
}
return *this;
}
template <class T>
JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
{ predicate(std::declval<T>()) } -> std::convertible_to<bool>;
} {
if (this->hasError()) return *this;
if (auto v = this->template tryGet<T>()) {
if (!predicate(*v)) {
this->setError("json value is not {}", name);
}
}
return *this;
}
template <class T>
JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
{ predicate(std::declval<T>()) } -> std::convertible_to<Result<>>;
} {
if (this->hasError()) return *this;
if (auto v = this->template tryGet<T>()) {
auto p = predicate(*v);
if (!p) {
this->setError("json value is not {}: {}", name, p.unwrapErr());
}
}
return *this;
}
// -- Dealing with objects --
/**
* Check if this object has an optional key. Asserts that this JSON
* value is an object. If the key doesn't exist, returns a
* `JsonExpectValue` that does nothing
* @returns The key, which is a no-op value if it didn't exist
*/
JsonExpectedValue has(std::string_view key);
/**
* Check if this object has an optional key. Asserts that this JSON
* value is an object. If the key doesn't exist, sets an error and
* returns a `JsonExpectValue` that does nothing
* @returns The key, which is a no-op value if it didn't exist
*/
JsonExpectedValue needs(std::string_view key);
/**
* Asserts that this JSON value is an object. Get all object
* properties
*/
std::vector<std::pair<std::string, JsonExpectedValue>> 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<JsonExpectedValue> items();
operator bool() const;
Result<> ok();
template <class T>
Result<T> ok(T value) {
auto ok = this->ok();
if (!ok) {
return Err(ok.unwrapErr());
}
return Ok(std::forward<T>(value));
}
};
GEODE_DLL JsonExpectedValue checkJson(matjson::Value const& json, std::string_view rootScopeName);
}

View file

@ -390,6 +390,15 @@ namespace geode {
return m_handle == nullptr;
}
/**
* Create a new Task that is immediately cancelled
* @param name The name of the Task; used for debugging
*/
static Task cancelled(std::string_view const name = "<Cancelled Task>") {
auto task = Task(Handle::create(name));
Task::cancel(task.m_handle);
return task;
}
/**
* Create a new Task that immediately finishes with the given
* value

View file

@ -0,0 +1,59 @@
#pragma once
#include <tuple>
namespace geode::utils::function {
namespace detail {
template <class F>
struct ImplExtract;
template <class R, class... A>
struct ImplExtract<R(A...)> {
using Type = R(A...);
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class... A>
struct ImplExtract<R(*)(A...)> {
using Type = R(A...);
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class C, class... A>
struct ImplExtract<R(C::*)(A...)> {
using Type = R(A...);
using Class = C;
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class R, class C, class... A>
struct ImplExtract<R(C::*)(A...) const> {
using Type = R(A...);
using Class = C;
using Return = R;
using Args = std::tuple<A...>;
static constexpr std::size_t ARG_COUNT = std::tuple_size_v<Args>;
};
template <class F>
requires requires { &F::operator(); }
struct ImplExtract<F> : public ImplExtract<decltype(&F::operator())> {};
template <class F>
using Extract = ImplExtract<std::remove_cvref_t<F>>;
}
template <class F>
using FunctionInfo = detail::Extract<F>;
template <class F>
using Return = typename detail::Extract<F>::Return;
template <class F>
using Args = typename detail::Extract<F>::Args;
template <std::size_t Ix, class F>
using Arg = std::tuple_element_t<Ix, typename detail::Extract<F>::Args>;
}

View file

@ -17,13 +17,22 @@
namespace geode {
using ByteVector = std::vector<uint8_t>;
// todo in v4: remove this
template <typename T>
[[deprecated("Use geode::toBytes instead")]]
ByteVector toByteArray(T const& a) {
ByteVector out;
out.resize(sizeof(T));
std::memcpy(out.data(), &a, sizeof(T));
return out;
}
template <typename T>
ByteVector toBytes(T const& a) {
ByteVector out;
out.resize(sizeof(T));
std::memcpy(out.data(), &a, sizeof(T));
return out;
}
namespace utils {
// helper for std::visit

BIN
loader/resources/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

View file

@ -64,13 +64,6 @@
}
},
"settings": {
"show-platform-console": {
"type": "bool",
"default": false,
"name": "Show Platform Console",
"description": "Show the native console (if one exists). <cr>This setting is meant for developers</c>",
"platforms": ["win", "mac"]
},
"auto-check-updates": {
"type": "bool",
"default": true,
@ -83,6 +76,24 @@
"name": "Disable Crash Popup",
"description": "Disables the popup at startup asking if you'd like to send a bug report; intended for developers"
},
"enable-geode-theme": {
"type": "bool",
"default": true,
"name": "Enable Geode-Themed Colors",
"description": "When enabled, the Geode menu has a <ca>Geode-themed color scheme</c>. <cy>This does not affect any other menus!</c>"
},
"developer-title": {
"type": "title",
"name": "Developer Settings"
},
"show-platform-console": {
"type": "bool",
"default": false,
"name": "Show Platform Console",
"description": "Show the native console (if one exists). <cr>This setting is meant for developers</c>",
"platforms": ["win", "mac"],
"requires-restart": true
},
"server-cache-size-limit": {
"type": "int",
"default": 20,
@ -90,12 +101,6 @@
"max": 100,
"name": "Server Cache Size Limit",
"description": "Limits the size of the cache used for loading mods. Higher values result in higher memory usage."
},
"enable-geode-theme": {
"type": "bool",
"default": true,
"name": "Enable Geode-Themed Colors",
"description": "When enabled, the Geode menu has a <ca>Geode-themed color scheme</c>. <cy>This does not affect any other menus!</c>"
}
},
"issues": {

View file

@ -823,6 +823,13 @@ CCSize AxisLayout::getSizeHint(CCNode* on) const {
axis.crossLength = cross;
}
}
if (auto l = m_impl->m_autoGrowAxisMinLength) {
length = std::max(length, *l);
}
// No overflow
else {
length = std::min(length, nodeAxis(on, m_impl->m_axis, 1.f).axisLength);
}
if (!m_impl->m_allowCrossAxisOverflow) {
cross = nodeAxis(on, m_impl->m_axis, 1.f).crossLength;
}

View file

@ -185,6 +185,7 @@ int geodeEntry(void* platformData) {
log::popNest();
// download and install new loader update in the background
if (Mod::get()->getSettingValue<bool>("auto-check-updates")) {
log::info("Starting loader update check");
updater::checkForLoaderUpdates();

View file

@ -3,10 +3,10 @@
#ifdef GEODE_IS_MACOS
#include <AppKit/AppKit.h>
#include <loader/LoaderImpl.hpp>
#include <loader/LogImpl.hpp>
#include <CoreGraphics/CoreGraphics.h>
#include <AppKit/AppKit.h>
#include <Cocoa/Cocoa.h>
bool safeModeCheck() {

View file

@ -9,7 +9,7 @@ Loader::Loader() : m_impl(new Impl) {}
Loader::~Loader() {}
Loader* Loader::get() {
static auto g_geode = new Loader;
static auto g_geode = new Loader();
return g_geode;
}

View file

@ -156,15 +156,25 @@ bool Mod::hasSetting(std::string_view const key) const {
}
std::optional<Setting> Mod::getSettingDefinition(std::string_view const key) const {
return m_impl->getSettingDefinition(key);
return m_impl->m_settings->getLegacyDefinition(std::string(key));
}
SettingValue* Mod::getSetting(std::string_view const key) const {
return m_impl->getSetting(key);
return m_impl->m_settings->getLegacy(std::string(key)).get();
}
std::shared_ptr<SettingV3> Mod::getSettingV3(std::string_view const key) const {
return m_impl->m_settings->get(std::string(key));
}
void Mod::registerCustomSetting(std::string_view const key, std::unique_ptr<SettingValue> value) {
return m_impl->registerCustomSetting(key, std::move(value));
auto reg = m_impl->m_settings->registerLegacyCustomSetting(key, std::move(value));
if (!reg) {
log::error("Unable to register custom setting: {}", reg.unwrapErr());
}
}
Result<> Mod::registerCustomSettingType(std::string_view type, SettingGenerator generator) {
return m_impl->m_settings->registerCustomSettingType(type, generator);
}
std::vector<std::string> Mod::getLaunchArgumentNames() const {

View file

@ -53,7 +53,7 @@ Result<> Mod::Impl::setup() {
// always create temp dir for all mods, even if disabled, so resources can be loaded
GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp dir: {error}"));
this->setupSettings();
m_settings = std::make_unique<ModSettingsManager>(m_metadata);
auto loadRes = this->loadData();
if (!loadRes) {
log::warn("Unable to load data for \"{}\": {}", m_metadata.getID(), loadRes.unwrapErr());
@ -182,49 +182,11 @@ Result<> Mod::Impl::loadData() {
// Check if settings exist
auto settingPath = m_saveDirPath / "settings.json";
if (std::filesystem::exists(settingPath)) {
GEODE_UNWRAP_INTO(auto settingData, utils::file::readString(settingPath));
// parse settings.json
std::string error;
auto res = matjson::parse(settingData, error);
if (error.size() > 0) {
return Err("Unable to parse settings.json: " + error);
}
auto json = res.value();
JsonChecker checker(json);
auto root = checker.root(fmt::format("[{}/settings.json]", this->getID()));
GEODE_UNWRAP_INTO(auto json, utils::file::readJson(settingPath));
m_savedSettingsData = json;
for (auto& [key, value] : root.items()) {
// check if this is a known setting
if (auto setting = this->getSetting(key)) {
// load its value
if (!setting->load(value.json())) {
log::logImpl(
Severity::Error,
m_self,
"{}: Unable to load value for setting \"{}\"",
m_metadata.getID(),
key
);
}
}
else {
if (auto definition = this->getSettingDefinition(key)) {
// Found a definition for this setting, it's most likely a custom setting
// Don't warn it, as it's expected to be loaded by the mod
}
else {
log::logImpl(
Severity::Warning,
m_self,
"Encountered unknown setting \"{}\" while loading "
"settings",
key
);
}
}
auto load = m_settings->load(json);
if (!load) {
log::warn("Unable to load settings: {}", load.unwrapErr());
}
}
@ -253,103 +215,45 @@ Result<> Mod::Impl::saveData() {
return Ok();
}
// saveData is expected to be synchronous, and always called from GD thread
ModStateEvent(m_self, ModEventType::DataSaved).post();
// Data saving should be fully fail-safe
std::unordered_set<std::string> coveredSettings;
// Settings
matjson::Value json = matjson::Object();
for (auto& [key, value] : m_settings) {
coveredSettings.insert(key);
if (!value->save(json[key])) {
log::error("Unable to save setting \"{}\"", key);
}
}
// if some settings weren't provided a custom settings handler (for example,
// If some settings weren't provided a custom settings handler (for example,
// the mod was not loaded) then make sure to save their previous state in
// order to not lose data
log::debug("Check covered");
if (!m_savedSettingsData.is_object()) {
m_savedSettingsData = matjson::Object();
}
for (auto& [key, value] : m_savedSettingsData.as_object()) {
log::debug("Check if {} is saved", key);
if (!coveredSettings.contains(key)) {
json[key] = value;
}
}
matjson::Value json = m_savedSettingsData;
m_settings->save(json);
std::string settingsStr = json.dump();
std::string savedStr = m_saved.dump();
auto res = utils::file::writeString(m_saveDirPath / "settings.json", settingsStr);
auto res = utils::file::writeString(m_saveDirPath / "settings.json", json.dump());
if (!res) {
log::error("Unable to save settings: {}", res.unwrapErr());
}
auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", savedStr);
auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump());
if (!res2) {
log::error("Unable to save values: {}", res2.unwrapErr());
}
// saveData is expected to be synchronous, and always called from GD thread
ModStateEvent(m_self, ModEventType::DataSaved).post();
return Ok();
}
void Mod::Impl::setupSettings() {
for (auto& [key, sett] : m_metadata.getSettings()) {
if (auto value = sett.createDefaultValue()) {
m_settings.emplace(key, std::move(value));
}
}
}
void Mod::Impl::registerCustomSetting(std::string_view const key, std::unique_ptr<SettingValue> value) {
auto keystr = std::string(key);
if (!m_settings.count(keystr)) {
// load data
if (m_savedSettingsData.contains(key)) {
value->load(m_savedSettingsData[key]);
}
m_settings.emplace(keystr, std::move(value));
}
}
bool Mod::Impl::hasSettings() const {
return m_metadata.getSettings().size();
return m_metadata.getSettingsV3().size();
}
std::vector<std::string> Mod::Impl::getSettingKeys() const {
std::vector<std::string> keys;
for (auto& [key, _] : m_metadata.getSettings()) {
for (auto& [key, _] : m_metadata.getSettingsV3()) {
keys.push_back(key);
}
return keys;
}
std::optional<Setting> Mod::Impl::getSettingDefinition(std::string_view const key) const {
for (auto& setting : m_metadata.getSettings()) {
if (setting.first == key) {
return setting.second;
}
}
return std::nullopt;
}
SettingValue* Mod::Impl::getSetting(std::string_view const key) const {
auto keystr = std::string(key);
if (m_settings.count(keystr)) {
return m_settings.at(keystr).get();
}
return nullptr;
}
bool Mod::Impl::hasSetting(std::string_view const key) const {
for (auto& setting : m_metadata.getSettings()) {
for (auto& setting : m_metadata.getSettingsV3()) {
if (setting.first == key) {
return true;
}

View file

@ -4,6 +4,7 @@
#include "ModPatch.hpp"
#include <Geode/loader/Loader.hpp>
#include <string_view>
#include <Geode/loader/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<std::string, std::unique_ptr<SettingValue>> m_settings;
std::unique_ptr<ModSettingsManager> m_settings = nullptr;
/**
* Settings save data. Stored for efficient loading of custom settings
*/
@ -74,6 +75,8 @@ namespace geode {
Impl(Mod* self, ModMetadata const& metadata);
~Impl();
Impl(Impl const&) = delete;
Impl(Impl&&) = delete;
Result<> setup();
@ -83,8 +86,6 @@ namespace geode {
// called on a separate thread
Result<> unzipGeodeFile(ModMetadata metadata);
void setupSettings();
std::string getID() const;
std::string getName() const;
std::vector<std::string> getDevelopers() const;
@ -116,9 +117,6 @@ namespace geode {
bool hasSettings() const;
std::vector<std::string> getSettingKeys() const;
bool hasSetting(std::string_view const key) const;
std::optional<Setting> getSettingDefinition(std::string_view const key) const;
SettingValue* getSetting(std::string_view const key) const;
void registerCustomSetting(std::string_view const key, std::unique_ptr<SettingValue> value);
std::string getLaunchArgumentName(std::string_view const name) const;
std::vector<std::string> getLaunchArgumentNames() const;

View file

@ -125,45 +125,28 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
}
catch (...) { }
JsonChecker checker(impl->m_rawJSON);
auto root = checker.root(checkerRoot).obj();
auto root = checkJson(impl->m_rawJSON, checkerRoot);
root.needs("geode").into(impl->m_geodeVersion);
root.addKnownKey("gd");
// Check GD version
// (use rawJson because i dont like JsonMaybeValue)
if (rawJson.contains("gd")) {
std::string ver;
if (rawJson["gd"].is_object()) {
auto key = PlatformID::toShortString(GEODE_PLATFORM_TARGET, true);
if (rawJson["gd"].contains(key) && rawJson["gd"][key].is_string())
ver = rawJson["gd"][key].as_string();
} else if (rawJson["gd"].is_string()) {
if (auto gd = root.needs("gd")) {
// In the future when we get rid of support for string format just
// change all of this to the gd.needs(...) stuff
gd.assertIs({ matjson::Type::Object, matjson::Type::String });
if (gd.isObject()) {
gd.needs(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH)
.mustBe<std::string>("a valid gd version", [](auto const& str) {
return str == "*" || numFromString<double>(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<double>(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<bool(std::string const&)>(&ModMetadata::Impl::validateOldID))
.mustBe<std::string>(ID_REGEX, &ModMetadata::Impl::validateOldID)
.into(impl->m_id);
// if (!isDeprecatedIDForm(impl->m_id)) {
@ -180,7 +163,7 @@ Result<ModMetadata> 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<std::string>());
}
}
@ -205,11 +188,9 @@ Result<ModMetadata> 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<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
}
@ -219,11 +200,10 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
}
Dependency dependency;
// todo: make this use validateID in full 2.0.0 release
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::Impl::validateOldID)).into(dependency.id);
obj.needs("version").into(dependency.version);
obj.has("importance").into(dependency.importance);
obj.checkUnknownKeys();
dep.needs("id").mustBe<std::string>(ID_REGEX, &ModMetadata::Impl::validateOldID).into(dependency.id);
dep.needs("version").into(dependency.version);
dep.has("importance").into(dependency.importance);
dep.checkUnknownKeys();
if (
dependency.version.getComparison() != VersionCompare::MoreEq &&
@ -247,24 +227,30 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
impl->m_dependencies.push_back(dependency);
}
for (auto& incompat : root.has("incompatibilities").iterate()) {
auto obj = incompat.obj();
for (auto& incompat : root.has("incompatibilities").items()) {
bool onThisPlatform = !incompat.has("platforms");
for (auto& plat : incompat.has("platforms").items()) {
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
}
}
if (!onThisPlatform) {
continue;
}
Incompatibility incompatibility;
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::Impl::validateOldID)).into(incompatibility.id);
obj.needs("version").into(incompatibility.version);
obj.has("importance").into(incompatibility.importance);
obj.checkUnknownKeys();
incompat.needs("id").mustBe<std::string>(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<matjson::Object>()) {
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<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
}
@ -273,25 +259,23 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
continue;
}
}
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value));
impl->m_settings.emplace_back(key, sett);
impl->m_settings.emplace_back(key, value.json());
}
if (auto resources = root.has("resources").obj()) {
for (auto& [key, _] : resources.has("spritesheets").items()) {
if (auto resources = root.has("resources")) {
for (auto& [key, _] : resources.has("spritesheets").properties()) {
impl->m_spritesheets.push_back(impl->m_id + "/" + key);
}
}
if (auto issues = root.has("issues").obj()) {
if (auto issues = root.has("issues")) {
IssuesInfo issuesInfo;
issues.needs("info").into(issuesInfo.info);
issues.has("url").intoAs<std::string>(issuesInfo.url);
issues.has("url").into(issuesInfo.url);
impl->m_issues = issuesInfo;
}
if (auto links = root.has("links").obj()) {
if (auto links = root.has("links")) {
links.has("homepage").into(info.getLinksMut().getImpl()->m_homepage);
links.has("source").into(info.getLinksMut().getImpl()->m_source);
links.has("community").into(info.getLinksMut().getImpl()->m_community);
@ -299,19 +283,16 @@ Result<ModMetadata> 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<std::string>());
}
// 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> ModMetadata::Impl::create(ModJson const& json) {
@ -542,6 +523,18 @@ std::vector<std::string> ModMetadata::getSpritesheets() const {
return m_impl->m_spritesheets;
}
std::vector<std::pair<std::string, Setting>> ModMetadata::getSettings() const {
std::vector<std::pair<std::string, Setting>> 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<std::pair<std::string, matjson::Value>> ModMetadata::getSettingsV3() const {
return m_impl->m_settings;
}
std::unordered_set<std::string> ModMetadata::getTags() const {
@ -561,7 +554,7 @@ VersionInfo ModMetadata::getGeodeVersion() const {
return m_impl->m_geodeVersion;
}
Result<> ModMetadata::checkGameVersion() const {
if (!m_impl->m_gdVersion.empty()) {
if (!m_impl->m_gdVersion.empty() && m_impl->m_gdVersion != "*") {
auto const ver = m_impl->m_gdVersion;
auto res = numFromString<double>(ver);
@ -643,6 +636,10 @@ void ModMetadata::setSpritesheets(std::vector<std::string> const& value) {
m_impl->m_spritesheets = value;
}
void ModMetadata::setSettings(std::vector<std::pair<std::string, Setting>> 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<std::pair<std::string, matjson::Value>> const& value) {
m_impl->m_settings = value;
}
void ModMetadata::setTags(std::unordered_set<std::string> const& value) {

View file

@ -4,6 +4,7 @@
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/VersionInfo.hpp>
#include <Geode/loader/SettingV3.hpp>
using namespace geode::prelude;
@ -36,7 +37,7 @@ namespace geode {
std::vector<Dependency> m_dependencies;
std::vector<Incompatibility> m_incompatibilities;
std::vector<std::string> m_spritesheets;
std::vector<std::pair<std::string, Setting>> m_settings;
std::vector<std::pair<std::string, matjson::Value>> m_settings;
std::unordered_set<std::string> m_tags;
bool m_needsEarlyLoad = false;
bool m_isAPI = false;

View file

@ -0,0 +1,253 @@
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include "ModImpl.hpp"
using namespace geode::prelude;
// All setting type generators are put in a shared pool for two reasons:
// #1 no need to duplicate the built-in settings between all mods
// #2 easier lookup of custom settings if a mod uses another mod's custom setting type
class SharedSettingTypesPool final {
private:
std::unordered_map<std::string, SettingGenerator> m_types;
SharedSettingTypesPool() : m_types({
// todo in v4: remove this
{ "custom", &LegacyCustomSettingV3::parse },
{ "title", &TitleSettingV3::parse },
{ "bool", &BoolSettingV3::parse },
{ "int", &IntSettingV3::parse },
{ "float", &FloatSettingV3::parse },
{ "string", &StringSettingV3::parse },
{ "file", &FileSettingV3::parse },
{ "folder", &FileSettingV3::parse },
{ "path", &FileSettingV3::parse },
{ "rgb", &Color3BSettingV3::parse },
{ "color", &Color3BSettingV3::parse },
{ "rgba", &Color4BSettingV3::parse },
}) {}
public:
static SharedSettingTypesPool& get() {
static auto inst = SharedSettingTypesPool();
return inst;
}
Result<> add(std::string_view modID, std::string_view type, SettingGenerator generator) {
// Limit type to just [a-z0-9\-]+
if (type.empty() || !std::all_of(type.begin(), type.end(), +[](char c) {
return
('a' <= c && c <= 'z') ||
('0' <= c && c <= '9') ||
(c == '-');
})) {
return Err("Custom setting types must match the regex [a-z0-9\\-]+");
}
auto full = fmt::format("{}/{}", modID, type);
if (m_types.contains(full)) {
return Err("Type \"{}\" has already been registered for mod {}", type, modID);
}
m_types.emplace(full, generator);
return Ok();
}
std::optional<SettingGenerator> find(std::string_view modID, std::string_view fullType) {
// Find custom settings via namespaced lookup
if (fullType.starts_with("custom:")) {
auto full = std::string(fullType.substr(fullType.find(':') + 1));
// If there's no mod ID in the type name, use the current mod's ID
if (full.find('/') == std::string_view::npos) {
full = fmt::format("{}/{}", modID, full);
}
if (m_types.contains(full)) {
return m_types.at(full);
}
}
// Otherwise find a built-in setting
else {
auto full = std::string(fullType);
if (m_types.contains(full)) {
return m_types.at(full);
}
}
// Return null if nothing was found
return std::nullopt;
}
};
class ModSettingsManager::Impl final {
public:
struct SettingInfo final {
std::string type;
matjson::Value json;
std::shared_ptr<SettingV3> v3 = nullptr;
// todo: remove in v4
std::shared_ptr<SettingValue> legacy = nullptr;
};
std::string modID;
std::unordered_map<std::string, SettingInfo> settings;
bool restartRequired = false;
void createSettings() {
for (auto& [key, setting] : settings) {
if (setting.v3) {
continue;
}
auto gen = SharedSettingTypesPool::get().find(modID, setting.type);
// The type was not found, meaning it probably hasn't been registered yet
if (!gen) {
continue;
}
if (auto v3 = (*gen)(key, modID, setting.json)) {
setting.v3 = *v3;
}
else {
log::error(
"Unable to parse setting '{}' for mod {}: {}",
key, modID, v3.unwrapErr()
);
}
}
}
};
ModSettingsManager* ModSettingsManager::from(Mod* mod) {
if (!mod) return nullptr;
return ModImpl::getImpl(mod)->m_settings.get();
}
ModSettingsManager::ModSettingsManager(ModMetadata const& metadata)
: m_impl(std::make_unique<Impl>())
{
m_impl->modID = metadata.getID();
for (auto const& [key, json] : metadata.getSettingsV3()) {
auto setting = Impl::SettingInfo();
setting.json = json;
auto root = checkJson(json, "setting");
root.needs("type").into(setting.type);
if (root) {
if (setting.type == "custom") {
log::warn(
"Setting \"{}\" in mod {} has the old \"custom\" type - "
"this type has been deprecated and will be removed in Geode v4.0.0. "
"Use the new \"custom:type-name-here\" syntax for defining custom "
"setting types - see more in INSERT TUTORIAL HERE",
key, m_impl->modID
);
}
m_impl->settings.emplace(key, setting);
}
else {
log::error("Setting '{}' in mod {} is missing type", key, m_impl->modID);
}
}
m_impl->createSettings();
}
ModSettingsManager::~ModSettingsManager() {}
ModSettingsManager::ModSettingsManager(ModSettingsManager&&) = default;
void ModSettingsManager::markRestartRequired() {
m_impl->restartRequired = true;
}
Result<> ModSettingsManager::registerCustomSettingType(std::string_view type, SettingGenerator generator) {
GEODE_UNWRAP(SharedSettingTypesPool::get().add(m_impl->modID, type, generator));
m_impl->createSettings();
return Ok();
}
Result<> ModSettingsManager::registerLegacyCustomSetting(std::string_view key, std::unique_ptr<SettingValue>&& ptr) {
auto id = std::string(key);
if (!m_impl->settings.count(id)) {
return Err("No such setting '{}' in mod {}", id, m_impl->modID);
}
auto& sett = m_impl->settings.at(id);
if (auto custom = typeinfo_pointer_cast<LegacyCustomSettingV3>(sett.v3)) {
if (!custom->getValue()) {
custom->setValue(std::move(ptr));
}
else {
return Err("Setting '{}' in mod {} has already been registed", id, m_impl->modID);
}
}
else {
return Err("Setting '{}' in mod {} is not a legacy custom setting", id, m_impl->modID);
}
return Ok();
}
Result<> ModSettingsManager::load(matjson::Value const& json) {
auto root = checkJson(json, "Settings");
for (auto const& [key, value] : root.properties()) {
if (m_impl->settings.contains(key)) {
auto& sett = m_impl->settings.at(key);
if (!sett.v3) continue;
try {
if (!sett.v3->load(value.json())) {
log::error("Unable to load setting '{}' for mod {}", key, m_impl->modID);
}
}
// matjson::JsonException doesn't catch all possible json errors
catch(std::exception const& e) {
log::error("Unable to load setting '{}' for mod {} (JSON exception): {}", key, m_impl->modID, e.what());
}
}
}
return Ok();
}
void ModSettingsManager::save(matjson::Value& json) {
for (auto& [key, sett] : m_impl->settings) {
if (!sett.v3) {
continue;
}
// Store the value in an intermediary so if `save` fails the existing
// value loaded from disk isn't overwritten
matjson::Value value;
try {
if (sett.v3->save(value)) {
json[key] = value;
}
else {
log::error("Unable to save setting '{}' for mod {}", key, m_impl->modID);
}
}
catch(matjson::JsonException const& e) {
log::error("Unable to save setting '{}' for mod {} (JSON exception): {}", key, m_impl->modID, e.what());
}
}
}
std::shared_ptr<SettingV3> ModSettingsManager::get(std::string_view key) {
auto id = std::string(key);
return m_impl->settings.count(id) ? m_impl->settings.at(id).v3 : nullptr;
}
std::shared_ptr<SettingValue> ModSettingsManager::getLegacy(std::string_view key) {
auto id = std::string(key);
if (!m_impl->settings.count(id)) {
return nullptr;
}
auto& info = m_impl->settings.at(id);
// If this setting has alreay been given a legacy interface, give that
if (info.legacy) {
return info.legacy;
}
// Uninitialized settings are null
if (!info.v3) {
return nullptr;
}
// Generate new legacy interface
if (auto legacy = info.v3->convertToLegacyValue()) {
info.legacy.swap(*legacy);
return info.legacy;
}
return nullptr;
}
std::optional<Setting> ModSettingsManager::getLegacyDefinition(std::string_view key) {
if (auto s = this->get(key)) {
return s->convertToLegacy();
}
return std::nullopt;
}
bool ModSettingsManager::restartRequired() const {
return m_impl->restartRequired;
}

View file

@ -1,6 +1,7 @@
#include <ui/mods/settings/GeodeSettingNode.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Setting.hpp>
#include <Geode/loader/SettingV3.hpp>
#include <Geode/loader/SettingEvent.hpp>
#include <Geode/loader/SettingNode.hpp>
#include <Geode/utils/general.hpp>
@ -279,10 +280,10 @@ std::string SettingValue::getModID() const {
}
void SettingValue::valueChanged() {
// this is actually p neat because now if the mod gets disabled this wont
// post the event so that side-effect is automatically handled :3
if (auto mod = Loader::get()->getLoadedMod(m_modID)) {
SettingChangedEvent(mod, this).post();
if (auto sett = mod->getSettingV3(m_key)) {
sett->markChanged();
}
}
}
@ -296,19 +297,36 @@ void SettingValue::valueChanged() {
return type_##SettingNode::create(this, width); \
} \
template<> \
typename GeodeSettingValue<type_##Setting>::ValueType \
GeodeSettingValue<type_##Setting>::getValue() const { \
using S = typename SettingTypeForValueType<ValueType>::SettingType; \
if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \
if (auto setting = typeinfo_pointer_cast<S>(mod->getSettingV3(m_key))) {\
return setting->getValue(); \
} \
} \
return m_value; \
} \
template<> \
void GeodeSettingValue< \
type_##Setting \
>::setValue(ValueType const& value) { \
m_value = this->toValid(value).first; \
this->valueChanged(); \
using S = typename SettingTypeForValueType<ValueType>::SettingType; \
if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \
if (auto setting = typeinfo_pointer_cast<S>(mod->getSettingV3(m_key))) {\
return setting->setValue(value); \
} \
} \
} \
template<> \
Result<> GeodeSettingValue< \
type_##Setting \
>::validate(ValueType const& value) const { \
auto reason = this->toValid(value).second; \
if (reason.has_value()) { \
return Err(static_cast<std::string>(reason.value())); \
using S = typename SettingTypeForValueType<ValueType>::SettingType; \
if (auto mod = Loader::get()->getInstalledMod(m_modID)) { \
if (auto setting = typeinfo_pointer_cast<S>(mod->getSettingV3(m_key))) {\
return setting->isValid(value); \
} \
} \
return Ok(); \
} \
@ -316,8 +334,11 @@ void SettingValue::valueChanged() {
typename type_##Setting::ValueType SettingValueSetter< \
typename type_##Setting::ValueType \
>::get(SettingValue* setting) { \
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
return b->getValue(); \
using S = typename SettingTypeForValueType<typename type_##Setting::ValueType>::SettingType; \
if (auto mod = Loader::get()->getInstalledMod(setting->getModID())) { \
if (auto sett = typeinfo_pointer_cast<S>(mod->getSettingV3(setting->getKey()))) { \
return sett->getValue(); \
} \
} \
return typename type_##Setting::ValueType(); \
} \
@ -328,8 +349,11 @@ void SettingValue::valueChanged() {
SettingValue* setting, \
typename type_##Setting::ValueType const& value \
) { \
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
b->setValue(value); \
using S = typename SettingTypeForValueType<typename type_##Setting::ValueType>::SettingType; \
if (auto mod = Loader::get()->getInstalledMod(setting->getModID())) { \
if (auto sett = typeinfo_pointer_cast<S>(mod->getSettingV3(setting->getKey()))) { \
return sett->setValue(value); \
} \
} \
}

View file

@ -0,0 +1,712 @@
#include "SettingNodeV3.hpp"
#include <Geode/loader/SettingNode.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include <ui/mods/GeodeStyle.hpp>
class SettingNodeSizeChangeEventV3::Impl final {
public:
SettingNodeV3* node;
};
SettingNodeSizeChangeEventV3::SettingNodeSizeChangeEventV3(SettingNodeV3* node)
: m_impl(std::make_shared<Impl>())
{
m_impl->node = node;
}
SettingNodeSizeChangeEventV3::~SettingNodeSizeChangeEventV3() = default;
SettingNodeV3* SettingNodeSizeChangeEventV3::getNode() const {
return m_impl->node;
}
class SettingNodeValueChangeEventV3::Impl final {
public:
SettingNodeV3* node;
bool commit = false;
};
SettingNodeValueChangeEventV3::SettingNodeValueChangeEventV3(SettingNodeV3* node, bool commit)
: m_impl(std::make_shared<Impl>())
{
m_impl->node = node;
m_impl->commit = commit;
}
SettingNodeValueChangeEventV3::~SettingNodeValueChangeEventV3() = default;
SettingNodeV3* SettingNodeValueChangeEventV3::getNode() const {
return m_impl->node;
}
bool SettingNodeValueChangeEventV3::isCommit() const {
return m_impl->commit;
}
class SettingNodeV3::Impl final {
public:
std::shared_ptr<SettingV3> setting;
CCLayerColor* bg;
CCLabelBMFont* nameLabel;
CCMenu* nameMenu;
CCMenu* buttonMenu;
CCMenuItemSpriteExtra* resetButton;
CCLabelBMFont* statusLabel;
ccColor4B bgColor = ccc4(0, 0, 0, 0);
bool committed = false;
};
bool SettingNodeV3::init(std::shared_ptr<SettingV3> setting, float width) {
if (!CCNode::init())
return false;
// note: setting may be null due to UnresolvedCustomSettingNodeV3
m_impl = std::make_shared<Impl>();
m_impl->setting = setting;
m_impl->bg = CCLayerColor::create({ 0, 0, 0, 0 });
m_impl->bg->setContentSize({ width, 0 });
m_impl->bg->ignoreAnchorPointForPosition(false);
m_impl->bg->setAnchorPoint(ccp(.5f, .5f));
this->addChildAtPosition(m_impl->bg, Anchor::Center);
m_impl->nameMenu = CCMenu::create();
m_impl->nameMenu->setContentWidth(width / 2 + 25);
m_impl->nameLabel = CCLabelBMFont::create(setting ? setting->getDisplayName().c_str() : "", "bigFont.fnt");
m_impl->nameLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .4f)->setScalePriority(1));
m_impl->nameMenu->addChild(m_impl->nameLabel);
m_impl->statusLabel = CCLabelBMFont::create("", "bigFont.fnt");
m_impl->statusLabel->setScale(.25f);
this->addChildAtPosition(m_impl->statusLabel, Anchor::Left, ccp(10, -10), ccp(0, .5f));
if (setting && setting->getDescription()) {
auto descSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
descSpr->setScale(.5f);
auto descBtn = CCMenuItemSpriteExtra::create(
descSpr, this, menu_selector(SettingNodeV3::onDescription)
);
m_impl->nameMenu->addChild(descBtn);
}
auto resetSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr);
resetSpr->setScale(.5f);
m_impl->resetButton = CCMenuItemSpriteExtra::create(
resetSpr, this, menu_selector(SettingNodeV3::onReset)
);
m_impl->nameMenu->addChild(m_impl->resetButton);
m_impl->nameMenu->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::Start));
m_impl->nameMenu->getLayout()->ignoreInvisibleChildren(true);
this->addChildAtPosition(m_impl->nameMenu, Anchor::Left, ccp(10, 0), ccp(0, .5f));
m_impl->buttonMenu = CCMenu::create();
m_impl->buttonMenu->setContentSize({ width / 2 - 55, 30 });
m_impl->buttonMenu->setLayout(AnchorLayout::create());
this->addChildAtPosition(m_impl->buttonMenu, Anchor::Right, ccp(-10, 0), ccp(1, .5f));
this->setAnchorPoint({ .5f, .5f });
this->setContentSize({ width, 30 });
return true;
}
void SettingNodeV3::updateState(CCNode* invoker) {
m_impl->statusLabel->setVisible(false);
m_impl->nameLabel->setColor(this->hasUncommittedChanges() ? ccc3(17, 221, 0) : ccWHITE);
m_impl->resetButton->setVisible(this->hasNonDefaultValue());
m_impl->bg->setColor(to3B(m_impl->bgColor));
m_impl->bg->setOpacity(m_impl->bgColor.a);
if (m_impl->setting && !m_impl->setting->shouldEnable()) {
if (auto desc = m_impl->setting->getEnableIfDescription()) {
m_impl->nameLabel->setColor(ccGRAY);
m_impl->statusLabel->setVisible(true);
m_impl->statusLabel->setColor("mod-list-errors-found"_cc3b);
m_impl->statusLabel->setString(desc->c_str());
}
}
if (m_impl->setting && m_impl->setting->requiresRestart() && m_impl->committed) {
m_impl->statusLabel->setVisible(true);
m_impl->statusLabel->setColor("mod-list-restart-required-label"_cc3b);
m_impl->statusLabel->setString("Restart Required");
m_impl->bg->setColor("mod-list-restart-required-label-bg"_cc3b);
m_impl->bg->setOpacity(75);
}
m_impl->nameMenu->setContentWidth(this->getContentWidth() - m_impl->buttonMenu->getContentWidth() - 20);
m_impl->nameMenu->updateLayout();
}
void SettingNodeV3::onDescription(CCObject*) {
auto title = m_impl->setting->getDisplayName();
FLAlertLayer::create(
nullptr,
title.c_str(),
m_impl->setting->getDescription().value_or("No description provided"),
"OK", nullptr,
clamp(title.size() * 16, 300, 400)
)->show();
}
void SettingNodeV3::onReset(CCObject*) {
createQuickPopup(
"Reset",
fmt::format(
"Are you sure you want to <cr>reset</c> <cl>{}</c> to <cy>default</c>?",
this->getSetting()->getDisplayName()
),
"Cancel", "Reset",
[this](auto, bool btn2) {
if (btn2) {
this->resetToDefault();
}
}
);
}
void SettingNodeV3::setDefaultBGColor(ccColor4B color) {
m_impl->bgColor = color;
this->updateState(nullptr);
}
void SettingNodeV3::markChanged(CCNode* invoker) {
this->updateState(invoker);
SettingNodeValueChangeEventV3(this, false).post();
}
void SettingNodeV3::commit() {
this->onCommit();
m_impl->committed = true;
this->updateState(nullptr);
SettingNodeValueChangeEventV3(this, true).post();
}
void SettingNodeV3::resetToDefault() {
if (!m_impl->setting) return;
m_impl->setting->reset();
m_impl->committed = true;
this->onResetToDefault();
this->updateState(nullptr);
SettingNodeValueChangeEventV3(this, false).post();
}
void SettingNodeV3::setContentSize(CCSize const& size) {
CCNode::setContentSize(size);
m_impl->bg->setContentSize(size);
this->updateLayout();
SettingNodeSizeChangeEventV3(this).post();
}
CCLabelBMFont* SettingNodeV3::getNameLabel() const {
return m_impl->nameLabel;
}
CCLabelBMFont* SettingNodeV3::getStatusLabel() const {
return m_impl->statusLabel;
}
CCMenu* SettingNodeV3::getNameMenu() const {
return m_impl->nameMenu;
}
CCMenu* SettingNodeV3::getButtonMenu() const {
return m_impl->buttonMenu;
}
CCLayerColor* SettingNodeV3::getBG() const {
return m_impl->bg;
}
std::shared_ptr<SettingV3> SettingNodeV3::getSetting() const {
return m_impl->setting;
}
// TitleSettingNodeV3
bool TitleSettingNodeV3::init(std::shared_ptr<TitleSettingV3> setting, float width) {
if (!SettingNodeV3::init(setting, width))
return false;
auto collapseSprBG = CCSprite::create("square02c_001.png");
collapseSprBG->setColor(ccc3(25, 25, 25));
collapseSprBG->setOpacity(105);
auto collapseSpr = CCSprite::createWithSpriteFrameName("edit_downBtn_001.png");
collapseSpr->setScale(1.9f);
collapseSprBG->addChildAtPosition(collapseSpr, Anchor::Center);
collapseSprBG->setScale(.2f);
auto uncollapseSprBG = CCSprite::create("square02c_001.png");
uncollapseSprBG->setColor(ccc3(25, 25, 25));
uncollapseSprBG->setOpacity(105);
auto uncollapseSpr = CCSprite::createWithSpriteFrameName("edit_delCBtn_001.png");
uncollapseSpr->setScale(1.5f);
uncollapseSprBG->addChildAtPosition(uncollapseSpr, Anchor::Center);
uncollapseSprBG->setScale(.2f);
m_collapseToggle = CCMenuItemToggler::create(
collapseSprBG, uncollapseSprBG,
this, menu_selector(TitleSettingNodeV3::onCollapse)
);
m_collapseToggle->m_notClickable = true;
this->getButtonMenu()->setContentWidth(20);
this->getButtonMenu()->addChildAtPosition(m_collapseToggle, Anchor::Center);
this->getNameLabel()->setFntFile("goldFont.fnt");
this->getNameMenu()->updateLayout();
this->setContentHeight(20);
this->updateState(nullptr);
return true;
}
void TitleSettingNodeV3::onCollapse(CCObject* sender) {
m_collapseToggle->toggle(!m_collapseToggle->isToggled());
// This triggers popup state to update due to SettingNodeValueChangeEventV3 being posted
this->markChanged(static_cast<CCNode*>(sender));
}
void TitleSettingNodeV3::onCommit() {}
bool TitleSettingNodeV3::isCollapsed() const {
return m_collapseToggle->isToggled();
}
bool TitleSettingNodeV3::hasUncommittedChanges() const {
return false;
}
bool TitleSettingNodeV3::hasNonDefaultValue() const {
return false;
}
void TitleSettingNodeV3::onResetToDefault() {}
std::shared_ptr<TitleSettingV3> TitleSettingNodeV3::getSetting() const {
return std::static_pointer_cast<TitleSettingV3>(SettingNodeV3::getSetting());
}
TitleSettingNodeV3* TitleSettingNodeV3::create(std::shared_ptr<TitleSettingV3> setting, float width) {
auto ret = new TitleSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// BoolSettingNodeV3
bool BoolSettingNodeV3::init(std::shared_ptr<BoolSettingV3> setting, float width) {
if (!SettingValueNodeV3::init(setting, width))
return false;
this->getButtonMenu()->setContentWidth(20);
m_toggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(BoolSettingNodeV3::onToggle), .55f
);
m_toggle->m_onButton->setContentSize({ 25, 25 });
m_toggle->m_onButton->getNormalImage()->setPosition(ccp(25, 25) / 2);
m_toggle->m_offButton->setContentSize({ 25, 25 });
m_toggle->m_offButton->getNormalImage()->setPosition(ccp(25, 25) / 2);
m_toggle->m_notClickable = true;
m_toggle->toggle(setting->getValue());
this->getButtonMenu()->addChildAtPosition(m_toggle, Anchor::Right, ccp(-10, 0));
this->updateState(nullptr);
return true;
}
void BoolSettingNodeV3::updateState(CCNode* invoker) {
SettingValueNodeV3::updateState(invoker);
auto enable = this->getSetting()->shouldEnable();
m_toggle->toggle(this->getValue());
m_toggle->setCascadeColorEnabled(true);
m_toggle->setCascadeOpacityEnabled(true);
m_toggle->setEnabled(enable);
m_toggle->setColor(enable ? ccWHITE : ccGRAY);
m_toggle->setOpacity(enable ? 255 : 155);
}
void BoolSettingNodeV3::onToggle(CCObject*) {
this->setValue(!m_toggle->isToggled(), m_toggle);
this->markChanged(m_toggle);
}
BoolSettingNodeV3* BoolSettingNodeV3::create(std::shared_ptr<BoolSettingV3> setting, float width) {
auto ret = new BoolSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// StringSettingNodeV3
bool StringSettingNodeV3::init(std::shared_ptr<StringSettingV3> setting, float width) {
if (!SettingValueNodeV3::init(setting, width))
return false;
m_input = TextInput::create(setting->getEnumOptions() ? width / 2 - 50 : width / 2, "Text");
m_input->setCallback([this](auto const& str) {
this->setValue(str, m_input);
});
m_input->setScale(.7f);
m_input->setString(this->getSetting()->getValue());
this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center);
if (setting->getEnumOptions()) {
m_input->getBGSprite()->setVisible(false);
m_input->setEnabled(false);
m_input->getInputNode()->m_placeholderLabel->setOpacity(255);
m_input->getInputNode()->m_placeholderLabel->setColor(ccWHITE);
m_arrowLeftSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
m_arrowLeftSpr->setFlipX(true);
m_arrowLeftSpr->setScale(.4f);
auto arrowLeftBtn = CCMenuItemSpriteExtra::create(
m_arrowLeftSpr, this, menu_selector(StringSettingNodeV3::onArrow)
);
arrowLeftBtn->setTag(-1);
this->getButtonMenu()->addChildAtPosition(arrowLeftBtn, Anchor::Left, ccp(5, 0));
m_arrowRightSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
m_arrowRightSpr->setScale(.4f);
auto arrowRightBtn = CCMenuItemSpriteExtra::create(
m_arrowRightSpr, this, menu_selector(StringSettingNodeV3::onArrow)
);
arrowRightBtn->setTag(1);
this->getButtonMenu()->addChildAtPosition(arrowRightBtn, Anchor::Right, ccp(-5, 0));
}
this->updateState(nullptr);
return true;
}
void StringSettingNodeV3::updateState(CCNode* invoker) {
SettingValueNodeV3::updateState(invoker);
if (invoker != m_input) {
m_input->setString(this->getValue());
}
auto enable = this->getSetting()->shouldEnable();
if (!this->getSetting()->getEnumOptions()) {
m_input->setEnabled(enable);
}
else {
m_arrowRightSpr->setOpacity(enable ? 255 : 155);
m_arrowRightSpr->setColor(enable ? ccWHITE : ccGRAY);
m_arrowLeftSpr->setOpacity(enable ? 255 : 155);
m_arrowLeftSpr->setColor(enable ? ccWHITE : ccGRAY);
}
}
void StringSettingNodeV3::onArrow(CCObject* sender) {
auto options = *this->getSetting()->getEnumOptions();
auto index = ranges::indexOf(options, this->getValue()).value_or(0);
if (sender->getTag() > 0) {
index = index < options.size() - 1 ? index + 1 : 0;
}
else {
index = index > 0 ? index - 1 : options.size() - 1;
}
this->setValue(options.at(index), static_cast<CCNode*>(sender));
}
StringSettingNodeV3* StringSettingNodeV3::create(std::shared_ptr<StringSettingV3> setting, float width) {
auto ret = new StringSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// FileSettingNodeV3
bool FileSettingNodeV3::init(std::shared_ptr<FileSettingV3> setting, float width) {
if (!SettingValueNodeV3::init(setting, width))
return false;
auto labelBG = extension::CCScale9Sprite::create("square02b_001.png", { 0, 0, 80, 80 });
labelBG->setScale(.25f);
labelBG->setColor({ 0, 0, 0 });
labelBG->setOpacity(90);
labelBG->setContentSize({ 420, 80 });
this->getButtonMenu()->addChildAtPosition(labelBG, Anchor::Center, ccp(-10, 0));
m_fileIcon = CCSprite::create();
this->getButtonMenu()->addChildAtPosition(m_fileIcon, Anchor::Left, ccp(5, 0));
m_nameLabel = CCLabelBMFont::create("", "bigFont.fnt");
this->getButtonMenu()->addChildAtPosition(m_nameLabel, Anchor::Left, ccp(13, 0), ccp(0, .5f));
m_selectBtnSpr = CCSprite::createWithSpriteFrameName("GJ_plus2Btn_001.png");
m_selectBtnSpr->setScale(.7f);
m_selectBtn = CCMenuItemSpriteExtra::create(
m_selectBtnSpr, this, menu_selector(FileSettingNodeV3::onPickFile)
);
this->getButtonMenu()->addChildAtPosition(m_selectBtn, Anchor::Right, ccp(-5, 0));
this->updateState(nullptr);
return true;
}
void FileSettingNodeV3::updateState(CCNode* invoker) {
SettingValueNodeV3::updateState(invoker);
m_fileIcon->setDisplayFrame(CCSpriteFrameCache::get()->spriteFrameByName(
this->getSetting()->isFolder() ? "folderIcon_001.png" : "file.png"_spr
));
limitNodeSize(m_fileIcon, ccp(10, 10), 1.f, .1f);
if (this->getValue().empty()) {
m_nameLabel->setString(this->getSetting()->isFolder() ? "No Folder Selected" : "No File Selected");
m_nameLabel->setColor(ccGRAY);
m_nameLabel->setOpacity(155);
}
else {
m_nameLabel->setString(this->getValue().filename().string().c_str());
m_nameLabel->setColor(ccWHITE);
m_nameLabel->setOpacity(255);
}
m_nameLabel->limitLabelWidth(75, .35f, .1f);
auto enable = this->getSetting()->shouldEnable();
m_selectBtnSpr->setOpacity(enable ? 255 : 155);
m_selectBtnSpr->setColor(enable ? ccWHITE : ccGRAY);
m_selectBtn->setEnabled(enable);
}
void FileSettingNodeV3::onPickFile(CCObject*) {
m_pickListener.bind([this](auto* event) {
auto value = event->getValue();
if (!value) {
return;
}
if (value->isOk()) {
this->setValue(value->unwrap(), nullptr);
}
else {
FLAlertLayer::create(
"Failed",
fmt::format("Failed to pick file: {}", value->unwrapErr()),
"Ok"
)->show();
}
});
std::error_code ec;
m_pickListener.setFilter(file::pick(
this->getSetting()->isFolder() ?
file::PickMode::OpenFolder :
(this->getSetting()->useSaveDialog() ? file::PickMode::SaveFile : file::PickMode::OpenFile),
{
// Prefer opening the current path directly if possible
this->getValue().empty() || !std::filesystem::exists(this->getValue().parent_path(), ec) ?
dirs::getGameDir() :
this->getValue(),
this->getSetting()->getFilters().value_or(std::vector<file::FilePickOptions::Filter>())
}
));
}
FileSettingNodeV3* FileSettingNodeV3::create(std::shared_ptr<FileSettingV3> setting, float width) {
auto ret = new FileSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// Color3BSettingNodeV3
bool Color3BSettingNodeV3::init(std::shared_ptr<Color3BSettingV3> setting, float width) {
if (!SettingValueNodeV3::init(setting, width))
return false;
m_colorSprite = ColorChannelSprite::create();
m_colorSprite->setScale(.65f);
m_colorBtn = CCMenuItemSpriteExtra::create(
m_colorSprite, this, menu_selector(Color3BSettingNodeV3::onSelectColor)
);
this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0));
this->updateState(nullptr);
return true;
}
void Color3BSettingNodeV3::updateState(CCNode* invoker) {
SettingValueNodeV3::updateState(invoker);
m_colorSprite->setColor(this->getValue());
auto enable = this->getSetting()->shouldEnable();
m_colorSprite->setOpacity(enable ? 255 : 155);
m_colorBtn->setEnabled(enable);
}
void Color3BSettingNodeV3::onSelectColor(CCObject*) {
auto popup = ColorPickPopup::create(this->getValue());
popup->setDelegate(this);
popup->show();
}
void Color3BSettingNodeV3::updateColor(ccColor4B const& color) {
this->setValue(to3B(color), nullptr);
}
Color3BSettingNodeV3* Color3BSettingNodeV3::create(std::shared_ptr<Color3BSettingV3> setting, float width) {
auto ret = new Color3BSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// Color4BSettingNodeV3
bool Color4BSettingNodeV3::init(std::shared_ptr<Color4BSettingV3> setting, float width) {
if (!SettingValueNodeV3::init(setting, width))
return false;
m_colorSprite = ColorChannelSprite::create();
m_colorSprite->setScale(.65f);
m_colorBtn = CCMenuItemSpriteExtra::create(
m_colorSprite, this, menu_selector(Color4BSettingNodeV3::onSelectColor)
);
this->getButtonMenu()->addChildAtPosition(m_colorBtn, Anchor::Right, ccp(-10, 0));
this->updateState(nullptr);
return true;
}
void Color4BSettingNodeV3::updateState(CCNode* invoker) {
SettingValueNodeV3::updateState(invoker);
m_colorSprite->setColor(to3B(this->getValue()));
m_colorSprite->updateOpacity(this->getValue().a / 255.f);
auto enable = this->getSetting()->shouldEnable();
m_colorSprite->setOpacity(enable ? 255 : 155);
m_colorBtn->setEnabled(enable);
}
void Color4BSettingNodeV3::onSelectColor(CCObject*) {
auto popup = ColorPickPopup::create(this->getValue());
popup->setDelegate(this);
popup->show();
}
void Color4BSettingNodeV3::updateColor(ccColor4B const& color) {
this->setValue(color, nullptr);
}
Color4BSettingNodeV3* Color4BSettingNodeV3::create(std::shared_ptr<Color4BSettingV3> setting, float width) {
auto ret = new Color4BSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// UnresolvedCustomSettingNodeV3
bool UnresolvedCustomSettingNodeV3::init(std::string_view key, Mod* mod, float width) {
if (!SettingNodeV3::init(nullptr, width))
return false;
m_mod = mod;
this->setContentHeight(30);
auto label = CCLabelBMFont::create(
(mod && mod->isEnabled() ?
fmt::format("Missing setting '{}'", key) :
fmt::format("Enable the Mod to Edit '{}'", key)
).c_str(),
"bigFont.fnt"
);
label->setColor(mod && mod->isEnabled() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b);
label->limitLabelWidth(width - m_obContentSize.height, .3f, .1f);
this->addChildAtPosition(label, Anchor::Left, ccp(m_obContentSize.height / 2, 0), ccp(0, .5f));
return true;
}
void UnresolvedCustomSettingNodeV3::updateState(CCNode* invoker) {
SettingNodeV3::updateState(invoker);
this->getBG()->setColor(m_mod && m_mod->isEnabled() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b);
this->getBG()->setOpacity(75);
}
void UnresolvedCustomSettingNodeV3::onCommit() {}
bool UnresolvedCustomSettingNodeV3::hasUncommittedChanges() const {
return false;
}
bool UnresolvedCustomSettingNodeV3::hasNonDefaultValue() const {
return false;
}
void UnresolvedCustomSettingNodeV3::onResetToDefault() {}
UnresolvedCustomSettingNodeV3* UnresolvedCustomSettingNodeV3::create(std::string_view key, Mod* mod, float width) {
auto ret = new UnresolvedCustomSettingNodeV3();
if (ret && ret->init(key, mod, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// LegacyCustomSettingToV3Node
bool LegacyCustomSettingToV3Node::init(std::shared_ptr<LegacyCustomSettingV3> original, float width) {
if (!SettingNodeV3::init(original, width))
return false;
this->getNameMenu()->setVisible(false);
this->getButtonMenu()->setVisible(false);
m_original = original->getValue()->createNode(width);
m_original->setDelegate(this);
this->setContentSize({ width, m_original->getContentHeight() });
this->addChildAtPosition(m_original, Anchor::BottomLeft, ccp(0, 0), ccp(0, 0));
return true;
}
void LegacyCustomSettingToV3Node::settingValueChanged(SettingNode*) {
SettingNodeValueChangeEventV3(this, false).post();
}
void LegacyCustomSettingToV3Node::settingValueCommitted(SettingNode*) {
SettingNodeValueChangeEventV3(this, true).post();
}
void LegacyCustomSettingToV3Node::onCommit() {
m_original->commit();
}
bool LegacyCustomSettingToV3Node::hasUncommittedChanges() const {
return m_original->hasUncommittedChanges();
}
bool LegacyCustomSettingToV3Node::hasNonDefaultValue() const {
return m_original->hasNonDefaultValue();
}
void LegacyCustomSettingToV3Node::onResetToDefault() {
m_original->resetToDefault();
}
LegacyCustomSettingToV3Node* LegacyCustomSettingToV3Node::create(std::shared_ptr<LegacyCustomSettingV3> original, float width) {
auto ret = new LegacyCustomSettingToV3Node();
if (ret && ret->init(original, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -0,0 +1,336 @@
#pragma once
#include <Geode/loader/SettingV3.hpp>
#include <Geode/loader/SettingNode.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/ColorPickPopup.hpp>
using namespace geode::prelude;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !! If these classes are ever exposed in a public header, make sure to pimpl EVERYTHING! !!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
class TitleSettingNodeV3 : public SettingNodeV3 {
protected:
CCMenuItemToggler* m_collapseToggle;
bool init(std::shared_ptr<TitleSettingV3> setting, float width);
void onCommit() override;
void onCollapse(CCObject*);
public:
static TitleSettingNodeV3* create(std::shared_ptr<TitleSettingV3> setting, float width);
bool isCollapsed() const;
bool hasUncommittedChanges() const override;
bool hasNonDefaultValue() const override;
void onResetToDefault() override;
std::shared_ptr<TitleSettingV3> getSetting() const;
};
class BoolSettingNodeV3 : public SettingValueNodeV3<BoolSettingV3> {
protected:
CCMenuItemToggler* m_toggle;
bool init(std::shared_ptr<BoolSettingV3> setting, float width);
void updateState(CCNode* invoker) override;
void onToggle(CCObject*);
public:
static BoolSettingNodeV3* create(std::shared_ptr<BoolSettingV3> setting, float width);
};
template <class S>
class NumberSettingNodeV3 : public SettingValueNodeV3<S> {
protected:
using ValueType = typename S::ValueType;
using ValueAssignType = typename S::ValueAssignType;
TextInput* m_input;
Slider* m_slider;
CCMenuItemSpriteExtra* m_arrowLeftBtn;
CCMenuItemSpriteExtra* m_bigArrowLeftBtn;
CCMenuItemSpriteExtra* m_arrowRightBtn;
CCMenuItemSpriteExtra* m_bigArrowRightBtn;
CCSprite* m_arrowLeftBtnSpr;
CCSprite* m_bigArrowLeftBtnSpr;
CCSprite* m_arrowRightBtnSpr;
CCSprite* m_bigArrowRightBtnSpr;
float valueToSlider(ValueType value) {
auto min = this->getSetting()->getMinValue().value_or(-100);
auto max = this->getSetting()->getMaxValue().value_or(+100);
auto range = max - min;
return static_cast<float>(clamp(static_cast<double>(value - min) / range, 0.0, 1.0));
}
ValueType valueFromSlider(float num) {
auto min = this->getSetting()->getMinValue().value_or(-100);
auto max = this->getSetting()->getMaxValue().value_or(+100);
auto range = max - min;
auto value = static_cast<ValueType>(num * range + min);
auto step = this->getSetting()->getSliderSnap();
if (step > 0) {
value = static_cast<ValueType>(round(value / step) * step);
}
return value;
}
bool init(std::shared_ptr<S> setting, float width) {
if (!SettingValueNodeV3<S>::init(setting, width))
return false;
m_bigArrowLeftBtnSpr = CCSprite::create();
m_bigArrowLeftBtnSpr->setCascadeColorEnabled(true);
m_bigArrowLeftBtnSpr->setCascadeOpacityEnabled(true);
auto bigArrowLeftSpr1 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png");
auto bigArrowLeftSpr2 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png");
m_bigArrowLeftBtnSpr->setContentSize(bigArrowLeftSpr1->getContentSize() + ccp(20, 0));
m_bigArrowLeftBtnSpr->addChildAtPosition(bigArrowLeftSpr2, Anchor::Center, ccp(10, 0));
m_bigArrowLeftBtnSpr->addChildAtPosition(bigArrowLeftSpr1, Anchor::Center, ccp(-10, 0));
m_bigArrowLeftBtnSpr->setScale(.3f);
m_bigArrowLeftBtn = CCMenuItemSpriteExtra::create(
m_bigArrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow)
);
m_bigArrowLeftBtn->setUserObject(ObjWrapper<ValueType>::create(-setting->getBigArrowStepSize()));
m_bigArrowLeftBtn->setVisible(setting->isBigArrowsEnabled());
this->getButtonMenu()->addChildAtPosition(m_bigArrowLeftBtn, Anchor::Left, ccp(5, 0));
m_arrowLeftBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png");
m_arrowLeftBtnSpr->setScale(.5f);
m_arrowLeftBtn = CCMenuItemSpriteExtra::create(
m_arrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow)
);
m_arrowLeftBtn->setUserObject(ObjWrapper<ValueType>::create(-setting->getArrowStepSize()));
m_arrowLeftBtn->setVisible(setting->isArrowsEnabled());
this->getButtonMenu()->addChildAtPosition(m_arrowLeftBtn, Anchor::Left, ccp(22, 0));
m_input = TextInput::create(this->getButtonMenu()->getContentWidth() - 40, "Num");
m_input->setScale(.7f);
m_input->setCallback([this, setting](auto const& str) {
this->setValue(numFromString<ValueType>(str).unwrapOr(setting->getDefaultValue()), m_input);
});
if (!setting->isInputEnabled()) {
m_input->getBGSprite()->setVisible(false);
m_input->setEnabled(false);
m_input->getInputNode()->m_placeholderLabel->setOpacity(255);
m_input->getInputNode()->m_placeholderLabel->setColor(ccWHITE);
}
this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center);
m_arrowRightBtnSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png");
m_arrowRightBtnSpr->setFlipX(true);
m_arrowRightBtnSpr->setScale(.5f);
m_arrowRightBtn = CCMenuItemSpriteExtra::create(
m_arrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow)
);
m_arrowRightBtn->setUserObject(ObjWrapper<ValueType>::create(setting->getArrowStepSize()));
m_arrowRightBtn->setVisible(setting->isArrowsEnabled());
this->getButtonMenu()->addChildAtPosition(m_arrowRightBtn, Anchor::Right, ccp(-22, 0));
m_bigArrowRightBtnSpr = CCSprite::create();
m_bigArrowRightBtnSpr->setCascadeColorEnabled(true);
m_bigArrowRightBtnSpr->setCascadeOpacityEnabled(true);
auto bigArrowRightSpr1 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png");
bigArrowRightSpr1->setFlipX(true);
auto bigArrowRightSpr2 = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png");
bigArrowRightSpr2->setFlipX(true);
m_bigArrowRightBtnSpr->setContentSize(bigArrowRightSpr1->getContentSize() + ccp(20, 0));
m_bigArrowRightBtnSpr->addChildAtPosition(bigArrowRightSpr1, Anchor::Center, ccp(-10, 0));
m_bigArrowRightBtnSpr->addChildAtPosition(bigArrowRightSpr2, Anchor::Center, ccp(10, 0));
m_bigArrowRightBtnSpr->setScale(.3f);
m_bigArrowRightBtn = CCMenuItemSpriteExtra::create(
m_bigArrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow)
);
m_bigArrowRightBtn->setUserObject(ObjWrapper<ValueType>::create(setting->getBigArrowStepSize()));
m_bigArrowRightBtn->setVisible(setting->isBigArrowsEnabled());
this->getButtonMenu()->addChildAtPosition(m_bigArrowRightBtn, Anchor::Right, ccp(-5, 0));
if (setting->isSliderEnabled()) {
this->setContentHeight(45);
this->getButtonMenu()->updateAnchoredPosition(Anchor::Right, ccp(-10, 7));
m_slider = Slider::create(this, menu_selector(NumberSettingNodeV3::onSlider));
m_slider->setScale(.5f);
this->getButtonMenu()->addChildAtPosition(m_slider, Anchor::Center, ccp(0, -20), ccp(0, 0));
}
this->setValue(setting->getValue(), nullptr);
this->updateState(nullptr);
return true;
}
void updateState(CCNode* invoker) override {
SettingValueNodeV3<S>::updateState(invoker);
auto enable = this->getSetting()->shouldEnable();
if (this->getSetting()->isInputEnabled()) {
m_input->setEnabled(enable);
}
if (invoker != m_input) {
m_input->setString(numToString(this->getValue()));
}
auto min = this->getSetting()->getMinValue();
auto enableLeft = enable && (!min || this->getValue() > *min);
m_arrowLeftBtn->setEnabled(enableLeft);
m_bigArrowLeftBtn->setEnabled(enableLeft);
m_arrowLeftBtnSpr->setOpacity(enableLeft ? 255 : 155);
m_arrowLeftBtnSpr->setColor(enableLeft ? ccWHITE : ccGRAY);
m_bigArrowLeftBtnSpr->setOpacity(enableLeft ? 255 : 155);
m_bigArrowLeftBtnSpr->setColor(enableLeft ? ccWHITE : ccGRAY);
auto max = this->getSetting()->getMaxValue();
auto enableRight = enable && (!max || this->getValue() < *max);
m_arrowRightBtn->setEnabled(enableRight);
m_bigArrowRightBtn->setEnabled(enableRight);
m_arrowRightBtnSpr->setOpacity(enableRight ? 255 : 155);
m_arrowRightBtnSpr->setColor(enableRight ? ccWHITE : ccGRAY);
m_bigArrowRightBtnSpr->setOpacity(enableRight ? 255 : 155);
m_bigArrowRightBtnSpr->setColor(enableRight ? ccWHITE : ccGRAY);
if (m_slider) {
m_slider->m_touchLogic->m_thumb->setValue(this->valueToSlider(this->getValue()));
m_slider->updateBar();
m_slider->m_sliderBar->setColor(enable ? ccWHITE : ccGRAY);
m_slider->m_touchLogic->m_thumb->setColor(enable ? ccWHITE : ccGRAY);
m_slider->m_touchLogic->m_thumb->setEnabled(enable);
}
}
void onArrow(CCObject* sender) {
auto value = this->getValue() + static_cast<ObjWrapper<ValueType>*>(
static_cast<CCNode*>(sender)->getUserObject()
)->getValue();
if (auto min = this->getSetting()->getMinValue()) {
value = std::max(*min, value);
}
if (auto max = this->getSetting()->getMaxValue()) {
value = std::min(*max, value);
}
this->setValue(value, static_cast<CCNode*>(sender));
}
void onSlider(CCObject*) {
this->setValue(this->valueFromSlider(m_slider->m_touchLogic->m_thumb->getValue()), m_slider);
}
public:
static NumberSettingNodeV3* create(std::shared_ptr<S> setting, float width) {
auto ret = new NumberSettingNodeV3();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
using IntSettingNodeV3 = NumberSettingNodeV3<IntSettingV3>;
using FloatSettingNodeV3 = NumberSettingNodeV3<FloatSettingV3>;
class StringSettingNodeV3 : public SettingValueNodeV3<StringSettingV3> {
protected:
TextInput* m_input;
CCSprite* m_arrowLeftSpr = nullptr;
CCSprite* m_arrowRightSpr = nullptr;
bool init(std::shared_ptr<StringSettingV3> setting, float width);
void updateState(CCNode* invoker) override;
void onArrow(CCObject* sender);
public:
static StringSettingNodeV3* create(std::shared_ptr<StringSettingV3> setting, float width);
};
class FileSettingNodeV3 : public SettingValueNodeV3<FileSettingV3> {
protected:
CCSprite* m_fileIcon;
CCLabelBMFont* m_nameLabel;
EventListener<Task<Result<std::filesystem::path>>> m_pickListener;
CCMenuItemSpriteExtra* m_selectBtn;
CCSprite* m_selectBtnSpr;
bool init(std::shared_ptr<FileSettingV3> setting, float width);
void updateState(CCNode* invoker) override;
void onPickFile(CCObject*);
public:
static FileSettingNodeV3* create(std::shared_ptr<FileSettingV3> setting, float width);
};
class Color3BSettingNodeV3 : public SettingValueNodeV3<Color3BSettingV3>, public ColorPickPopupDelegate {
protected:
CCMenuItemSpriteExtra* m_colorBtn;
ColorChannelSprite* m_colorSprite;
bool init(std::shared_ptr<Color3BSettingV3> setting, float width);
void updateState(CCNode* invoker) override;
void onSelectColor(CCObject*);
void updateColor(ccColor4B const& color) override;
public:
static Color3BSettingNodeV3* create(std::shared_ptr<Color3BSettingV3> setting, float width);
};
class Color4BSettingNodeV3 : public SettingValueNodeV3<Color4BSettingV3>, public ColorPickPopupDelegate {
protected:
CCMenuItemSpriteExtra* m_colorBtn;
ColorChannelSprite* m_colorSprite;
bool init(std::shared_ptr<Color4BSettingV3> setting, float width);
void updateState(CCNode* invoker) override;
void onSelectColor(CCObject*);
void updateColor(ccColor4B const& color) override;
public:
static Color4BSettingNodeV3* create(std::shared_ptr<Color4BSettingV3> setting, float width);
};
class UnresolvedCustomSettingNodeV3 : public SettingNodeV3 {
protected:
Mod* m_mod;
bool init(std::string_view key, Mod* mod, float width);
void updateState(CCNode* invoker) override;
void onCommit() override;
public:
static UnresolvedCustomSettingNodeV3* create(std::string_view key, Mod* mod, float width);
bool hasUncommittedChanges() const override;
bool hasNonDefaultValue() const override;
void onResetToDefault() override;
};
// If these classes do get exposed in headers,
// LegacyCustomSettingToV3Node SHOULD NOT BE EXPOSED!!!!!! DO NOT DO THAT!!!!
class LegacyCustomSettingToV3Node : public SettingNodeV3, public SettingNodeDelegate {
protected:
SettingNode* m_original;
bool init(std::shared_ptr<LegacyCustomSettingV3> original, float width);
void onCommit() override;
void settingValueChanged(SettingNode*) override;
void settingValueCommitted(SettingNode*) override;
public:
static LegacyCustomSettingToV3Node* create(std::shared_ptr<LegacyCustomSettingV3> original, float width);
bool hasUncommittedChanges() const override;
bool hasNonDefaultValue() const override;
void onResetToDefault() override;
};

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
#import <Foundation/Foundation.h>
#include <Geode/loader/IPC.hpp>
#include <Geode/loader/Log.hpp>
#include <iostream>
@ -5,7 +6,6 @@
#include <loader/console.hpp>
#include <loader/IPC.hpp>
#include <loader/ModImpl.hpp>
#import <Foundation/Foundation.h>
#include <sys/stat.h>
#include <loader/LogImpl.hpp>

View file

@ -182,6 +182,7 @@ Result<> nfdPick(
}
if (options.defaultPath && options.defaultPath.value().wstring().size()) {
std::filesystem::path path = options.defaultPath.value();
path.make_preferred();
if (mode == NFDMode::OpenFile || mode == NFDMode::SaveFile) {
if (!std::filesystem::exists(path) || !std::filesystem::is_directory(path)) {
if (path.has_filename()) {

View file

@ -123,11 +123,13 @@ Task<Result<std::filesystem::path>> file::pick(PickMode mode, FilePickOptions co
Result<std::filesystem::path> result;
auto pickresult = nfdPick(nfdMode, options, &path);
if (pickresult.isErr()) {
result = Err(pickresult.err().value());
} else {
result = Ok(path);
if (pickresult.unwrapErr() == "Dialog cancelled") {
return RetTask::cancelled();
}
return RetTask::immediate(Err(pickresult.unwrapErr()));
} else {
return RetTask::immediate(Ok(path));
}
return RetTask::immediate(std::move(result));
}
Task<Result<std::vector<std::filesystem::path>>> file::pickMany(FilePickOptions const& options) {

View file

@ -18,6 +18,7 @@ $on_mod(Loaded) {
ColorProvider::get()->define("mod-list-updates-available-bg-2"_spr, { 45, 110, 222, 255 });
ColorProvider::get()->define("mod-list-errors-found"_spr, { 235, 35, 112, 255 });
ColorProvider::get()->define("mod-list-errors-found-2"_spr, { 245, 27, 27, 255 });
ColorProvider::get()->define("mod-list-gray"_spr, { 205, 205, 205, 255 });
ColorProvider::get()->define("mod-list-tab-deselected-bg"_spr, { 26, 24, 29, 255 });
ColorProvider::get()->define("mod-list-tab-selected-bg"_spr, { 168, 147, 185, 255 });
ColorProvider::get()->define("mod-list-tab-selected-bg-alt"_spr, { 147, 163, 185, 255 });

View file

@ -19,8 +19,8 @@ enum class GeodePopupStyle {
template <class... Args>
class GeodePopup : public Popup<Args...> {
protected:
bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default) {
const bool geodeTheme = Mod::get()->template getSettingValue<bool>("enable-geode-theme");
bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default, bool forceDisableTheme = false) {
const bool geodeTheme = !forceDisableTheme && Mod::get()->template getSettingValue<bool>("enable-geode-theme");
const char* bg;
switch (style) {
default:

View file

@ -90,6 +90,11 @@ bool ModsStatusNode::init() {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3* ev) {
this->updateState();
return ListenerResult::Propagate;
});
this->updateState();
return true;

View file

@ -12,6 +12,7 @@
#include "sources/ModListSource.hpp"
#include "UpdateModListState.hpp"
#include <server/DownloadManager.hpp>
#include <Geode/loader/SettingV3.hpp>
using namespace geode::prelude;
@ -39,6 +40,7 @@ protected:
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
DownloadState m_lastState = DownloadState::None;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
bool init();
void updateState();

View file

@ -298,6 +298,11 @@ bool ModItem::init(ModSource&& source) {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(server::ModDownloadFilter(m_source.getID()));
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) {
this->updateState();
return ListenerResult::Propagate;
});
return true;
}

View file

@ -35,6 +35,7 @@ protected:
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
std::optional<server::ServerModUpdate> m_availableUpdate;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
/**
* @warning Make sure `getMetadata` and `createModLogo` are callable

View file

@ -577,6 +577,7 @@ void ModList::updateState() {
filterSpr->setState(!isDefaultQuery);
auto clearSpr = static_cast<GeodeSquareSprite*>(m_clearFiltersBtn->getNormalImage());
m_clearFiltersBtn->setEnabled(!isDefaultQuery);
clearSpr->setColor(isDefaultQuery ? ccGRAY : ccWHITE);
clearSpr->setOpacity(isDefaultQuery ? 90 : 255);
clearSpr->getTopSprite()->setColor(isDefaultQuery ? ccGRAY : ccWHITE);

View file

@ -3,6 +3,7 @@
#include <Geode/ui/MDTextArea.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include "ConfirmUninstallPopup.hpp"
@ -557,6 +558,13 @@ bool ModPopup::setup(ModSource&& src) {
mainContainer->updateLayout();
m_mainLayer->addChildAtPosition(mainContainer, Anchor::Center);
m_settingsBG = CCScale9Sprite::create("square02b_001.png");
m_settingsBG->setColor({ 0, 0, 0 });
m_settingsBG->setOpacity(75);
m_settingsBG->setScale(.3f);
m_settingsBG->setContentSize(ccp(35, 30) / linksBG->getScale());
m_buttonMenu->addChildAtPosition(m_settingsBG, Anchor::BottomLeft, ccp(28, 25));
auto settingsSpr = createGeodeCircleButton(CCSprite::createWithSpriteFrameName("settings.png"_spr));
settingsSpr->setScale(.6f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
@ -601,15 +609,30 @@ bool ModPopup::setup(ModSource&& src) {
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(m_source.getID());
m_settingNodeListener.bind([this](SettingNodeValueChangeEventV3*) {
this->updateState();
return ListenerResult::Propagate;
});
return true;
}
void ModPopup::updateState() {
auto asMod = m_source.asMod();
auto wantsRestart = m_source.wantsRestart();
auto wantsRestartBecauseOfSettings = asMod && ModSettingsManager::from(asMod)->restartRequired();
m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0));
m_installBG->setOpacity(wantsRestart ? 40 : 75);
m_installBG->setColor((wantsRestart && !wantsRestartBecauseOfSettings) ?
to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) :
ccBLACK
);
m_installBG->setOpacity((wantsRestart && !wantsRestartBecauseOfSettings) ? 40 : 75);
m_settingsBG->setColor(wantsRestartBecauseOfSettings ?
to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) :
ccBLACK
);
m_settingsBG->setOpacity(wantsRestartBecauseOfSettings ? 40 : 75);
m_settingsBG->setVisible(wantsRestartBecauseOfSettings);
m_restartRequiredLabel->setVisible(wantsRestart);
if (!wantsRestart && asMod) {

View file

@ -30,6 +30,7 @@ protected:
CCMenuItemSpriteExtra* m_cancelBtn;
CCLabelBMFont* m_installStatusLabel;
CCScale9Sprite* m_installBG;
CCScale9Sprite* m_settingsBG;
CCLabelBMFont* m_enabledStatusLabel;
ButtonSprite* m_restartRequiredLabel;
CCNode* m_rightColumn;
@ -40,6 +41,7 @@ protected:
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_settingNodeListener;
bool setup(ModSource&& src) override;
void updateState();

View file

@ -1,12 +1,38 @@
#include "ModSettingsPopup.hpp"
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Setting.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/ui/General.hpp>
#include "GeodeSettingNode.hpp"
#include <loader/SettingNodeV3.hpp>
// needed for weightedFuzzyMatch
#include <ui/mods/sources/ModListSource.hpp>
static bool matchSearch(SettingNodeV3* node, std::string const& query) {
if (typeinfo_cast<TitleSettingNodeV3*>(node)) {
return true;
}
bool addToList = false;
auto setting = node->getSetting();
double weighted = 0;
if (auto name = setting->getName()) {
addToList |= weightedFuzzyMatch(setting->getKey(), query, 0.5, weighted);
addToList |= weightedFuzzyMatch(*name, query, 1, weighted);
}
// If there's no name, give full weight to key
else {
addToList |= weightedFuzzyMatch(setting->getKey(), query, 1, weighted);
}
if (auto desc = setting->getDescription()) {
addToList |= weightedFuzzyMatch(*desc, query, 0.02, weighted);
}
if (weighted < 2) {
addToList = false;
}
return addToList;
}
bool ModSettingsPopup::setup(Mod* mod) {
m_noElasticity = true;
@ -20,78 +46,95 @@ bool ModSettingsPopup::setup(Mod* mod) {
auto layerBG = CCLayerColor::create({ 0, 0, 0, 75 });
layerBG->setContentSize(layerSize);
m_mainLayer->addChildAtPosition(layerBG, Anchor::Center, -layerSize / 2);
layerBG->ignoreAnchorPointForPosition(false);
m_mainLayer->addChildAtPosition(layerBG, Anchor::Center);
auto layer = ScrollLayer::create(layerSize);
layer->setTouchEnabled(true);
auto searchContainer = CCMenu::create();
searchContainer->setContentSize({ layerSize.width, 30 });
m_searchInput = TextInput::create((layerSize.width - 15) / .7f - 40, "Search Settings...");
m_searchInput->setTextAlign(TextInputAlign::Left);
m_searchInput->setScale(.7f);
m_searchInput->setCallback([this](auto const&) {
this->updateState();
m_list->moveToTop();
});
m_searchInput->setID("search-input");
searchContainer->addChildAtPosition(m_searchInput, Anchor::Left, ccp(7.5f, 0), ccp(0, .5f));
auto searchClearSpr = GeodeSquareSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png");
searchClearSpr->setScale(.45f);
m_searchClearBtn = CCMenuItemSpriteExtra::create(
searchClearSpr, this, menu_selector(ModSettingsPopup::onClearSearch)
);
m_searchClearBtn->setID("clear-search-button");
searchContainer->addChildAtPosition(m_searchClearBtn, Anchor::Right, ccp(-20, 0));
layerBG->addChildAtPosition(searchContainer, Anchor::Top, ccp(0, 0), ccp(.5f, 1));
m_list = ScrollLayer::create(layerSize - ccp(0, searchContainer->getContentHeight()));
m_list->setTouchEnabled(true);
float totalHeight = .0f;
std::vector<CCNode*> rendered;
bool hasBG = true;
for (auto& key : mod->getSettingKeys()) {
SettingNode* node;
if (auto sett = mod->getSetting(key)) {
SettingNodeV3* node;
if (auto sett = mod->getSettingV3(key)) {
node = sett->createNode(layerSize.width);
}
else {
node = CustomSettingPlaceholderNode::create(key, layerSize.width);
}
node->setDelegate(this);
totalHeight += node->getScaledContentSize().height;
if (hasBG) {
auto bg = CCLayerColor::create({ 0, 0, 0, 50 });
bg->setContentSize(node->getScaledContentSize());
bg->setPosition(0.f, -totalHeight);
bg->setZOrder(-10);
layer->m_contentLayer->addChild(bg);
rendered.push_back(bg);
hasBG = false;
}
else {
hasBG = true;
node = UnresolvedCustomSettingNodeV3::create(key, mod, layerSize.width);
}
node->setPosition(0.f, -totalHeight);
layer->m_contentLayer->addChild(node);
// auto separator = CCLayerColor::create({ 0, 0, 0, 50 }, layerSize.width, 1.f);
// separator->setOpacity(bg ? 100 : 50);
// separator->ignoreAnchorPointForPosition(false);
// bg->addChildAtPosition(separator, Anchor::Bottom, ccp(0, 0), ccp(.5f, .5f));
auto separator = CCLayerColor::create(
{ 0, 0, 0, static_cast<GLubyte>(hasBG ? 100 : 50) }, layerSize.width, 1.f
);
separator->setPosition(0.f, -totalHeight);
layer->m_contentLayer->addChild(separator);
rendered.push_back(separator);
rendered.push_back(node);
m_settings.push_back(node);
m_list->m_contentLayer->addChild(node);
}
if (totalHeight < layerSize.height) {
totalHeight = layerSize.height;
}
for (auto& node : rendered) {
node->setPositionY(node->getPositionY() + totalHeight);
}
layer->m_contentLayer->setContentSize({ layerSize.width, totalHeight });
layer->moveToTop();
m_list->m_contentLayer->setLayout(
ColumnLayout::create()
->setAxisReverse(true)
->setAutoGrowAxis(layerSize.height)
->setCrossAxisOverflow(false)
->setAxisAlignment(AxisAlignment::End)
->setGap(0)
);
m_list->moveToTop();
layerBG->addChild(layer);
layerBG->addChildAtPosition(m_list, Anchor::BottomLeft);
// layer borders
m_mainLayer->addChildAtPosition(createGeodeListBorders({layerSize.width, layerSize.height - 2}), Anchor::Center);
m_mainLayer->addChildAtPosition(createGeodeListBorders(layerSize), Anchor::Center);
auto scrollBar = Scrollbar::create(m_list);
m_mainLayer->addChildAtPosition(
scrollBar, Anchor::Center, ccp(layerBG->getContentWidth() / 2 + 10, 0)
);
// buttons
m_applyMenu = CCMenu::create();
m_applyMenu->setContentWidth(150);
m_applyMenu->setLayout(RowLayout::create());
m_applyMenu->getLayout()->ignoreInvisibleChildren(true);
auto restartBtnSpr = createGeodeButton("Restart Now", true);
restartBtnSpr->setScale(.6f);
m_restartBtn = CCMenuItemSpriteExtra::create(
restartBtnSpr, this, menu_selector(ModSettingsPopup::onRestart)
);
m_applyMenu->addChildAtPosition(m_restartBtn, Anchor::Bottom, ccp(0, 20));
m_applyBtnSpr = createGeodeButton("Apply", true);
m_applyBtnSpr->setScale(.6f);
m_applyBtn = CCMenuItemSpriteExtra::create(
m_applyBtnSpr, this, menu_selector(ModSettingsPopup::onApply)
);
m_buttonMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20));
m_applyMenu->addChildAtPosition(m_applyBtn, Anchor::Bottom, ccp(0, 20));
m_mainLayer->addChildAtPosition(m_applyMenu, Anchor::Bottom, ccp(0, 20));
auto resetBtnSpr = createGeodeButton("Reset All", true);
resetBtnSpr->setScale(.6f);
@ -101,15 +144,34 @@ bool ModSettingsPopup::setup(Mod* mod) {
);
m_buttonMenu->addChildAtPosition(resetBtn, Anchor::BottomLeft, ccp(45, 20));
auto openDirBtnSpr = createGeodeButton("Open Folder", true);
openDirBtnSpr->setScale(.6f);
auto configFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png");
m_openConfigDirBtnSpr = createGeodeButton(configFolderSpr, "");
m_openConfigDirBtnSpr->setScale(.6f);
m_openConfigDirBtnSpr->getIcon()->setScale(m_openConfigDirBtnSpr->getIcon()->getScale() * 1.4f);
auto openConfigDirBtn = CCMenuItemSpriteExtra::create(
m_openConfigDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenConfigDirectory)
);
m_buttonMenu->addChildAtPosition(openConfigDirBtn, Anchor::BottomRight, ccp(-50, 20));
auto settingFolderSpr = CCSprite::createWithSpriteFrameName("folderIcon_001.png");
auto settingFolderSprSub = CCSprite::createWithSpriteFrameName("settings.png"_spr);
settingFolderSprSub->setColor(ccBLACK);
settingFolderSprSub->setOpacity(155);
settingFolderSprSub->setScale(.55f);
settingFolderSpr->addChildAtPosition(settingFolderSprSub, Anchor::Center, ccp(0, -3));
auto openDirBtnSpr = createGeodeButton(settingFolderSpr, "");
openDirBtnSpr->setScale(.6f);
openDirBtnSpr->getIcon()->setScale(openDirBtnSpr->getIcon()->getScale() * 1.4f);
auto openDirBtn = CCMenuItemSpriteExtra::create(
openDirBtnSpr, this, menu_selector(ModSettingsPopup::onOpenSaveDirectory)
);
m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-53, 20));
m_buttonMenu->addChildAtPosition(openDirBtn, Anchor::BottomRight, ccp(-20, 20));
this->settingValueChanged(nullptr);
m_changeListener.bind([this](auto* ev) {
this->updateState(ev->getNode());
return ListenerResult::Propagate;
});
this->updateState();
return true;
}
@ -126,7 +188,21 @@ void ModSettingsPopup::onApply(CCObject*) {
FLAlertLayer::create("Info", "No changes have been made.", "OK")->show();
}
}
void ModSettingsPopup::onRestart(CCObject*) {
// Update button state to let user know it's restarting but it might take a bit
m_restartBtn->setEnabled(false);
static_cast<ButtonSprite*>(m_restartBtn->getNormalImage())->setString("Restarting...");
m_restartBtn->updateSprite();
// Actually restart
Loader::get()->queueInMainThread([] {
// Delayed by 2 frames - one is needed to render the "Restarting text"
Loader::get()->queueInMainThread([] {
// the other never finishes rendering because the game actually restarts at this point
game::restart();
});
});
}
void ModSettingsPopup::onResetAll(CCObject*) {
createQuickPopup(
"Reset All",
@ -142,27 +218,89 @@ void ModSettingsPopup::onResetAll(CCObject*) {
}
);
}
void ModSettingsPopup::settingValueCommitted(SettingNode*) {
if (this->hasUncommitted()) {
m_applyBtnSpr->setColor({0xff, 0xff, 0xff});
m_applyBtn->setEnabled(true);
}
else {
m_applyBtnSpr->setColor({0x44, 0x44, 0x44});
m_applyBtn->setEnabled(false);
}
void ModSettingsPopup::onOpenSaveDirectory(CCObject*) {
file::openFolder(m_mod->getSaveDir());
}
void ModSettingsPopup::onOpenConfigDirectory(CCObject*) {
file::openFolder(m_mod->getConfigDir());
this->updateState();
}
void ModSettingsPopup::onClearSearch(CCObject*) {
m_searchInput->setString("");
this->updateState();
m_list->moveToTop();
}
void ModSettingsPopup::settingValueChanged(SettingNode*) {
void ModSettingsPopup::updateState(SettingNodeV3* invoker) {
auto search = m_searchInput->getString();
auto hasSearch = !search.empty();
m_restartBtn->setVisible(ModSettingsManager::from(m_mod)->restartRequired());
m_applyMenu->updateLayout();
auto configDirExists = std::filesystem::exists(m_mod->getConfigDir(false));
m_openConfigDirBtnSpr->setCascadeColorEnabled(true);
m_openConfigDirBtnSpr->setCascadeOpacityEnabled(true);
m_openConfigDirBtnSpr->setColor(configDirExists ? ccWHITE : ccGRAY);
m_openConfigDirBtnSpr->setOpacity(configDirExists ? 255 : 155);
auto listPosBefore = m_list->m_contentLayer->getPositionY();
auto listHeightBefore = m_list->m_contentLayer->getContentHeight();
// Update search visibility + all settings with "enable-if" schemes +
// checkerboard BG
TitleSettingNodeV3* lastTitle = nullptr;
bool bg = false;
for (auto& sett : m_settings) {
if (auto asTitle = typeinfo_cast<TitleSettingNodeV3*>(sett.data())) {
lastTitle = asTitle;
}
sett->removeFromParent();
if (
// Show if the setting is not a title and is not subject to a collapsed title
!(lastTitle && lastTitle != sett && lastTitle->isCollapsed()) &&
// Show if there's no search query or if the setting matches it
(!hasSearch || matchSearch(sett, search))
) {
m_list->m_contentLayer->addChild(sett);
sett->setDefaultBGColor(ccc4(0, 0, 0, bg ? 60 : 20));
bg = !bg;
}
// Avoid infinite loops
if (sett == invoker) {
continue;
}
if (sett->getSetting() && sett->getSetting()->getEnableIf()) {
sett->updateState(nullptr);
}
}
m_list->m_contentLayer->updateLayout();
// Preserve relative list position if something has been collapsed
m_list->m_contentLayer->setPositionY(
listPosBefore +
(listHeightBefore - m_list->m_contentLayer->getContentHeight())
);
m_applyBtnSpr->setCascadeColorEnabled(true);
m_applyBtnSpr->setCascadeOpacityEnabled(true);
if (this->hasUncommitted()) {
m_applyBtnSpr->setColor({0xff, 0xff, 0xff});
m_applyBtnSpr->setColor(ccWHITE);
m_applyBtnSpr->setOpacity(255);
m_applyBtn->setEnabled(true);
}
else {
m_applyBtnSpr->setColor({0x44, 0x44, 0x44});
m_applyBtnSpr->setColor(ccGRAY);
m_applyBtnSpr->setOpacity(155);
m_applyBtn->setEnabled(false);
}
auto clearSpr = static_cast<GeodeSquareSprite*>(m_searchClearBtn->getNormalImage());
m_searchClearBtn->setEnabled(hasSearch);
clearSpr->setColor(hasSearch ? ccWHITE : ccGRAY);
clearSpr->setOpacity(hasSearch ? 255 : 90);
clearSpr->getTopSprite()->setColor(hasSearch ? ccWHITE : ccGRAY);
clearSpr->getTopSprite()->setOpacity(hasSearch ? 255 : 90);
}
bool ModSettingsPopup::hasUncommitted() const {
@ -175,28 +313,26 @@ bool ModSettingsPopup::hasUncommitted() const {
}
void ModSettingsPopup::onClose(CCObject* sender) {
if (sender && this->hasUncommitted()) {
if (this->hasUncommitted()) {
createQuickPopup(
"Unsaved Changes",
"You have <cr>unsaved changes</c>! Are you sure you "
"want to exit?",
"Cancel", "Discard",
[this](FLAlertLayer*, bool btn2) {
if (btn2) this->onClose(nullptr);
if (btn2) {
GeodePopup::onClose(nullptr);
}
}
);
return;
}
Popup<Mod*>::onClose(sender);
}
void ModSettingsPopup::onOpenSaveDirectory(CCObject*) {
file::openFolder(m_mod->getSaveDir());
GeodePopup::onClose(sender);
}
ModSettingsPopup* ModSettingsPopup::create(Mod* mod) {
auto ret = new ModSettingsPopup();
if (ret->init(440.f, 280.f, mod)) {
if (ret->init(440, 280, mod)) {
ret->autorelease();
return ret;
}

View file

@ -1,28 +1,36 @@
#pragma once
#include <Geode/loader/SettingNode.hpp>
#include <Geode/loader/SettingV3.hpp>
#include <Geode/ui/Popup.hpp>
#include <Geode/utils/cocos.hpp>
#include "../GeodeStyle.hpp"
using namespace geode::prelude;
class ModSettingsPopup : public GeodePopup<Mod*>, public SettingNodeDelegate {
class ModSettingsPopup : public GeodePopup<Mod*> {
protected:
Mod* m_mod;
std::vector<SettingNode*> m_settings;
ScrollLayer* m_list;
std::vector<Ref<SettingNodeV3>> m_settings;
CCMenu* m_applyMenu;
CCMenuItemSpriteExtra* m_applyBtn;
CCMenuItemSpriteExtra* m_restartBtn;
ButtonSprite* m_applyBtnSpr;
void settingValueChanged(SettingNode*) override;
void settingValueCommitted(SettingNode*) override;
IconButtonSprite* m_openConfigDirBtnSpr;
TextInput* m_searchInput;
CCMenuItemSpriteExtra* m_searchClearBtn;
EventListener<EventFilter<SettingNodeValueChangeEventV3>> m_changeListener;
bool setup(Mod* mod) override;
void updateState(SettingNodeV3* invoker = nullptr);
bool hasUncommitted() const;
void onClose(CCObject*) override;
void onApply(CCObject*);
void onRestart(CCObject*);
void onResetAll(CCObject*);
void onOpenSaveDirectory(CCObject*);
void onOpenConfigDirectory(CCObject*);
void onClearSearch(CCObject*);
public:
static ModSettingsPopup* create(Mod* mod);

View file

@ -1,5 +1,6 @@
#include "ModListSource.hpp"
#include <server/DownloadManager.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#define FTS_FUZZY_MATCH_IMPLEMENTATION
#include <Geode/external/fts/fts_fuzzy_match.h>
@ -90,6 +91,9 @@ bool ModListSource::isRestartRequired() {
if (mod->getRequestedAction() != ModRequestedAction::None) {
return true;
}
if (ModSettingsManager::from(mod)->restartRequired()) {
return true;
}
}
if (server::ModDownloadManager::get()->wantsRestart()) {
return true;

View file

@ -1,6 +1,7 @@
#include "ModSource.hpp"
#include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/ModSettingsManager.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <server/DownloadManager.hpp>
#include <Geode/binding/GameObject.hpp>
@ -106,7 +107,8 @@ bool ModSource::wantsRestart() const {
}
return std::visit(makeVisitor {
[](Mod* mod) {
return mod->getRequestedAction() != ModRequestedAction::None;
return mod->getRequestedAction() != ModRequestedAction::None ||
ModSettingsManager::from(mod)->restartRequired();
},
[](server::ServerModMetadata const& metdata) {
return false;

View file

@ -10,6 +10,55 @@
using namespace geode::prelude;
// class ColorPickPopupEvent::Impl final {
// public:
// ColorPickPopup* popup;
// ccColor4B color;
// bool closed = false;
// };
// ColorPickPopupEvent::ColorPickPopupEvent(ColorPickPopup* popup, ccColor4B const& color)
// : m_impl(std::make_shared<Impl>())
// {
// m_impl->popup = popup;
// m_impl->color = color;
// }
// ColorPickPopupEvent::~ColorPickPopupEvent() = default;
// ColorPickPopup* ColorPickPopupEvent::getPopup() const {
// return m_impl->popup;
// }
// ccColor4B ColorPickPopupEvent::getColor() const {
// return m_impl->color;
// }
// bool ColorPickPopupEvent::isPopupClosed() const {
// return m_impl->closed;
// }
// class ColorPickPopupEventFilter::Impl final {
// public:
// ColorPickPopup* popup;
// };
// ListenerResult ColorPickPopupEventFilter::handle(utils::MiniFunction<Callback> fn, ColorPickPopupEvent* event) {
// if (event->getPopup() == m_impl->popup) {
// if (event->isPopupClosed()) {
// m_impl->popup = nullptr;
// }
// else {
// fn(event);
// }
// }
// return ListenerResult::Propagate;
// }
// ColorPickPopupEventFilter::ColorPickPopupEventFilter() : ColorPickPopupEventFilter(nullptr) {}
// ColorPickPopupEventFilter::ColorPickPopupEventFilter(ColorPickPopup* popup)
// : m_impl(std::make_shared<Impl>())
// {
// m_impl->popup = popup;
// }
// ColorPickPopupEventFilter::~ColorPickPopupEventFilter() = default;
bool ColorPickPopup::setup(ccColor4B const& color, bool isRGBA) {
m_noElasticity = true;
m_color = color;
@ -334,7 +383,9 @@ void ColorPickPopup::updateState(CCNode* except) {
}
m_resetBtn->setVisible(m_originalColor != m_color);
m_newColorSpr->setColor(to3B(m_color));
if (m_delegate) m_delegate->updateColor(m_color);
if (m_delegate) {
m_delegate->updateColor(m_color);
}
}
void ColorPickPopup::onOpacitySlider(CCObject* sender) {

View file

@ -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<JsonMaybeSomething*>(this);
}
// template<class Json>
// template<nlohmann::detail::value_t T>
// 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<value_t::Array>();
return *this;
}
// template<class Json>
// template<nlohmann::detail::value_t... T>
// 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<class Json>
// template<nlohmann::detail::value_t T>
// JsonMaybeValue JsonMaybeValue::is() {
// if (this->isError()) return *this;
// self().m_hasValue = jsonConvertibleTo(self().m_json.type(), T);
// m_inferType = false;
// return *this;
// }
// template<class Json>
// template<class T>
// JsonMaybeValue JsonMaybeValue::validate(JsonValueValidator<T> validator) {
// if (this->isError()) return *this;
// try {
// if (!validator(self().m_json.template get<T>())) {
// 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<class Json>
// template<class T>
// JsonMaybeValue JsonMaybeValue::inferType() {
// if (this->isError() || !m_inferType) return *this;
// return this->as<getJsonType<T>()>();
// }
// template<class Json>
// template<class T>
// JsonMaybeValue JsonMaybeValue::intoRaw(T& target) {
// if (this->isError()) return *this;
// target = self().m_json;
// return *this;
// }
// template<class Json>
// template<class T>
// JsonMaybeValue JsonMaybeValue::into(T& target) {
// return this->intoAs<T, T>(target);
// }
// template<class Json>
// template<class T>
// JsonMaybeValue JsonMaybeValue::into(std::optional<T>& target) {
// return this->intoAs<T, std::optional<T>>(target);
// }
// template<class Json>
// template<class A, class T>
// JsonMaybeValue JsonMaybeValue::intoAs(T& target) {
// this->inferType<A>();
// if (this->isError()) return *this;
// try {
// target = self().m_json.template get<A>();
// } catch(...) {
// this->setError(
// self().m_hierarchy + ": Invalid type \"" +
// std::string(self().m_json.type_name()) + "\""
// );
// }
// return *this;
// }
// template<class Json>
// template<class T>
// T JsonMaybeValue::get() {
// this->inferType<T>();
// if (this->isError()) return T();
// try {
// return self().m_json.template get<T>();
// } catch(...) {
// this->setError(
// self().m_hierarchy + ": Invalid type to get \"" +
// std::string(self().m_json.type_name()) + "\""
// );
// }
// return T();
// }
JsonMaybeObject JsonMaybeValue::obj() {
this->as<value_t::Object>();
return JsonMaybeObject(self().m_checker, self().m_json, self().m_hierarchy, self().m_hasValue);
}
// template<class Json>
// template<class T>
// struct JsonMaybeValue::Iterator {
// std::vector<T> m_values;
// using iterator = typename std::vector<T>::iterator;
// using const_iterator = typename std::vector<T>::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<value_t::Array>();
if (this->isError()) return *this;
@ -216,7 +65,6 @@ JsonMaybeValue JsonMaybeValue::at(size_t i) {
);
}
typename JsonMaybeValue::template Iterator<JsonMaybeValue> JsonMaybeValue::iterate() {
this->as<value_t::Array>();
Iterator<JsonMaybeValue> iter;
@ -232,7 +80,6 @@ typename JsonMaybeValue::template Iterator<JsonMaybeValue> JsonMaybeValue::itera
return iter;
}
typename JsonMaybeValue::template Iterator<std::pair<std::string, JsonMaybeValue>> JsonMaybeValue::items() {
this->as<value_t::Object>();
Iterator<std::pair<std::string, JsonMaybeValue>> iter;
@ -248,33 +95,26 @@ typename JsonMaybeValue::template Iterator<std::pair<std::string, JsonMaybeValue
return iter;
}
JsonMaybeObject::JsonMaybeObject(
JsonChecker& checker, matjson::Value& json, std::string const& hierarchy, bool hasValue
) :
JsonMaybeSomething(checker, json, hierarchy, hasValue) {}
) : JsonMaybeSomething(checker, json, hierarchy, hasValue) {}
JsonMaybeSomething& JsonMaybeObject::self() {
return *static_cast<JsonMaybeSomething*>(this);
}
void JsonMaybeObject::addKnownKey(std::string const& key) {
m_knownKeys.insert(key);
}
matjson::Value& JsonMaybeObject::json() {
return self().m_json;
}
JsonMaybeValue JsonMaybeObject::emptyValue() {
return JsonMaybeValue(self().m_checker, self().m_json, "", false);
}
JsonMaybeValue JsonMaybeObject::has(std::string const& key) {
this->addKnownKey(key);
if (this->isError()) return emptyValue();
@ -284,7 +124,6 @@ JsonMaybeValue JsonMaybeObject::has(std::string const& key) {
return JsonMaybeValue(self().m_checker, self().m_json[key], key, true);
}
JsonMaybeValue JsonMaybeObject::needs(std::string const& key) {
this->addKnownKey(key);
if (this->isError()) return emptyValue();
@ -295,7 +134,6 @@ JsonMaybeValue JsonMaybeObject::needs(std::string const& key) {
return JsonMaybeValue(self().m_checker, self().m_json[key], key, true);
}
// TODO: gross hack :3 (ctrl+f this comment to find the other part)
extern bool s_jsonCheckerShouldCheckUnknownKeys;
bool s_jsonCheckerShouldCheckUnknownKeys = true;
@ -309,20 +147,274 @@ void JsonMaybeObject::checkUnknownKeys() {
}
}
JsonChecker::JsonChecker(matjson::Value& json) : m_json(json), m_result(std::monostate()) {}
bool JsonChecker::isError() const {
return std::holds_alternative<std::string>(m_result);
}
std::string JsonChecker::getError() const {
return std::get<std::string>(m_result);
}
JsonMaybeValue JsonChecker::root(std::string const& hierarchy) {
return JsonMaybeValue(*this, m_json, hierarchy, true);
}
// This is used for null JsonExpectedValues (for example when doing
// `json.has("key")` where "key" doesn't exist)
static matjson::Value NULL_SCOPED_VALUE = nullptr;
class JsonExpectedValue::Impl final {
public:
// Values shared between JsonExpectedValues related to the same JSON
struct Shared final {
matjson::Value originalJson;
std::optional<std::string> 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> shared;
matjson::Value& scope;
std::string scopeName;
std::string key;
std::unordered_set<std::string> knownKeys;
Impl()
: shared(nullptr),
scope(NULL_SCOPED_VALUE)
{}
// Create a root Impl
Impl(std::shared_ptr<Shared> 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<Impl>())
{}
JsonExpectedValue::JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key)
: m_impl(std::make_unique<Impl>(from, scope, key))
{}
JsonExpectedValue::JsonExpectedValue(matjson::Value const& json, std::string_view rootScopeName)
: m_impl(std::make_unique<Impl>(std::make_shared<Impl::Shared>(json, rootScopeName)))
{}
JsonExpectedValue::~JsonExpectedValue() {}
JsonExpectedValue::JsonExpectedValue(JsonExpectedValue&&) = default;
JsonExpectedValue& JsonExpectedValue::operator=(JsonExpectedValue&&) = default;
const char* JsonExpectedValue::matJsonTypeToString(matjson::Type ty) {
switch (ty) {
case matjson::Type::Null: return "null";
case matjson::Type::Bool: return "bool";
case matjson::Type::Number: return "number";
case matjson::Type::String: return "string";
case matjson::Type::Array: return "array";
case matjson::Type::Object: return "object";
default: return "unknown";
}
}
matjson::Value const& JsonExpectedValue::getJSONRef() const {
return m_impl->scope;
}
matjson::Value JsonExpectedValue::json() const {
return m_impl->scope;
}
std::string JsonExpectedValue::key() const {
return m_impl->key;
}
bool JsonExpectedValue::hasError() const {
return !m_impl->shared || m_impl->shared->error.has_value();
}
void JsonExpectedValue::setError(std::string_view error) {
m_impl->shared->error.emplace(fmt::format("[{}]: {}", m_impl->scopeName, error));
}
bool JsonExpectedValue::is(matjson::Type type) const {
if (this->hasError()) return false;
return m_impl->scope.type() == type;
}
bool JsonExpectedValue::isNull() const {
return this->is(matjson::Type::Null);
}
bool JsonExpectedValue::isBool() const {
return this->is(matjson::Type::Bool);
}
bool JsonExpectedValue::isNumber() const {
return this->is(matjson::Type::Number);
}
bool JsonExpectedValue::isString() const {
return this->is(matjson::Type::String);
}
bool JsonExpectedValue::isArray() const {
return this->is(matjson::Type::Array);
}
bool JsonExpectedValue::isObject() const {
return this->is(matjson::Type::Object);
}
JsonExpectedValue& JsonExpectedValue::assertIs(matjson::Type type) {
if (this->hasError()) return *this;
if (m_impl->scope.type() != type) {
this->setError(
"invalid type {}, expected {}",
matJsonTypeToString(m_impl->scope.type()),
matJsonTypeToString(type)
);
}
return *this;
}
JsonExpectedValue& JsonExpectedValue::assertIsNull() {
return this->assertIs(matjson::Type::Null);
}
JsonExpectedValue& JsonExpectedValue::assertIsBool() {
return this->assertIs(matjson::Type::Bool);
}
JsonExpectedValue& JsonExpectedValue::assertIsNumber() {
return this->assertIs(matjson::Type::Number);
}
JsonExpectedValue& JsonExpectedValue::assertIsString() {
return this->assertIs(matjson::Type::String);
}
JsonExpectedValue& JsonExpectedValue::assertIsArray() {
return this->assertIs(matjson::Type::Array);
}
JsonExpectedValue& JsonExpectedValue::assertIsObject() {
return this->assertIs(matjson::Type::Object);
}
JsonExpectedValue& JsonExpectedValue::assertIs(std::initializer_list<matjson::Type> 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<std::vector<std::string>>(types, [](matjson::Type t) {
return matJsonTypeToString(t);
}), " or ")
);
}
return *this;
}
JsonExpectedValue JsonExpectedValue::has(std::string_view key) {
if (this->hasError()) {
return JsonExpectedValue();
}
if (!this->assertIs(matjson::Type::Object)) {
return JsonExpectedValue();
}
m_impl->knownKeys.insert(std::string(key));
if (!m_impl->scope.contains(key)) {
return JsonExpectedValue();
}
return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key);
}
JsonExpectedValue JsonExpectedValue::needs(std::string_view key) {
if (this->hasError()) {
return JsonExpectedValue();
}
if (!this->assertIs(matjson::Type::Object)) {
return JsonExpectedValue();
}
m_impl->knownKeys.insert(std::string(key));
if (!m_impl->scope.contains(key)) {
this->setError("missing required key {}", key);
return JsonExpectedValue();
}
return JsonExpectedValue(m_impl.get(), m_impl->scope[key], key);
}
std::vector<std::pair<std::string, JsonExpectedValue>> JsonExpectedValue::properties() {
if (this->hasError()) {
return std::vector<std::pair<std::string, JsonExpectedValue>>();
}
if (!this->assertIs(matjson::Type::Object)) {
return std::vector<std::pair<std::string, JsonExpectedValue>>();
}
std::vector<std::pair<std::string, JsonExpectedValue>> res;
for (auto& [k, v] : m_impl->scope.as_object()) {
res.push_back(std::make_pair(k, JsonExpectedValue(m_impl.get(), v, k)));
}
return res;
}
void JsonExpectedValue::checkUnknownKeys() {
if (this->hasError()) return;
for (auto&& [key, _] : this->properties()) {
if (!m_impl->knownKeys.contains(key)) {
log::warn("{} contains unknown key \"{}\"", m_impl->scopeName, key);
}
}
}
size_t JsonExpectedValue::length() {
if (this->hasError()) {
return 0;
}
if (!this->assertIs(matjson::Type::Array)) {
return 0;
}
return m_impl->scope.as_array().size();
}
JsonExpectedValue JsonExpectedValue::at(size_t index) {
if (this->hasError()) {
return JsonExpectedValue();
}
if (!this->assertIs(matjson::Type::Array)) {
return JsonExpectedValue();
}
auto& arr = m_impl->scope.as_array();
if (arr.size() <= index) {
this->setError(
"array expected to have at least size {}, but its size was only {}",
index + 1, arr.size()
);
return JsonExpectedValue();
}
return JsonExpectedValue(m_impl.get(), arr.at(index), std::to_string(index));
}
std::vector<JsonExpectedValue> JsonExpectedValue::items() {
if (this->hasError()) {
return std::vector<JsonExpectedValue>();
}
if (!this->assertIs(matjson::Type::Array)) {
return std::vector<JsonExpectedValue>();
}
std::vector<JsonExpectedValue> res;
size_t i = 0;
for (auto& v : m_impl->scope.as_array()) {
res.push_back(JsonExpectedValue(m_impl.get(), v, std::to_string(i++)));
}
return res;
}
JsonExpectedValue::operator bool() const {
// The shared check is because null values should evaluate to false so `obj.has("key")` evaluates to false
return m_impl->shared && !this->hasError();
}
Result<> JsonExpectedValue::ok() {
if (m_impl->shared && m_impl->shared->error) {
return Err(*m_impl->shared->error);
}
return Ok();
}
JsonExpectedValue geode::checkJson(matjson::Value const& json, std::string_view rootScopeName) {
return JsonExpectedValue(json, rootScopeName);
}

View file

@ -5,6 +5,12 @@
using namespace geode::prelude;
PlatformID PlatformID::from(const char* str) {
// todo in v4: this should just be
// "win" -> Windows
// "mac", "mac-intel", "mac-arm" -> Mac
// "ios" -> iOS
// "android", "android32", "android64" -> Android
// no linux
switch (hash(str)) {
case hash("win"):
case hash("Windows"):
@ -33,29 +39,38 @@ PlatformID PlatformID::from(const char* str) {
}
bool PlatformID::coveredBy(const char* str, PlatformID t) {
switch (hash(str)) {
case hash("win"): return t == PlatformID::Windows;
case hash("mac"): return t == PlatformID::MacIntel || t == PlatformID::MacArm;
case hash("mac-intel"): return t == PlatformID::MacIntel;
case hash("mac-arm"): return t == PlatformID::MacArm;
case hash("ios"): return t == PlatformID::iOS;
case hash("android"): return t == PlatformID::Android32 || t == PlatformID::Android64;
case hash("android32"): return t == PlatformID::Android32;
case hash("android64"): return t == PlatformID::Android64;
case hash("linux"): return t == PlatformID::Linux;
default: return false;
}
// todo in v4: this is ridiculously inefficient currently - in v4 just use a flag check!
return ranges::contains(getCovered(str), t);
}
bool PlatformID::coveredBy(std::string const& str, PlatformID t) {
return PlatformID::coveredBy(str.c_str(), t);
}
std::vector<PlatformID> PlatformID::getCovered(std::string_view str) {
switch (hash(str)) {
case hash("desktop"): return { PlatformID::Windows, PlatformID::MacArm, PlatformID::MacIntel };
case hash("mobile"): return { PlatformID::iOS, PlatformID::Android32, PlatformID::Android64 };
case hash("win"): return { PlatformID::Windows };
case hash("mac"): return { PlatformID::MacIntel, PlatformID::MacArm };
case hash("mac-intel"): return { PlatformID::MacIntel };
case hash("mac-arm"): return { PlatformID::MacArm };
case hash("ios"): return { PlatformID::iOS };
case hash("android"): return { PlatformID::Android32, PlatformID::Android64 };
case hash("android32"): return { PlatformID::Android32 };
case hash("android64"): return { PlatformID::Android64 };
// todo in v4: no linux
case hash("linux"): return { PlatformID::Linux };
default: return {};
}
}
PlatformID PlatformID::from(std::string const& str) {
return PlatformID::from(str.c_str());
}