mirror of
https://github.com/geode-sdk/geode.git
synced 2024-11-25 17:07:58 -05:00
commit
67814ece83
51 changed files with 4612 additions and 548 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
|||
# Geode Changelog
|
||||
|
||||
## v3.6.0
|
||||
* Major rework of the entire settings system with lots of new features; see the [docs page](https://docs.geode-sdk.org/mods/settings) for more
|
||||
* Rework JSON validation; now uses the `JsonExpectedValue` class with the `checkJson` helper (89d1a51)
|
||||
* Add `Task::cancelled` for creating immediately cancelled Tasks (1a82d12)
|
||||
* Add function type utilities in `utils/function.hpp` (659c168)
|
||||
* Add `typeinfo_pointer_cast` for casting `std::shared_ptr`s (28cc6fd)
|
||||
* Add `GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH` (1032d9a)
|
||||
* Add `PlatformID::getCovered` (d5718be)
|
||||
* Rename `toByteArray` to `toBytes` (6eb0797)
|
||||
* Improve `AxisLayout::getSizeHint` (85e7b5e)
|
||||
* Fix issues with file dialogs on Windows (62b6241, 971e3fb)
|
||||
* Mod incompatibilities may now be platform-specific (9f1c70a)
|
||||
|
||||
## v3.5.0
|
||||
* Move CCLighting to cocos headers (#1036)
|
||||
* Add new `gd::string` constructor (bae22b4)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
3.5.0
|
||||
3.6.0
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
56
loader/include/Geode/loader/ModSettingsManager.hpp
Normal file
56
loader/include/Geode/loader/ModSettingsManager.hpp
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
822
loader/include/Geode/loader/SettingV3.hpp
Normal file
822
loader/include/Geode/loader/SettingV3.hpp
Normal 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()
|
||||
);
|
||||
}
|
|
@ -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__
|
||||
|
|
|
@ -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,
|
||||
Unknown = -1,
|
||||
Windows = 0,
|
||||
MacIntel = 1,
|
||||
MacArm = 2,
|
||||
iOS = 3,
|
||||
Android32 = 4,
|
||||
Android64 = 5,
|
||||
Linux = 6,
|
||||
};
|
||||
|
||||
using Type = decltype(Unknown);
|
||||
|
@ -171,7 +173,14 @@ namespace geode {
|
|||
*/
|
||||
static GEODE_DLL bool coveredBy(const char* str, PlatformID t);
|
||||
static GEODE_DLL bool coveredBy(std::string const& str, PlatformID t);
|
||||
/**
|
||||
* Returns the list of platforms covered by this string name. For
|
||||
* example, "android" would return both Android32 and Android64
|
||||
* todo in v4: deprecate this as the flagged version deals with this
|
||||
*/
|
||||
static GEODE_DLL std::vector<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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <typeinfo>
|
||||
#include <memory>
|
||||
|
||||
namespace geode {
|
||||
struct PlatformInfo {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
59
loader/include/Geode/utils/function.hpp
Normal file
59
loader/include/Geode/utils/function.hpp
Normal 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>;
|
||||
}
|
|
@ -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
BIN
loader/resources/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 764 B |
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
253
loader/src/loader/ModSettingsManager.cpp
Normal file
253
loader/src/loader/ModSettingsManager.cpp
Normal 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;
|
||||
}
|
|
@ -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,29 +297,49 @@ 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())); \
|
||||
} \
|
||||
return Ok(); \
|
||||
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(); \
|
||||
} \
|
||||
template<> \
|
||||
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(); \
|
||||
} \
|
||||
template<> \
|
||||
|
@ -328,9 +349,12 @@ 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); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define IMPL_TO_VALID(type_) \
|
||||
|
|
712
loader/src/loader/SettingNodeV3.cpp
Normal file
712
loader/src/loader/SettingNodeV3.cpp
Normal 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;
|
||||
}
|
336
loader/src/loader/SettingNodeV3.hpp
Normal file
336
loader/src/loader/SettingNodeV3.hpp
Normal 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;
|
||||
};
|
1220
loader/src/loader/SettingV3.cpp
Normal file
1220
loader/src/loader/SettingV3.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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());
|
||||
if (pickresult.unwrapErr() == "Dialog cancelled") {
|
||||
return RetTask::cancelled();
|
||||
}
|
||||
return RetTask::immediate(Err(pickresult.unwrapErr()));
|
||||
} else {
|
||||
result = Ok(path);
|
||||
return RetTask::immediate(Ok(path));
|
||||
}
|
||||
return RetTask::immediate(std::move(result));
|
||||
}
|
||||
|
||||
Task<Result<std::vector<std::filesystem::path>>> file::pickMany(FilePickOptions const& options) {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue