rework settings

- Settings no longer abuse templates
 - Custom settings are now supported (finally)
 - Bumped version to v1.0.0-alpha
This commit is contained in:
HJfod 2022-12-13 22:39:45 +02:00
parent b1e0276a7e
commit 9ffb15b616
31 changed files with 1509 additions and 1158 deletions

View file

@ -1,9 +1,22 @@
cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build libraries static" FORCE)
# Read version
file(READ VERSION GEODE_VERSION)
string(STRIP "${GEODE_VERSION}" GEODE_VERSION)
# Check if version has a tag like v1.0.0-alpha
string(FIND ${GEODE_VERSION} "-" GEODE_VERSION_HAS_TAG)
if (GEODE_VERSION_HAS_TAG)
string(REGEX MATCH "[a-z]+[0-9]?$" GEODE_VERSION_TAG ${GEODE_VERSION})
string(SUBSTRING "${GEODE_VERSION}" 0 ${GEODE_VERSION_HAS_TAG} GEODE_VERSION)
else()
set(GEODE_VERSION_TAG "")
endif()
message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG}")
project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 20)

View file

@ -1 +1 @@
0.6.1
1.0.0-alpha

View file

@ -89,10 +89,6 @@ namespace geode {
this->enable();
}
// todo: maybe add these?
EventListener(EventListener const& other) = delete;
EventListener(EventListener&& other) = delete;
void bind(std::function<Callback> fn) {
m_callback = fn;
}

View file

@ -18,19 +18,6 @@
#include <vector>
namespace geode {
template <class T>
struct HandleToSaved : public T {
Mod* m_mod;
std::string m_key;
HandleToSaved(std::string const& key, Mod* mod, T const& value) :
T(value), m_key(key), m_mod(mod) {}
HandleToSaved(HandleToSaved const&) = delete;
HandleToSaved(HandleToSaved&&) = delete;
~HandleToSaved();
};
/**
* @class Mod
* Represents a Mod ingame.
@ -82,6 +69,14 @@ namespace geode {
* Saved values
*/
nlohmann::json m_saved;
/**
* Setting values
*/
std::unordered_map<std::string, std::unique_ptr<SettingValue>> m_settings;
/**
* Settings save data. Stored for efficient loading of custom settings
*/
nlohmann::json m_savedSettingsData;
/**
* Load the platform binary
@ -90,6 +85,8 @@ namespace geode {
Result<> unloadPlatformBinary();
Result<> createTempDir();
void setupSettings();
// no copying
Mod(Mod const&) = delete;
Mod operator=(Mod const&) = delete;
@ -146,25 +143,31 @@ namespace geode {
ghc::filesystem::path getConfigDir(bool create = true) const;
bool hasSettings() const;
decltype(ModInfo::settings) getSettings() const;
std::vector<std::string> getSettingKeys() const;
bool hasSetting(std::string const& key) const;
std::shared_ptr<Setting> getSetting(std::string const& key) const;
std::optional<Setting> getSettingDefinition(std::string const& key) const;
SettingValue* getSetting(std::string const& key) const;
void registerCustomSetting(
std::string const& key,
std::unique_ptr<SettingValue> value
);
template <class T>
T getSettingValue(std::string const& key) const {
if (this->hasSetting(key)) {
return geode::getBuiltInSettingValue<T>(this->getSetting(key));
if (auto sett = this->getSetting(key)) {
return SettingValueSetter<T>::get(sett);
}
return T();
}
template <class T>
bool setSettingValue(std::string const& key, T const& value) {
if (this->hasSetting(key)) {
geode::setBuiltInSettingValue<T>(this->getSetting(key), value);
return true;
T setSettingValue(std::string const& key, T const& value) {
if (auto sett = this->getSetting(key)) {
auto old = this->getSettingValue<T>(sett);
SettingValueSetter<T>::set(sett, value);
return old;
}
return false;
return T();
}
template <class T>
@ -194,16 +197,6 @@ namespace geode {
return defaultValue;
}
template <class T>
HandleToSaved<T> getSavedMutable(std::string const& key) {
return HandleToSaved(key, this, this->getSavedValue<T>(key));
}
template <class T>
HandleToSaved<T> getSavedMutable(std::string const& key, T const& defaultValue) {
return HandleToSaved(key, this, this->getSavedValue<T>(key, defaultValue));
}
/**
* Set the value of an automatically saved variable. When the game is
* closed, the value is automatically saved under the key
@ -389,11 +382,6 @@ namespace geode {
ModJson getRuntimeInfo() const;
};
template <class T>
HandleToSaved<T>::~HandleToSaved() {
m_mod->setSavedValue(m_key, static_cast<T>(*this));
}
/**
* To bypass the need for cyclic dependencies,
* this function does the exact same as Mod::get()

View file

@ -10,8 +10,6 @@ namespace geode {
class Unzip;
}
using ModJson = nlohmann::ordered_json;
struct GEODE_DLL Dependency {
std::string id;
ComparableVersionInfo version;
@ -111,8 +109,9 @@ namespace geode {
std::vector<std::string> spritesheets;
/**
* Mod settings
* @note Not a map because insertion order must be preserved
*/
std::vector<std::pair<std::string, std::shared_ptr<Setting>>> settings;
std::vector<std::pair<std::string, Setting>> settings;
/**
* Whether the mod can be disabled or not
*/

View file

@ -1,502 +1,244 @@
#pragma once
#include "Types.hpp"
#include "../DefaultInclude.hpp"
#include "../utils/JsonValidation.hpp"
#include "../utils/Result.hpp"
#include "../external/json/json.hpp"
#include "../utils/file.hpp"
#include "../utils/ranges.hpp"
#include "../utils/cocos.hpp"
#include "ModInfo.hpp"
#include "../external/json/json.hpp"
#include <optional>
#include <regex>
#include <unordered_set>
#pragma warning(push)
#pragma warning(disable : 4275)
namespace geode {
class Setting;
class SettingNode;
class BoolSetting;
class IntSetting;
class FloatSetting;
class StringSetting;
class SettingValue;
struct ModInfo;
struct GEODE_DLL BoolSetting final {
using ValueType = bool;
enum class SettingType {
Bool,
Int,
Float,
String,
File,
Color,
ColorAlpha,
User,
std::optional<std::string> name;
std::optional<std::string> description;
bool defaultValue;
static Result<BoolSetting> parse(JsonMaybeObject<ModJson>& obj);
};
/**
* Base class for all settings in Geode mods. Note that for most purposes
* you should use the built-in setting types. If you need a custom setting
* type however, inherit from this class. Do note that you are responsible
* for things like storing the default value, broadcasting value change
* events, making the setting node etc.
*/
class Setting : public std::enable_shared_from_this<Setting> {
struct GEODE_DLL IntSetting final {
using ValueType = int64_t;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
std::optional<ValueType> min;
std::optional<ValueType> max;
struct {
bool arrows = true;
bool bigArrows = false;
size_t arrowStep = 1;
size_t bigArrowStep = 5;
bool slider = true;
std::optional<ValueType> sliderStep = std::nullopt;
bool input = true;
} controls;
static Result<IntSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL FloatSetting final {
using ValueType = double;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
std::optional<ValueType> min;
std::optional<ValueType> max;
struct {
bool arrows = true;
bool bigArrows = false;
size_t arrowStep = 1;
size_t bigArrowStep = 5;
bool slider = true;
std::optional<ValueType> sliderStep = std::nullopt;
bool input = true;
} controls;
static Result<FloatSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL StringSetting final {
using ValueType = std::string;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
std::optional<std::string> match;
static Result<StringSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL FileSetting final {
using ValueType = ghc::filesystem::path;
using Filter = utils::file::FilePickOptions::Filter;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
struct {
std::vector<Filter> filters;
} controls;
static Result<FileSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL ColorSetting final {
using ValueType = cocos2d::ccColor3B;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
static Result<ColorSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL ColorAlphaSetting final {
using ValueType = cocos2d::ccColor4B;
std::optional<std::string> name;
std::optional<std::string> description;
ValueType defaultValue;
static Result<ColorAlphaSetting> parse(JsonMaybeObject<ModJson>& obj);
};
struct GEODE_DLL CustomSetting final {
ModJson json;
};
using SettingKind = std::variant<
BoolSetting,
IntSetting,
FloatSetting,
StringSetting,
FileSetting,
ColorSetting,
ColorAlphaSetting,
CustomSetting
>;
struct GEODE_DLL Setting final {
private:
std::string m_key;
SettingKind m_kind;
Setting() = default;
public:
static Result<Setting> parse(
std::string const& key,
JsonMaybeValue<ModJson>& obj
);
Setting(std::string const& key, SettingKind const& kind);
template<class T>
std::optional<T> get() {
if (std::holds_alternative<T>(m_kind)) {
return std::get<T>(m_kind);
}
return std::nullopt;
}
std::unique_ptr<SettingValue> createDefaultValue() const;
bool isCustom() const;
std::string getDisplayName() const;
std::optional<std::string> getDescription() const;
};
class GEODE_DLL SettingValue {
protected:
std::string m_key;
std::string m_modID;
friend struct ModInfo;
static Result<std::shared_ptr<Setting>> parse(
std::string const& type, std::string const& key, JsonMaybeObject<ModJson>& obj
);
SettingValue(std::string const& key);
public:
GEODE_DLL virtual ~Setting() = default;
// Load from mod.json
GEODE_DLL static Result<std::shared_ptr<Setting>> parse(
std::string const& key, ModJson const& json
);
// Load value from saved settings
virtual ~SettingValue() = default;
virtual bool load(nlohmann::json const& json) = 0;
// Save setting value
virtual bool save(nlohmann::json& json) const = 0;
virtual SettingNode* createNode(float width) = 0;
GEODE_DLL void valueChanged();
GEODE_DLL std::string getKey() const;
virtual SettingType getType() const = 0;
std::string getKey() const;
};
// built-in settings' implementation details
namespace {
#define GEODE_INT_PARSE_SETTING_IMPL(obj, func, ...) \
if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \
auto r = std::static_pointer_cast<Class>(res)->func(obj); \
if (!r) return Err(r.unwrapErr()); \
}
template<class T>
class GeodeSettingValue : public SettingValue {
public:
using ValueType = typename T::ValueType;
#define GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(func, ...) \
if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \
auto res = static_cast<Class*>(this)->func(value); \
if (!res) { \
return res; \
} \
}
template <class ValueType>
class IMinMax;
template <class Class, class ValueType>
class IOneOf;
template <class Class, class ValueType>
class IMatch;
class ICArrows;
template <class ValueType>
class ICSlider;
class ICInput;
class ICFileFilters;
template <class Class, class ValueType, SettingType Type>
class GeodeSetting : public Setting {
protected:
ValueType m_default;
ValueType m_value;
std::optional<std::string> m_name;
std::optional<std::string> m_description;
bool m_canResetToDefault = true;
T m_definition;
friend class Setting;
using Valid = std::pair<ValueType, std::optional<std::string>>;
static Result<std::shared_ptr<Class>> parse(
std::string const& key, JsonMaybeObject<ModJson>& obj
) {
auto res = std::make_shared<Class>();
res->m_key = key;
obj.needs("default").into(res->m_default);
obj.has("name").intoAs<std::string>(res->m_name);
obj.has("description").intoAs<std::string>(res->m_description);
GEODE_INT_PARSE_SETTING_IMPL(obj, parseMinMax, IMinMax<ValueType>);
GEODE_INT_PARSE_SETTING_IMPL(obj, parseOneOf, IOneOf<Class, ValueType>);
GEODE_INT_PARSE_SETTING_IMPL(obj, parseMatch, IMatch<Class, ValueType>);
res->setValue(res->m_default);
if (auto controls = obj.has("control").obj()) {
// every built-in setting type has a reset button
// by default
controls.has("can-reset").into(res->m_canResetToDefault);
GEODE_INT_PARSE_SETTING_IMPL(controls, parseArrows, ICArrows);
GEODE_INT_PARSE_SETTING_IMPL(controls, parseSlider, ICSlider<ValueType>);
GEODE_INT_PARSE_SETTING_IMPL(controls, parseInput, ICInput);
GEODE_INT_PARSE_SETTING_IMPL(controls, parseFileFilters, ICFileFilters);
}
return Ok(res);
}
GEODE_DLL Valid toValid(ValueType const& value) const;
public:
using value_t = ValueType;
GeodeSettingValue(std::string const& key, T const& definition)
: SettingValue(key),
m_definition(definition),
m_value(definition.defaultValue) {}
std::optional<std::string> getName() const {
return m_name;
bool load(nlohmann::json const& json) override {
try {
m_value = json.get<ValueType>();
return true;
} catch(...) {
return false;
}
}
bool save(nlohmann::json& json) const {
json = m_value;
return true;
}
std::string getDisplayName() const {
return m_name.value_or(m_key);
GEODE_DLL SettingNode* createNode(float width) override;
T castDefinition() const {
return m_definition;
}
std::optional<std::string> getDescription() const {
return m_description;
}
ValueType getDefault() const {
return m_default;
Setting getDefinition() const {
return Setting(m_key, m_definition);
}
ValueType getValue() const {
return m_value;
}
void setValue(ValueType const& value) {
m_value = value;
if constexpr (std::is_base_of_v<IMinMax<ValueType>, Class>) {
(void)static_cast<Class*>(this)->constrainMinMax(m_value);
m_value = this->toValid(value).first;
}
if constexpr (std::is_base_of_v<IOneOf<Class, ValueType>, Class>) {
(void)static_cast<Class*>(this)->constrainOneOf(m_value);
}
if constexpr (std::is_base_of_v<IMatch<Class, ValueType>, Class>) {
(void)static_cast<Class*>(this)->constrainMatch(m_value);
}
this->valueChanged();
}
Result<> isValidValue(ValueType value) {
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMinMax, IMinMax<ValueType>);
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainOneOf, IOneOf<Class, ValueType>);
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMatch, IMatch<Class, ValueType>);
return Ok();
}
bool load(nlohmann::json const& json) override {
auto rawJson = json;
JsonChecker(rawJson).root("[setting value]").into(m_value);
return true;
}
bool save(nlohmann::json& json) const override {
json = m_value;
return true;
}
bool canResetToDefault() const {
return m_canResetToDefault;
}
SettingType getType() const override {
return Type;
}
};
template <class ValueType>
class IMinMax {
protected:
std::optional<ValueType> m_min = std::nullopt;
std::optional<ValueType> m_max = std::nullopt;
public:
Result<> constrainMinMax(ValueType& value) {
if (m_min && value < m_min.value()) {
value = m_min.value();
return Err(
"Value must be between " + std::to_string(m_min.value()) + " and " +
std::to_string(m_max.value())
);
}
if (m_max && value > m_max.value()) {
value = m_max.value();
return Err(
"Value must be between " + std::to_string(m_min.value()) + " and " +
std::to_string(m_max.value())
);
Result<> 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();
}
Result<> parseMinMax(JsonMaybeObject<ModJson>& obj) {
obj.has("min").intoAs<ValueType>(m_min);
obj.has("max").intoAs<ValueType>(m_max);
return Ok();
}
std::optional<ValueType> getMin() const {
return m_min;
}
std::optional<ValueType> getMax() const {
return m_max;
}
};
template <class Class, class ValueType>
class IOneOf {
protected:
std::optional<std::unordered_set<ValueType>> m_oneOf = std::nullopt;
using BoolSettingValue = GeodeSettingValue<BoolSetting>;
using IntSettingValue = GeodeSettingValue<IntSetting>;
using FloatSettingValue = GeodeSettingValue<FloatSetting>;
using StringSettingValue = GeodeSettingValue<StringSetting>;
using FileSettingValue = GeodeSettingValue<FileSetting>;
using ColorSettingValue = GeodeSettingValue<ColorSetting>;
using ColorAlphaSettingValue = GeodeSettingValue<ColorAlphaSetting>;
public:
Result<> constrainOneOf(ValueType& value) {
if (m_oneOf && !m_oneOf.value().count(value)) {
value = static_cast<Class*>(this)->getDefault();
return Err(
"Value must be one of " + utils::ranges::join(m_oneOf.value(), ", ")
);
}
return Ok();
}
Result<> parseOneOf(JsonMaybeObject<ModJson>& obj) {
std::unordered_set<ValueType> oneOf {};
for (auto& item : obj.has("one-of").iterate()) {
oneOf.insert(item.get<ValueType>());
}
if (oneOf.size()) {
m_oneOf = oneOf;
}
return Ok();
}
auto getOneOf() const {
return m_oneOf;
}
template<class T>
struct SettingValueSetter {
static GEODE_DLL T get(SettingValue* setting);
static GEODE_DLL void set(SettingValue* setting, T const& value);
};
template <class Class, class ValueType>
class IMatch {
protected:
std::optional<ValueType> m_matchRegex = std::nullopt;
public:
Result<> constrainMatch(ValueType& value) {
if (m_matchRegex) {
auto regex = std::regex(m_matchRegex.value());
if (!std::regex_match(value, regex)) {
value = static_cast<Class*>(this)->getDefault();
return Err("Value must match regex " + m_matchRegex.value());
}
}
return Ok();
}
Result<> parseMatch(JsonMaybeObject<ModJson>& obj) {
obj.has("match").intoAs<ValueType>(m_matchRegex);
return Ok();
}
std::optional<ValueType> getMatch() const {
return m_matchRegex;
}
};
#define GEODE_INT_DECL_SETTING_CONTROL(Name, name, default, json) \
class IC##Name { \
protected: \
bool m_##name = default; \
\
public: \
Result<> parse##Name(JsonMaybeObject<ModJson>& obj) { \
obj.has(json).into(m_##name); \
return Ok(); \
} \
bool has##Name() const { return m_##name; } \
}
class ICArrows {
protected:
bool m_hasArrows = true;
bool m_hasBigArrows = false;
size_t m_arrowStep = 1;
size_t m_bigArrowStep = 5;
public:
Result<> parseArrows(JsonMaybeObject<ModJson>& obj) {
obj.has("arrows").into(m_hasArrows);
obj.has("arrow-step").into(m_arrowStep);
obj.has("big-arrows").into(m_hasBigArrows);
obj.has("big-arrow-step").into(m_bigArrowStep);
return Ok();
}
bool hasArrows() const {
return m_hasArrows;
}
bool hasBigArrows() const {
return m_hasBigArrows;
}
size_t getArrowStepSize() const {
return m_arrowStep;
}
size_t getBigArrowStepSize() const {
return m_bigArrowStep;
}
};
template <class ValueType>
class ICSlider {
protected:
bool m_hasSlider = true;
std::optional<ValueType> m_sliderStep = std::nullopt;
public:
Result<> parseSlider(JsonMaybeObject<ModJson>& obj) {
obj.has("slider").into(m_hasSlider);
obj.has("slider-step").intoAs<ValueType>(m_sliderStep);
return Ok();
}
bool hasSlider() const {
return m_hasSlider;
}
std::optional<ValueType> getSliderStepSize() const {
return m_sliderStep;
}
};
class ICFileFilters {
protected:
using Filter = utils::file::FilePickOptions::Filter;
std::optional<std::vector<Filter>> m_filters = std::nullopt;
public:
Result<> parseFileFilters(JsonMaybeObject<ModJson>& obj) {
std::vector<Filter> filters {};
for (auto& item : obj.has("filters").iterate()) {
if (auto iobj = item.obj()) {
Filter filter;
iobj.has("description").into(filter.description);
iobj.has("files").into(filter.files);
filters.push_back(filter);
}
}
if (filters.size()) {
m_filters = filters;
}
return Ok();
}
auto getFileFilters() const {
return m_filters;
}
};
GEODE_INT_DECL_SETTING_CONTROL(Input, hasInput, true, "input");
}
class BoolSetting : public GeodeSetting<BoolSetting, bool, SettingType::Bool> {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class IntSetting :
public GeodeSetting<IntSetting, int64_t, SettingType::Int>,
public IOneOf<IntSetting, int64_t>,
public IMinMax<int64_t>,
public ICArrows,
public ICSlider<int64_t>,
public ICInput {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class FloatSetting :
public GeodeSetting<FloatSetting, double, SettingType::Float>,
public IOneOf<FloatSetting, double>,
public IMinMax<double>,
public ICArrows,
public ICSlider<double>,
public ICInput {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class StringSetting :
public GeodeSetting<StringSetting, std::string, SettingType::String>,
public IOneOf<StringSetting, std::string>,
public IMatch<StringSetting, std::string> {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class FileSetting :
public GeodeSetting<FileSetting, ghc::filesystem::path, SettingType::File>,
public ICFileFilters {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class ColorSetting : public GeodeSetting<ColorSetting, cocos2d::ccColor3B, SettingType::Color> {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
class ColorAlphaSetting :
public GeodeSetting<ColorAlphaSetting, cocos2d::ccColor4B, SettingType::ColorAlpha> {
public:
GEODE_DLL SettingNode* createNode(float width) override;
};
// these can't be member functions because C++ is single-pass >:(
#define GEODE_INT_BUILTIN_SETTING_IF(type, action, ...) \
if constexpr (__VA_ARGS__) { \
if (setting->getType() == SettingType::type) { \
return std::static_pointer_cast<type##Setting>(setting)->action; \
} \
}
// clang-format off
template <class T>
T getBuiltInSettingValue(const std::shared_ptr<Setting> setting) {
GEODE_INT_BUILTIN_SETTING_IF(Bool, getValue(), std::is_same_v<T, bool>)
else GEODE_INT_BUILTIN_SETTING_IF(Float, getValue(), std::is_floating_point_v<T>)
else GEODE_INT_BUILTIN_SETTING_IF(Int, getValue(), std::is_integral_v<T>)
else GEODE_INT_BUILTIN_SETTING_IF(String, getValue(), std::is_same_v<T, std::string>)
else GEODE_INT_BUILTIN_SETTING_IF(File, getValue(), std::is_same_v<T, ghc::filesystem::path>)
else GEODE_INT_BUILTIN_SETTING_IF(Color, getValue(), std::is_same_v<T, cocos2d::ccColor3B>)
else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, getValue(), std::is_same_v<T, cocos2d::ccColor4B>)
else {
static_assert(!std::is_same_v<T, T>, "Unsupported type for getting setting value!");
}
return T();
}
template <class T>
void setBuiltInSettingValue(const std::shared_ptr<Setting> setting, T const& value) {
GEODE_INT_BUILTIN_SETTING_IF(Bool, setValue(value), std::is_same_v<T, bool>)
else GEODE_INT_BUILTIN_SETTING_IF(Float, setValue(value), std::is_floating_point_v<T>)
else GEODE_INT_BUILTIN_SETTING_IF(Int, setValue(value), std::is_integral_v<T>)
else GEODE_INT_BUILTIN_SETTING_IF(String, setValue(value), std::is_same_v<T, std::string>)
else GEODE_INT_BUILTIN_SETTING_IF(File, setValue(value), std::is_same_v<T, ghc::filesystem::path>)
else GEODE_INT_BUILTIN_SETTING_IF(Color, setValue(value), std::is_same_v<T, cocos2d::ccColor3B>)
else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, setValue(value), std::is_same_v<T, cocos2d::ccColor4B>)
else {
static_assert(!std::is_same_v<T, T>, "Unsupported type for getting setting value!");
}
}
// clang-format on
}
#pragma warning(pop)

View file

@ -7,67 +7,73 @@
#include <optional>
namespace geode {
class GEODE_DLL SettingChangedEvent : public Event {
protected:
std::string m_modID;
Setting* m_setting;
struct GEODE_DLL SettingChangedEvent : public Event {
Mod* mod;
SettingValue* value;
public:
SettingChangedEvent(std::string const& modID, Setting* setting);
std::string getModID() const;
Setting* getSetting() const;
SettingChangedEvent(Mod* mod, SettingValue* value);
};
template <typename T = Setting, typename = std::enable_if_t<std::is_base_of_v<Setting, T>>>
class SettingChangedFilter : public EventFilter<SettingChangedEvent> {
class GEODE_DLL SettingChangedFilter : public EventFilter<SettingChangedEvent> {
protected:
std::string m_modID;
std::optional<std::string> m_targetKey;
public:
using Callback = void(T*);
using Event = SettingChangedEvent;
using Callback = void(SettingValue*);
ListenerResult handle(std::function<Callback> fn, SettingChangedEvent* event);
/**
* Listen to changes on a setting, or all settings
* @param modID Mod whose settings to listen to
* @param settingID Setting to listen to, or all settings if nullopt
*/
SettingChangedFilter(
std::string const& modID,
std::optional<std::string> const& settingKey
);
};
/**
* Listen for built-in setting changes
*/
template<class T>
class GeodeSettingChangedFilter : public SettingChangedFilter {
public:
using Callback = void(T);
ListenerResult handle(std::function<Callback> fn, SettingChangedEvent* event) {
if (m_modID == event->getModID() &&
(!m_targetKey || m_targetKey.value() == event->getSetting()->getKey())) {
fn(static_cast<T*>(event->getSetting()));
if (
m_modID == event->mod->getID() &&
(!m_targetKey || m_targetKey.value() == event->value->getKey())
) {
fn(SettingValueSetter<T>::get(event->value));
}
return ListenerResult::Propagate;
}
/**
* Listen to changes on a specific setting
*/
SettingChangedFilter(
std::string const& modID, std::string const& settingID
) :
m_modID(modID),
m_targetKey(settingID) {}
/**
* Listen to changes on all of a mods' settings
*/
SettingChangedFilter(std::string const& modID) :
m_modID(modID), m_targetKey(std::nullopt) {}
protected:
std::string m_modID;
std::optional<std::string> m_targetKey;
GeodeSettingChangedFilter(
std::string const& modID,
std::string const& settingID
) : SettingChangedFilter(modID, settingID) {}
};
template <class T>
requires std::is_base_of_v<Setting, T>
std::monostate listenForSettingChanges(
std::string const& settingID, void (*callback)(T*)
std::string const& settingKey, void (*callback)(T)
) {
Loader::get()->scheduleOnModLoad(getMod(), [=]() {
new EventListener(
callback, SettingChangedFilter<T>(getMod()->getID(), settingID)
callback, GeodeSettingChangedFilter<T>(getMod()->getID(), settingKey)
);
});
return std::monostate();
}
static std::monostate listenForAllSettingChanges(void (*callback)(Setting*)) {
static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) {
Loader::get()->scheduleOnModLoad(getMod(), [=]() {
new EventListener(
callback, SettingChangedFilter(getMod()->getID())
callback, SettingChangedFilter(getMod()->getID(), std::nullopt)
);
});
return std::monostate();

View file

@ -7,17 +7,19 @@
namespace geode {
class SettingNode;
struct GEODE_DLL SettingNodeDelegate {
virtual void settingValueChanged(SettingNode* node);
virtual void settingValueCommitted(SettingNode* node);
struct SettingNodeDelegate {
virtual void settingValueChanged(SettingNode* node) {}
virtual void settingValueCommitted(SettingNode* node) {}
};
class GEODE_DLL SettingNode : public cocos2d::CCNode {
protected:
std::shared_ptr<Setting> m_setting;
SettingValue* m_value;
SettingNodeDelegate* m_delegate = nullptr;
bool init(std::shared_ptr<Setting> setting);
bool init(SettingValue* value);
void dispatchChanged();
void dispatchCommitted();
public:
void setDelegate(SettingNodeDelegate* delegate);

View file

@ -2,6 +2,7 @@
#include "../DefaultInclude.hpp"
#include "../platform/cplatform.h"
#include "../external/json/json.hpp"
#include <string>
@ -147,6 +148,7 @@ namespace geode {
class FieldIntermediate;
}
using ModJson = nlohmann::ordered_json;
}
/**

View file

@ -105,6 +105,7 @@ namespace geode {
);
GEODE_DLL bool isError() const;
GEODE_DLL std::string getError() const;
GEODE_DLL operator bool() const;
};

View file

@ -312,11 +312,13 @@ namespace geode {
protected:
EventListener<Filter> m_listener;
EventListenerNode(EventListener<Filter>&& listener)
: m_listener(std::move(listener)) {}
public:
static EventListenerNode* create(EventListener<Filter> listener) {
auto ret = new EventListenerNode();
auto ret = new EventListenerNode(std::move(listener));
if (ret && ret->init()) {
ret->m_listener = listener;
ret->autorelease();
return ret;
}
@ -325,9 +327,8 @@ namespace geode {
}
static EventListenerNode* create(typename Filter::Callback callback) {
auto ret = new EventListenerNode();
auto ret = new EventListenerNode(EventListener<Filter>(callback));
if (ret && ret->init()) {
ret->m_listener = EventListener(callback);
ret->autorelease();
return ret;
}
@ -343,9 +344,8 @@ namespace geode {
// for some reason msvc won't let me just call EventListenerNode::create...
// it claims no return value...
// despite me writing return EventListenerNode::create()......
auto ret = new EventListenerNode();
auto ret = new EventListenerNode(EventListener<Filter>(cls, callback));
if (ret && ret->init()) {
ret->m_listener.bind(cls, callback);
ret->autorelease();
return ret;
}

View file

@ -9,6 +9,12 @@
#include <string>
#include <unordered_set>
// allow converting ghc filesystem to json and back
namespace ghc::filesystem {
void GEODE_DLL to_json(nlohmann::json& json, path const& path);
void GEODE_DLL from_json(nlohmann::json const& json, path& path);
}
namespace geode::utils::file {
GEODE_DLL Result<std::string> readString(ghc::filesystem::path const& path);
GEODE_DLL Result<nlohmann::json> readJson(ghc::filesystem::path const& path);

View file

@ -1,6 +1,6 @@
#include "LoaderImpl.hpp"
#include <cocos2d.h>
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/IPC.hpp>
#include <Geode/loader/Loader.hpp>
@ -154,7 +154,8 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) {
Result<> Loader::Impl::saveData() {
// save mods' data
for (auto& [_, mod] : m_mods) {
for (auto& [id, mod] : m_mods) {
InternalMod::get()->setSavedValue("should-load-" + id, mod->isEnabled());
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
@ -254,10 +255,10 @@ void Loader::Impl::dispatchScheduledFunctions(Mod* mod) {
}
void Loader::Impl::scheduleOnModLoad(Mod* mod, ScheduledFunction func) {
std::lock_guard _(m_scheduledFunctionsMutex);
if (mod) {
return func();
}
std::lock_guard _(m_scheduledFunctionsMutex);
m_scheduledFunctions.push_back(func);
}
@ -403,9 +404,6 @@ bool Loader::Impl::didLastLaunchCrash() const {
return crashlog::didLastLaunchCrash();
}
void Loader::Impl::reset() {
this->closePlatformConsole();
@ -417,6 +415,7 @@ void Loader::Impl::reset() {
ghc::filesystem::remove_all(dirs::getModRuntimeDir());
ghc::filesystem::remove_all(dirs::getTempDir());
}
bool Loader::Impl::isReadyToHook() const {
return m_readyToHook;
}

View file

@ -17,6 +17,14 @@ Mod::Mod(ModInfo const& info) {
m_info = info;
m_saveDirPath = dirs::getModsSaveDir() / info.id;
ghc::filesystem::create_directories(m_saveDirPath);
this->setupSettings();
auto loadRes = this->loadData();
if (!loadRes) {
log::warn(
"Unable to load data for \"{}\": {}",
info.id, loadRes.unwrapErr()
);
}
}
Mod::~Mod() {
@ -93,14 +101,6 @@ std::vector<Hook*> Mod::getHooks() const {
return m_hooks;
}
bool Mod::hasSettings() const {
return m_info.settings.size();
}
decltype(ModInfo::settings) Mod::getSettings() const {
return m_info.settings;
}
// Settings and saved values
Result<> Mod::loadData() {
@ -117,12 +117,20 @@ Result<> Mod::loadData() {
JsonChecker checker(json);
auto root = checker.root("[settings.json]");
m_savedSettingsData = json;
for (auto& [key, value] : root.items()) {
// check if this is a known setting
if (auto setting = this->getSetting(key)) {
// load its value
if (!setting->load(value.json()))
return Err("Unable to load value for setting \"" + key + "\"");
if (!setting->load(value.json())) {
log::log(
Severity::Error,
this,
"{}: Unable to load value for setting \"{}\"",
m_info.id, key
);
}
}
else {
log::log(
@ -158,26 +166,91 @@ Result<> Mod::loadData() {
Result<> Mod::saveData() {
ModStateEvent(this, ModEventType::DataSaved).post();
// Data saving should be fully fail-safe
std::unordered_set<std::string> coveredSettings;
// Settings
auto json = nlohmann::json::object();
for (auto& [key, value] : m_info.settings) {
if (!value->save(json[key])) return Err("Unable to save setting \"" + key + "\"");
for (auto& [key, value] : m_settings) {
coveredSettings.insert(key);
if (!value->save(json[key])) {
log::error("Unable to save setting \"" + key + "\"");
}
}
GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4)));
// if some settings weren't provided a custom settings handler (for example,
// the mod was not loaded) then make sure to save their previous state in
// order to not lose data
try {
log::debug("Check covered");
for (auto& [key, value] : m_savedSettingsData.items()) {
log::debug("Check if {} is saved", key);
if (!coveredSettings.count(key)) {
json[key] = value;
}
}
} catch(...) {}
// Saved values
GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4)));
auto res = utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4));
if (!res) {
log::error("Unable to save settings: {}", res.unwrapErr());
}
auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4));
if (!res2) {
log::error("Unable to save values: {}", res2.unwrapErr());
}
return Ok();
}
std::shared_ptr<Setting> Mod::getSetting(std::string const& key) const {
void Mod::setupSettings() {
for (auto& [key, sett] : m_info.settings) {
if (auto value = sett.createDefaultValue()) {
m_settings.emplace(key, std::move(value));
}
}
}
void Mod::registerCustomSetting(
std::string const& key,
std::unique_ptr<SettingValue> value
) {
if (!m_settings.count(key)) {
// load data
if (m_savedSettingsData.count(key)) {
value->load(m_savedSettingsData.at(key));
}
m_settings.emplace(key, std::move(value));
}
}
bool Mod::hasSettings() const {
return m_info.settings.size();
}
std::vector<std::string> Mod::getSettingKeys() const {
std::vector<std::string> keys;
for (auto& [key, _] : m_info.settings) {
keys.push_back(key);
}
return keys;
}
std::optional<Setting> Mod::getSettingDefinition(std::string const& key) const {
for (auto& setting : m_info.settings) {
if (setting.first == key) {
return setting.second;
}
}
return std::nullopt;
}
SettingValue* Mod::getSetting(std::string const& key) const {
if (m_settings.count(key)) {
return m_settings.at(key).get();
}
return nullptr;
}
@ -211,11 +284,6 @@ Result<> Mod::loadBinary() {
ModStateEvent(this, ModEventType::Loaded).post();
auto loadRes = this->loadData();
if (!loadRes) {
log::warn("Unable to load data for \"{}\": {}", m_info.id, loadRes.unwrapErr());
}
Loader::get()->updateAllDependencies();
GEODE_UNWRAP(this->enable());

View file

@ -70,8 +70,7 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
}
for (auto& [key, value] : root.has("settings").items()) {
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value.json()));
sett->m_modID = info.id;
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value));
info.settings.push_back({ key, sett });
}

View file

@ -4,106 +4,361 @@
#include <Geode/loader/SettingEvent.hpp>
#include <Geode/loader/SettingNode.hpp>
#include <Geode/utils/general.hpp>
#include <regex>
USE_GEODE_NAMESPACE();
#define PROPAGATE(err) \
{ \
auto err__ = err; \
if (!err__) return Err(err__.error()); \
}
template<class T>
static void parseCommon(T& sett, JsonMaybeObject<ModJson>& obj) {
obj.has("name").into(sett.name);
obj.has("description").into(sett.description);
obj.has("default").into(sett.defaultValue);
}
std::string Setting::getKey() const {
Result<BoolSetting> BoolSetting::parse(JsonMaybeObject<ModJson>& obj) {
BoolSetting sett;
parseCommon(sett, obj);
return Ok(sett);
}
Result<IntSetting> IntSetting::parse(JsonMaybeObject<ModJson>& obj) {
IntSetting sett;
parseCommon(sett, obj);
obj.has("min").into(sett.min);
obj.has("max").into(sett.max);
if (auto controls = obj.has("control").obj()) {
controls.has("arrows").into(sett.controls.arrows);
controls.has("big-arrows").into(sett.controls.bigArrows);
controls.has("arrow-step").into(sett.controls.arrowStep);
controls.has("big-arrow-step").into(sett.controls.bigArrowStep);
controls.has("slider").into(sett.controls.slider);
controls.has("slider-step").into(sett.controls.sliderStep);
controls.has("input").into(sett.controls.input);
}
return Ok(sett);
}
Result<FloatSetting> FloatSetting::parse(JsonMaybeObject<ModJson>& obj) {
FloatSetting sett;
parseCommon(sett, obj);
obj.has("min").into(sett.min);
obj.has("max").into(sett.max);
if (auto controls = obj.has("control").obj()) {
controls.has("arrows").into(sett.controls.arrows);
controls.has("big-arrows").into(sett.controls.bigArrows);
controls.has("arrow-step").into(sett.controls.arrowStep);
controls.has("big-arrow-step").into(sett.controls.bigArrowStep);
controls.has("slider").into(sett.controls.slider);
controls.has("slider-step").into(sett.controls.sliderStep);
controls.has("input").into(sett.controls.input);
}
return Ok(sett);
}
Result<StringSetting> StringSetting::parse(JsonMaybeObject<ModJson>& obj) {
StringSetting sett;
parseCommon(sett, obj);
obj.has("match").into(sett.match);
return Ok(sett);
}
Result<FileSetting> FileSetting::parse(JsonMaybeObject<ModJson>& obj) {
FileSetting sett;
parseCommon(sett, obj);
if (auto controls = obj.has("control").obj()) {
for (auto& item : controls.has("filters").iterate()) {
if (auto iobj = item.obj()) {
Filter filter;
iobj.has("description").into(filter.description);
iobj.has("files").into(filter.files);
sett.controls.filters.push_back(filter);
}
}
}
return Ok(sett);
}
Result<ColorSetting> ColorSetting::parse(JsonMaybeObject<ModJson>& obj) {
ColorSetting sett;
parseCommon(sett, obj);
return Ok(sett);
}
Result<ColorAlphaSetting> ColorAlphaSetting::parse(JsonMaybeObject<ModJson>& obj) {
ColorAlphaSetting sett;
parseCommon(sett, obj);
return Ok(sett);
}
Result<Setting> Setting::parse(
std::string const& key,
JsonMaybeValue<ModJson>& value
) {
auto sett = Setting();
sett.m_key = key;
if (auto obj = value.obj()) {
std::string type;
obj.needs("type").into(type);
if (type.size()) {
switch (hash(type.c_str())) {
case hash("bool"): {
GEODE_UNWRAP_INTO(sett.m_kind, BoolSetting::parse(obj));
} break;
case hash("int"): {
GEODE_UNWRAP_INTO(sett.m_kind, IntSetting::parse(obj));
} break;
case hash("float"): {
GEODE_UNWRAP_INTO(sett.m_kind, FloatSetting::parse(obj));
} break;
case hash("string"): {
GEODE_UNWRAP_INTO(sett.m_kind, StringSetting::parse(obj));
} break;
case hash("rgb"): case hash("color"): {
GEODE_UNWRAP_INTO(sett.m_kind, ColorSetting::parse(obj));
} break;
case hash("rgba"): {
GEODE_UNWRAP_INTO(sett.m_kind, ColorAlphaSetting::parse(obj));
} break;
case hash("path"): case hash("file"): {
GEODE_UNWRAP_INTO(sett.m_kind, FileSetting::parse(obj));
} break;
case hash("custom"): {
sett.m_kind = CustomSetting {
.json = obj.json()
};
// skip checking unknown keys
return Ok(sett);
} break;
default: return Err("Unknown setting type \"" + type + "\"");
}
}
obj.checkUnknownKeys();
}
// if the type wasn't an object or a string, the JsonChecker that gave the
// JsonMaybeValue will fail eventually so we can continue on
return Ok(sett);
}
Setting::Setting(std::string const& key, SettingKind const& kind)
: m_key(key), m_kind(kind) {}
std::unique_ptr<SettingValue> Setting::createDefaultValue() const {
return std::visit(makeVisitor {
[&](BoolSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<BoolSettingValue>(m_key, sett);
},
[&](IntSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<IntSettingValue>(m_key, sett);
},
[&](FloatSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<FloatSettingValue>(m_key, sett);
},
[&](StringSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<StringSettingValue>(m_key, sett);
},
[&](FileSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<FileSettingValue>(m_key, sett);
},
[&](ColorSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<ColorSettingValue>(m_key, sett);
},
[&](ColorAlphaSetting const& sett) -> std::unique_ptr<SettingValue> {
return std::make_unique<ColorAlphaSettingValue>(m_key, sett);
},
[&](auto const& sett) -> std::unique_ptr<SettingValue> {
return nullptr;
},
}, m_kind);
}
bool Setting::isCustom() const {
return std::holds_alternative<CustomSetting>(m_kind);
}
std::string Setting::getDisplayName() const {
return std::visit(makeVisitor {
[&](CustomSetting const& sett) {
return std::string();
},
[&](auto const& sett) {
return sett.name.value_or(m_key);
},
}, m_kind);
}
std::optional<std::string> Setting::getDescription() const {
return std::visit(makeVisitor {
[&](CustomSetting const& sett) -> std::optional<std::string> {
return std::nullopt;
},
[&](auto const& sett) {
return sett.description;
},
}, m_kind);
}
// SettingValue
SettingValue::SettingValue(std::string const& key) : m_key(key) {}
std::string SettingValue::getKey() const {
return m_key;
}
Result<std::shared_ptr<Setting>> Setting::parse(
std::string const& type, std::string const& key, JsonMaybeObject<ModJson>& obj
// GeodeSettingValue & SettingValueSetter specializations
#define IMPL_NODE_AND_SETTERS(type_) \
template<> \
SettingNode* GeodeSettingValue< \
type_##Setting \
>::createNode(float width) { \
return type_##SettingNode::create(this, width); \
} \
typename type_##Setting::ValueType SettingValueSetter< \
typename type_##Setting::ValueType \
>::get(SettingValue* setting) { \
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
return b->getValue(); \
} \
return typename type_##Setting::ValueType(); \
} \
void SettingValueSetter< \
typename type_##Setting::ValueType \
>::set( \
SettingValue* setting, \
typename type_##Setting::ValueType const& value \
) { \
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
b->setValue(value); \
} \
}
#define IMPL_TO_VALID(type_) \
template<> \
typename GeodeSettingValue<type_##Setting>::Valid \
GeodeSettingValue<type_##Setting>::toValid( \
typename type_##Setting::ValueType const& value \
) const
// instantiate value setters
template struct SettingValueSetter<typename BoolSetting::ValueType>;
template struct SettingValueSetter<typename IntSetting::ValueType>;
template struct SettingValueSetter<typename FloatSetting::ValueType>;
template struct SettingValueSetter<typename StringSetting::ValueType>;
template struct SettingValueSetter<typename FileSetting::ValueType>;
template struct SettingValueSetter<typename ColorSetting::ValueType>;
template struct SettingValueSetter<typename ColorAlphaSetting::ValueType>;
// instantiate values
template class GeodeSettingValue<BoolSetting>;
template class GeodeSettingValue<IntSetting>;
template class GeodeSettingValue<FloatSetting>;
template class GeodeSettingValue<StringSetting>;
template class GeodeSettingValue<FileSetting>;
template class GeodeSettingValue<ColorSetting>;
template class GeodeSettingValue<ColorAlphaSetting>;
IMPL_NODE_AND_SETTERS(Bool);
IMPL_NODE_AND_SETTERS(Int);
IMPL_NODE_AND_SETTERS(Float);
IMPL_NODE_AND_SETTERS(String);
IMPL_NODE_AND_SETTERS(File);
IMPL_NODE_AND_SETTERS(Color);
IMPL_NODE_AND_SETTERS(ColorAlpha);
IMPL_TO_VALID(Bool) {
return { value, std::nullopt };
}
IMPL_TO_VALID(Int) {
if (m_definition.min && value < m_definition.min) {
return { m_definition.min.value(), fmt::format(
"Value must be more than or equal to {}",
m_definition.min.value()
) };
}
if (m_definition.max && value > m_definition.max) {
return { m_definition.max.value(), fmt::format(
"Value must be less than or equal to {}",
m_definition.max.value()
) };
}
return { value, std::nullopt };
}
IMPL_TO_VALID(Float) {
if (m_definition.min && value < m_definition.min) {
return { m_definition.min.value(), fmt::format(
"Value must be more than or equal to {}",
m_definition.min.value()
) };
}
if (m_definition.max && value > m_definition.max) {
return { m_definition.max.value(), fmt::format(
"Value must be less than or equal to {}",
m_definition.max.value()
) };
}
return { value, std::nullopt };
}
IMPL_TO_VALID(String) {
if (m_definition.match) {
auto regex = std::regex(m_definition.match.value());
if (!std::regex_match(value, regex)) {
return {
m_definition.defaultValue,
fmt::format(
"Value must match regex {}",
m_definition.match.value()
)
};
}
}
return { value, std::nullopt };
}
IMPL_TO_VALID(File) {
return { value, std::nullopt };
}
IMPL_TO_VALID(Color) {
return { value, std::nullopt };
}
IMPL_TO_VALID(ColorAlpha) {
return { value, std::nullopt };
}
// SettingChangedEvent
SettingChangedEvent::SettingChangedEvent(Mod* mod, SettingValue* value)
: mod(mod), value(value) {}
// SettingChangedFilter
ListenerResult SettingChangedFilter::handle(
std::function<Callback> fn, SettingChangedEvent* event
) {
switch (hash(type.c_str())) {
case hash("bool"): return BoolSetting::parse(key, obj);
case hash("int"): return IntSetting::parse(key, obj);
case hash("float"): return FloatSetting::parse(key, obj);
case hash("string"): return StringSetting::parse(key, obj);
case hash("rgb"):
case hash("color"): return ColorSetting::parse(key, obj);
case hash("rgba"): return ColorAlphaSetting::parse(key, obj);
case hash("path"):
case hash("file"): return FileSetting::parse(key, obj);
default: return Err("Setting \"" + key + "\" has unknown type \"" + type + "\"");
if (m_modID == event->mod->getID() &&
(!m_targetKey || m_targetKey.value() == event->value->getKey())
) {
fn(event->value);
}
return ListenerResult::Propagate;
}
Result<std::shared_ptr<Setting>> Setting::parse(std::string const& key, ModJson const& rawJson) {
if (rawJson.is_object()) {
auto json = rawJson;
JsonChecker checker(json);
auto root = checker.root("[setting \"" + key + "\"]").obj();
auto res = Setting::parse(root.needs("type").get<std::string>(), key, root);
root.checkUnknownKeys();
if (checker.isError()) {
return Err(checker.getError());
}
return res;
}
return Err("Setting value is not an object");
}
void Setting::valueChanged() {
SettingChangedEvent(m_modID, this).post();
}
SettingNode* BoolSetting::createNode(float width) {
return BoolSettingNode::create(
std::static_pointer_cast<BoolSetting>(shared_from_this()), width
);
}
SettingNode* IntSetting::createNode(float width) {
return IntSettingNode::create(std::static_pointer_cast<IntSetting>(shared_from_this()), width);
}
SettingNode* FloatSetting::createNode(float width) {
return FloatSettingNode::create(
std::static_pointer_cast<FloatSetting>(shared_from_this()), width
);
}
SettingNode* StringSetting::createNode(float width) {
return StringSettingNode::create(
std::static_pointer_cast<StringSetting>(shared_from_this()), width
);
}
SettingNode* FileSetting::createNode(float width) {
return FileSettingNode::create(
std::static_pointer_cast<FileSetting>(shared_from_this()), width
);
}
SettingNode* ColorSetting::createNode(float width) {
return ColorSettingNode::create(
std::static_pointer_cast<ColorSetting>(shared_from_this()), width
);
}
SettingNode* ColorAlphaSetting::createNode(float width) {
return ColorAlphaSettingNode::create(
std::static_pointer_cast<ColorAlphaSetting>(shared_from_this()), width
);
}
SettingChangedEvent::SettingChangedEvent(
std::string const& modID, Setting* setting
) :
m_modID(modID),
m_setting(setting) {}
std::string SettingChangedEvent::getModID() const {
return m_modID;
}
Setting* SettingChangedEvent::getSetting() const {
return m_setting;
}
SettingChangedFilter::SettingChangedFilter(
std::string const& modID,
std::optional<std::string> const& settingKey
) : m_modID(modID), m_targetKey(settingKey) {}

View file

@ -3,12 +3,20 @@
USE_GEODE_NAMESPACE();
void SettingNodeDelegate::settingValueChanged(SettingNode* node) {}
void SettingNode::dispatchChanged() {
if (m_delegate) {
m_delegate->settingValueChanged(this);
}
}
void SettingNodeDelegate::settingValueCommitted(SettingNode* node) {}
void SettingNode::dispatchCommitted() {
if (m_delegate) {
m_delegate->settingValueCommitted(this);
}
}
bool SettingNode::init(std::shared_ptr<Setting> setting) {
m_setting = setting;
bool SettingNode::init(SettingValue* value) {
m_value = value;
return true;
}

View file

@ -99,12 +99,9 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) {
return TRUE;
}
#endif
#define $_ GEODE_CONCAT(unnamedVar_, __LINE__)
static auto $_ =
listenForSettingChanges<BoolSetting>("show-platform-console", [](BoolSetting* setting) {
if (setting->getValue()) {
$execute {
listenForSettingChanges("show-platform-console", +[](bool value) {
if (value) {
Loader::get()->openPlatformConsole();
}
else {
@ -112,15 +109,15 @@ static auto $_ =
}
});
static auto $_ = listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json {
listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json {
return "Hello from Geode!";
});
});
static auto $_ = listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json {
listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json {
return Loader::get()->getInternalMod()->getModInfo();
});
});
static auto $_ = listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json {
listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json {
std::vector<nlohmann::json> res;
auto args = event->messageData;
@ -142,7 +139,8 @@ static auto $_ = listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json
}
return res;
});
});
}
int geodeEntry(void* platformData) {
// setup internals

View file

@ -1,32 +0,0 @@
#include <Geode/utils/string.hpp>
USE_GEODE_NAMESPACE();
std::string expandKnownLink(std::string const& link) {
switch (hash(utils::string::toLower(link).c_str())) {
case hash("github"):
if (!utils::string::contains(link, "/")) {
return "https://github.com/" + link;
}
break;
case hash("youtube"):
if (!utils::string::contains(link, "/")) {
return "https://youtube.com/channel/" + link;
}
break;
case hash("twitter"):
if (!utils::string::contains(link, "/")) {
return "https://twitter.com/" + link;
}
break;
case hash("newgrounds"):
if (!utils::string::contains(link, "/")) {
return "https://" + link + ".newgrounds.com";
}
break;
}
return link;
}

View file

@ -14,6 +14,7 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/ui/ListView.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/ranges.hpp>
#define FTS_FUZZY_MATCH_IMPLEMENTATION
#include <Geode/external/fts/fts_fuzzy_match.h>

View file

@ -8,6 +8,107 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
// Helpers
template <class Num>
Num parseNumForInput(std::string const& str) {
try {
if constexpr (std::is_same_v<Num, int64_t>) {
return std::stoll(str);
}
else if constexpr (std::is_same_v<Num, double>) {
return std::stod(str);
}
else {
static_assert(!std::is_same_v<Num, Num>, "Impl Num for parseNumForInput");
}
}
catch (...) {
return 0;
}
}
template<class T>
static float valueToSlider(T const& setting, typename T::ValueType value) {
auto min = setting.min ? setting.min.value() : -100;
auto max = setting.max ? setting.max.value() : +100;
auto range = max - min;
return static_cast<float>(clamp(static_cast<double>(value - min) / range, 0.0, 1.0));
}
template<class T>
static typename T::ValueType valueFromSlider(T const& setting, float num) {
auto min = setting.min ? setting.min.value() : -100;
auto max = setting.max ? setting.max.value() : +100;
auto range = max - min;
auto value = static_cast<typename T::ValueType>(num * range + min);
if (auto step = setting.controls.sliderStep) {
value = static_cast<typename T::ValueType>(
round(value / step.value()) * step.value()
);
}
return value;
}
template<class C, class T>
InputNode* createInput(C* node, GeodeSettingValue<T>* setting, float width) {
auto input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt");
input->setPosition({
-(width / 2 - 70.f) / 2,
setting->castDefinition().controls.slider ? 5.f : 0.f
});
input->setScale(.65f);
input->getInput()->setDelegate(node);
return input;
}
template<class C, class T>
Slider* createSlider(C* node, GeodeSettingValue<T>* setting, float width) {
auto slider = Slider::create(
node, menu_selector(C::onSlider), .5f
);
slider->setPosition(-50.f, -15.f);
node->updateSlider();
return slider;
}
template<class C, class T>
std::pair<
CCMenuItemSpriteExtra*, CCMenuItemSpriteExtra*
> createArrows(C* node, GeodeSettingValue<T>* setting, float width, bool big) {
auto yPos = setting->castDefinition().controls.slider ? 5.f : 0.f;
auto decArrowSpr = CCSprite::createWithSpriteFrameName(
big ? "double-nav.png"_spr : "navArrowBtn_001.png"
);
decArrowSpr->setFlipX(true);
decArrowSpr->setScale(.3f);
auto decArrow = CCMenuItemSpriteExtra::create(
decArrowSpr, node, menu_selector(C::onArrow)
);
decArrow->setPosition(-width / 2 + (big ? 65.f : 80.f), yPos);
decArrow->setTag(big ?
-setting->castDefinition().controls.bigArrowStep :
-setting->castDefinition().controls.arrowStep
);
auto incArrowSpr = CCSprite::createWithSpriteFrameName(
big ? "double-nav.png"_spr : "navArrowBtn_001.png"
);
incArrowSpr->setScale(.3f);
auto incArrow = CCMenuItemSpriteExtra::create(
incArrowSpr, node, menu_selector(C::onArrow)
);
incArrow->setTag(big ?
setting->castDefinition().controls.bigArrowStep :
setting->castDefinition().controls.arrowStep
);
incArrow->setPosition(big ? 5.f : -10.f, yPos);
return { decArrow, incArrow };
}
// BoolSettingNode
void BoolSettingNode::valueChanged(bool updateText) {
@ -21,7 +122,7 @@ void BoolSettingNode::onToggle(CCObject*) {
m_toggle->toggle(!m_uncommittedValue);
}
bool BoolSettingNode::setup(std::shared_ptr<BoolSetting> setting, float width) {
bool BoolSettingNode::setup(BoolSettingValue* setting, float width) {
m_toggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(BoolSettingNode::onToggle), .6f
);
@ -34,42 +135,172 @@ bool BoolSettingNode::setup(std::shared_ptr<BoolSetting> setting, float width) {
// IntSettingNode
float IntSettingNode::setupHeight(std::shared_ptr<IntSetting> setting) const {
return setting->hasSlider() ? 55.f : 40.f;
float IntSettingNode::setupHeight(IntSettingValue* setting) const {
return setting->castDefinition().controls.slider ? 55.f : 40.f;
}
bool IntSettingNode::setup(std::shared_ptr<IntSetting> setting, float width) {
this->setupArrows(setting, width);
this->setupInput(setting, width);
this->setupSlider(setting, width);
return true;
void IntSettingNode::onSlider(CCObject* slider) {
m_uncommittedValue = valueFromSlider(
setting()->castDefinition(),
static_cast<SliderThumb*>(slider)->getValue()
);
this->valueChanged(true);
}
void IntSettingNode::valueChanged(bool updateText) {
GeodeSettingNode::valueChanged(updateText);
this->updateLabel(updateText);
if (updateText) {
this->updateLabel();
}
this->updateSlider();
}
void IntSettingNode::updateSlider() {
if (m_slider) {
m_slider->setValue(valueToSlider(
setting()->castDefinition(),
m_uncommittedValue
));
m_slider->updateBar();
}
}
void IntSettingNode::updateLabel() {
if (m_input) {
// hacky way to make setString not called textChanged
m_input->getInput()->setDelegate(nullptr);
m_input->setString(numToString(m_uncommittedValue));
m_input->getInput()->setDelegate(this);
}
else {
m_label->setString(numToString(m_uncommittedValue).c_str());
m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f);
}
}
void IntSettingNode::onArrow(CCObject* sender) {
m_uncommittedValue += sender->getTag();
this->valueChanged(true);
}
void IntSettingNode::textChanged(CCTextInputNode* input) {
m_uncommittedValue = parseNumForInput<ValueType>(input->getString());
this->valueChanged(false);
}
bool IntSettingNode::setup(IntSettingValue* setting, float width) {
if (setting->castDefinition().controls.input) {
m_menu->addChild(m_input = createInput(this, setting, width));
}
else {
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setPosition({
-(width / 2 - 70.f) / 2,
setting->castDefinition().controls.slider ? 5.f : 0.f
});
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
m_menu->addChild(m_label);
}
if (setting->castDefinition().controls.slider) {
m_menu->addChild(m_slider = createSlider(this, setting, width));
}
if (setting->castDefinition().controls.arrows) {
auto [dec, inc] = createArrows(this, setting, width, false);
m_menu->addChild(m_decArrow = dec);
m_menu->addChild(m_incArrow = inc);
}
if (setting->castDefinition().controls.bigArrows) {
auto [dec, inc] = createArrows(this, setting, width, true);
m_menu->addChild(m_bigDecArrow = dec);
m_menu->addChild(m_bigIncArrow = inc);
}
return true;
}
// FloatSettingNode
float FloatSettingNode::setupHeight(std::shared_ptr<FloatSetting> setting) const {
return setting->hasSlider() ? 55.f : 40.f;
float FloatSettingNode::setupHeight(FloatSettingValue* setting) const {
return setting->castDefinition().controls.slider ? 55.f : 40.f;
}
bool FloatSettingNode::setup(std::shared_ptr<FloatSetting> setting, float width) {
this->setupArrows(setting, width);
this->setupInput(setting, width);
this->setupSlider(setting, width);
return true;
void FloatSettingNode::onSlider(CCObject* slider) {
m_uncommittedValue = valueFromSlider(
setting()->castDefinition(),
static_cast<SliderThumb*>(slider)->getValue()
);
this->valueChanged(true);
}
void FloatSettingNode::valueChanged(bool updateText) {
GeodeSettingNode::valueChanged(updateText);
this->updateLabel(updateText);
if (updateText) {
this->updateLabel();
}
this->updateSlider();
}
void FloatSettingNode::updateSlider() {
if (m_slider) {
m_slider->setValue(valueToSlider(
setting()->castDefinition(),
m_uncommittedValue
));
m_slider->updateBar();
}
}
void FloatSettingNode::updateLabel() {
if (m_input) {
// hacky way to make setString not called textChanged
m_input->getInput()->setDelegate(nullptr);
m_input->setString(numToString(m_uncommittedValue));
m_input->getInput()->setDelegate(this);
}
else {
m_label->setString(numToString(m_uncommittedValue).c_str());
m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f);
}
}
void FloatSettingNode::onArrow(CCObject* sender) {
m_uncommittedValue += sender->getTag();
this->valueChanged(true);
}
void FloatSettingNode::textChanged(CCTextInputNode* input) {
m_uncommittedValue = parseNumForInput<ValueType>(input->getString());
this->valueChanged(false);
}
bool FloatSettingNode::setup(FloatSettingValue* setting, float width) {
if (setting->castDefinition().controls.input) {
m_menu->addChild(m_input = createInput(this, setting, width));
}
else {
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setPosition({
-(width / 2 - 70.f) / 2,
setting->castDefinition().controls.slider ? 5.f : 0.f
});
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
m_menu->addChild(m_label);
}
if (setting->castDefinition().controls.slider) {
m_menu->addChild(m_slider = createSlider(this, setting, width));
}
if (setting->castDefinition().controls.arrows) {
auto [dec, inc] = createArrows(this, setting, width, false);
m_menu->addChild(m_decArrow = dec);
m_menu->addChild(m_incArrow = inc);
}
if (setting->castDefinition().controls.bigArrows) {
auto [dec, inc] = createArrows(this, setting, width, true);
m_menu->addChild(m_bigDecArrow = dec);
m_menu->addChild(m_bigIncArrow = inc);
}
return true;
}
// StringSettingNode
void StringSettingNode::updateLabel() {
@ -89,7 +320,7 @@ void StringSettingNode::valueChanged(bool updateText) {
this->updateLabel();
}
bool StringSettingNode::setup(std::shared_ptr<StringSetting> setting, float width) {
bool StringSettingNode::setup(StringSettingValue* setting, float width) {
m_input = InputNode::create(width / 2 - 10.f, "Text", "chatFont.fnt");
m_input->setPosition({ -(width / 2 - 70.f) / 2, .0f });
m_input->setScale(.65f);
@ -119,12 +350,11 @@ void FileSettingNode::valueChanged(bool updateText) {
}
void FileSettingNode::onPickFile(CCObject*) {
auto setting = std::static_pointer_cast<FileSetting>(m_setting);
if (auto path = file::pickFile(
file::PickMode::OpenFile,
{
dirs::getGameDir(),
setting->getFileFilters().value_or(std::vector<file::FilePickOptions::Filter>())
setting()->castDefinition().controls.filters
}
)) {
m_uncommittedValue = path.unwrap();
@ -132,7 +362,7 @@ void FileSettingNode::onPickFile(CCObject*) {
}
}
bool FileSettingNode::setup(std::shared_ptr<FileSetting> setting, float width) {
bool FileSettingNode::setup(FileSettingValue* setting, float width) {
m_input = InputNode::create(width / 2 - 30.f, "Path to File", "chatFont.fnt");
m_input->setPosition({ -(width / 2 - 80.f) / 2 - 15.f, .0f });
m_input->setScale(.65f);
@ -142,8 +372,9 @@ bool FileSettingNode::setup(std::shared_ptr<FileSetting> setting, float width) {
auto fileBtnSpr = CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png");
fileBtnSpr->setScale(.5f);
auto fileBtn =
CCMenuItemSpriteExtra::create(fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile));
auto fileBtn = CCMenuItemSpriteExtra::create(
fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile)
);
fileBtn->setPosition(.0f, .0f);
m_menu->addChild(fileBtn);
@ -169,7 +400,7 @@ void ColorSettingNode::onSelectColor(CCObject*) {
popup->show();
}
bool ColorSettingNode::setup(std::shared_ptr<ColorSetting> setting, float width) {
bool ColorSettingNode::setup(ColorSettingValue* setting, float width) {
m_colorSpr = ColorChannelSprite::create();
m_colorSpr->setColor(m_uncommittedValue);
m_colorSpr->setScale(.65f);
@ -203,7 +434,7 @@ void ColorAlphaSettingNode::onSelectColor(CCObject*) {
popup->show();
}
bool ColorAlphaSettingNode::setup(std::shared_ptr<ColorAlphaSetting> setting, float width) {
bool ColorAlphaSettingNode::setup(ColorAlphaSettingValue* setting, float width) {
m_colorSpr = ColorChannelSprite::create();
m_colorSpr->setColor(to3B(m_uncommittedValue));
m_colorSpr->updateOpacity(m_uncommittedValue.a / 255.f);
@ -217,3 +448,53 @@ bool ColorAlphaSettingNode::setup(std::shared_ptr<ColorAlphaSetting> setting, fl
return true;
}
// CustomSettingPlaceholderNode
void CustomSettingPlaceholderNode::commit() {
}
bool CustomSettingPlaceholderNode::hasUncommittedChanges() {
return false;
}
bool CustomSettingPlaceholderNode::hasNonDefaultValue() {
return false;
}
void CustomSettingPlaceholderNode::resetToDefault() {
}
bool CustomSettingPlaceholderNode::init(std::string const& key, float width) {
if (!SettingNode::init(nullptr))
return false;
constexpr auto sidePad = 40.f;
this->setContentSize({ width, 40.f });
auto info = CCLabelBMFont::create(
"You need to enable the mod to modify this setting.",
"bigFont.fnt"
);
info->setAnchorPoint({ .0f, .5f });
info->limitLabelWidth(width - sidePad, .5f, .1f);
info->setPosition({ sidePad / 2, m_obContentSize.height / 2 });
this->addChild(info);
return true;
}
CustomSettingPlaceholderNode* CustomSettingPlaceholderNode::create(
std::string const& key, float width
) {
auto ret = new CustomSettingPlaceholderNode;
if (ret && ret->init(key, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -15,38 +15,36 @@
USE_GEODE_NAMESPACE();
namespace {
template <class Num>
Num parseNumForInput(std::string const& str) {
try {
if constexpr (std::is_same_v<Num, int64_t>) {
return std::stoll(str);
}
else if constexpr (std::is_same_v<Num, double>) {
return std::stod(str);
}
}
catch (...) {
return 0;
}
#define IMPL_SETT_CREATE(type_) \
static type_##SettingNode* create( \
type_##SettingValue* value, float width \
) { \
auto ret = new type_##SettingNode; \
if (ret && ret->init(value, width)) { \
ret->autorelease(); \
return ret; \
} \
CC_SAFE_DELETE(ret); \
return nullptr; \
}
template <class N, class T>
class GeodeSettingNode : public SettingNode {
public:
using value_t = typename T::value_t;
template <class T>
class GeodeSettingNode : public SettingNode {
public:
using ValueType = typename T::ValueType;
protected:
protected:
float m_width;
float m_height;
value_t m_uncommittedValue;
ValueType m_uncommittedValue;
CCMenu* m_menu;
CCLabelBMFont* m_nameLabel;
CCLabelBMFont* m_errorLabel;
CCMenuItemSpriteExtra* m_resetBtn;
bool init(std::shared_ptr<T> setting, float width) {
if (!SettingNode::init(std::static_pointer_cast<Setting>(setting))) return false;
bool init(GeodeSettingValue<T>* setting, float width) {
if (!SettingNode::init(setting))
return false;
m_width = width;
m_height = this->setupHeight(setting);
@ -56,7 +54,7 @@ namespace {
m_uncommittedValue = setting->getValue();
auto name = setting->getDisplayName();
auto name = setting->getDefinition().getDisplayName();
m_nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
m_nameLabel->setAnchorPoint({ .0f, .5f });
@ -79,7 +77,7 @@ namespace {
float btnPos = 15.f;
if (setting->getDescription()) {
if (setting->castDefinition().description) {
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.6f);
@ -96,7 +94,6 @@ namespace {
btnPos += 20.f;
}
if (setting->canResetToDefault()) {
auto resetBtnSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr);
resetBtnSpr->setScale(.5f);
@ -109,7 +106,6 @@ namespace {
.0f
);
m_menu->addChild(m_resetBtn);
}
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
m_menu->registerWithTouchDispatcher();
@ -122,34 +118,35 @@ namespace {
}
void onDescription(CCObject*) {
auto setting = std::static_pointer_cast<T>(m_setting);
FLAlertLayer::create(
setting->getDisplayName().c_str(), setting->getDescription().value(), "OK"
)
->show();
setting()->getDefinition().getDisplayName().c_str(),
setting()->castDefinition().description.value(),
"OK"
)->show();
}
void onReset(CCObject*) {
auto setting = std::static_pointer_cast<T>(m_setting);
createQuickPopup(
"Reset",
"Are you sure you want to <cr>reset</c> <cl>" + setting->getDisplayName() +
"Are you sure you want to <cr>reset</c> <cl>" +
setting()->getDefinition().getDisplayName() +
"</c> to <cy>default</c>?",
"Cancel", "Reset",
[this](auto, bool btn2) {
if (btn2) this->resetToDefault();
if (btn2) {
this->resetToDefault();
}
}
);
}
virtual float setupHeight(std::shared_ptr<T> setting) const {
virtual float setupHeight(GeodeSettingValue<T>* setting) const {
return 40.f;
}
virtual bool setup(std::shared_ptr<T> setting, float width) = 0;
virtual bool setup(GeodeSettingValue<T>* setting, float width) = 0;
virtual void valueChanged(bool updateText = true) {
if (m_delegate) m_delegate->settingValueChanged(this);
if (this->hasUncommittedChanges()) {
m_nameLabel->setColor(cc3x(0x1d0));
}
@ -157,7 +154,7 @@ namespace {
m_nameLabel->setColor(cc3x(0xfff));
}
if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue());
auto isValid = std::static_pointer_cast<T>(m_setting)->isValidValue(m_uncommittedValue);
auto isValid = setting()->validate(m_uncommittedValue);
if (!isValid) {
m_errorLabel->setVisible(true);
m_errorLabel->setString(isValid.unwrapErr().c_str());
@ -165,285 +162,109 @@ namespace {
else {
m_errorLabel->setVisible(false);
}
this->dispatchChanged();
}
public:
static N* create(std::shared_ptr<T> setting, float width) {
auto ret = new N();
if (ret && ret->init(setting, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
GeodeSettingValue<T>* setting() {
return static_cast<GeodeSettingValue<T>*>(m_value);
}
public:
void commit() override {
std::static_pointer_cast<T>(m_setting)->setValue(m_uncommittedValue);
m_uncommittedValue = std::static_pointer_cast<T>(m_setting)->getValue();
setting()->setValue(m_uncommittedValue);
m_uncommittedValue = setting()->getValue();
this->valueChanged();
if (m_delegate) m_delegate->settingValueCommitted(this);
this->dispatchCommitted();
}
bool hasUncommittedChanges() override {
return m_uncommittedValue != std::static_pointer_cast<T>(m_setting)->getValue();
return m_uncommittedValue != setting()->getValue();
}
bool hasNonDefaultValue() override {
return m_uncommittedValue != std::static_pointer_cast<T>(m_setting)->getDefault();
return m_uncommittedValue != setting()->castDefinition().defaultValue;
}
void resetToDefault() override {
m_uncommittedValue = std::static_pointer_cast<T>(m_setting)->getDefault();
m_uncommittedValue = setting()->castDefinition().defaultValue;
this->valueChanged();
}
};
};
template <class C, class T>
class ImplInput : public TextInputDelegate {
protected:
InputNode* m_input = nullptr;
CCLabelBMFont* m_label = nullptr;
C* self() {
return static_cast<C*>(this);
}
void setupInput(std::shared_ptr<T> setting, float width) {
if (setting->hasInput()) {
m_input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt");
m_input->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f });
m_input->setScale(.65f);
m_input->getInput()->setDelegate(this);
self()->m_menu->addChild(m_input);
}
else {
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f });
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
self()->m_menu->addChild(m_label);
}
}
void updateLabel(bool updateText) {
if (!updateText) return;
if (m_input) {
// hacky way to make setString not called textChanged
m_input->getInput()->setDelegate(nullptr);
m_input->setString(numToString(self()->m_uncommittedValue));
m_input->getInput()->setDelegate(this);
}
else {
m_label->setString(numToString(self()->m_uncommittedValue).c_str());
m_label->limitLabelWidth(self()->m_width / 2 - 70.f, .5f, .1f);
}
}
void textChanged(CCTextInputNode* input) override {
try {
self()->m_uncommittedValue =
parseNumForInput<typename C::value_t>(input->getString());
self()->valueChanged(false);
}
catch (...) {
}
}
};
template <class C, class T>
class ImplArrows {
protected:
CCMenuItemSpriteExtra* m_decArrow = nullptr;
CCMenuItemSpriteExtra* m_incArrow = nullptr;
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
C* self() {
return static_cast<C*>(this);
}
void setupArrows(std::shared_ptr<T> setting, float width) {
auto yPos = setting->hasSlider() ? 5.f : 0.f;
if (setting->hasArrows()) {
auto decArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
decArrowSpr->setFlipX(true);
decArrowSpr->setScale(.3f);
m_decArrow = CCMenuItemSpriteExtra::create(
decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onDecrement)
);
m_decArrow->setPosition(-width / 2 + 80.f, yPos);
self()->m_menu->addChild(m_decArrow);
auto incArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
incArrowSpr->setScale(.3f);
m_incArrow = CCMenuItemSpriteExtra::create(
incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onIncrement)
);
m_incArrow->setPosition(-10.f, yPos);
self()->m_menu->addChild(m_incArrow);
}
if (setting->hasBigArrows()) {
auto decArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr);
decArrowSpr->setFlipX(true);
decArrowSpr->setScale(.3f);
m_bigDecArrow = CCMenuItemSpriteExtra::create(
decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigDecrement)
);
m_bigDecArrow->setPosition(-width / 2 + 65.f, yPos);
self()->m_menu->addChild(m_bigDecArrow);
auto incArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr);
incArrowSpr->setScale(.3f);
m_bigIncArrow = CCMenuItemSpriteExtra::create(
incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigIncrement)
);
m_bigIncArrow->setPosition(5.f, yPos);
self()->m_menu->addChild(m_bigIncArrow);
}
}
// intentionally a subclass to make it relatively safe to
// use as callbacks for the child class, since we give the
// child class as the target to CCMenuItemSpriteExtra
struct Callbacks : public C {
void onIncrement(CCObject*) {
this->m_uncommittedValue +=
std::static_pointer_cast<T>(this->m_setting)->getArrowStepSize();
this->valueChanged(true);
}
void onDecrement(CCObject*) {
this->m_uncommittedValue -=
std::static_pointer_cast<T>(this->m_setting)->getArrowStepSize();
this->valueChanged(true);
}
void onBigIncrement(CCObject*) {
this->m_uncommittedValue +=
std::static_pointer_cast<T>(this->m_setting)->getBigArrowStepSize();
this->valueChanged(true);
}
void onBigDecrement(CCObject*) {
this->m_uncommittedValue -=
std::static_pointer_cast<T>(this->m_setting)->getBigArrowStepSize();
this->valueChanged(true);
}
};
};
template <class C, class T>
class ImplSlider {
protected:
Slider* m_slider = nullptr;
C* self() {
return static_cast<C*>(this);
}
static float valueToSlider(std::shared_ptr<T> setting, typename T::value_t num) {
auto min = setting->getMin() ? setting->getMin().value() : -100;
auto max = setting->getMax() ? setting->getMax().value() : +100;
auto range = max - min;
return static_cast<float>(clamp(static_cast<double>(num - min) / range, 0.0, 1.0));
}
static typename T::value_t valueFromSlider(std::shared_ptr<T> setting, float num) {
auto min = setting->getMin() ? setting->getMin().value() : -100;
auto max = setting->getMax() ? setting->getMax().value() : +100;
auto range = max - min;
auto value = static_cast<typename T::value_t>(num * range + min);
if (auto step = setting->getSliderStepSize()) {
value =
static_cast<typename T::value_t>(round(value / step.value()) * step.value());
}
return value;
}
void setupSlider(std::shared_ptr<T> setting, float width) {
if (setting->hasSlider()) {
m_slider =
Slider::create(self(), menu_selector(ImplSlider::Callbacks::onSlider), .5f);
m_slider->setPosition(-50.f, -15.f);
self()->m_menu->addChild(m_slider);
this->updateSlider();
}
}
void updateSlider() {
if (m_slider) {
auto setting = std::static_pointer_cast<T>(self()->m_setting);
m_slider->setValue(valueToSlider(setting, self()->m_uncommittedValue));
m_slider->updateBar();
}
}
// intentionally a subclass to make it relatively safe to
// use as callbacks for the child class, since we give the
// child class as the target to CCMenuItemSpriteExtra
struct Callbacks : public C {
void onSlider(CCObject* slider) {
auto setting = std::static_pointer_cast<T>(this->m_setting);
this->m_uncommittedValue =
valueFromSlider(setting, static_cast<SliderThumb*>(slider)->getValue());
this->valueChanged(true);
}
};
};
}
class BoolSettingNode : public GeodeSettingNode<BoolSettingNode, BoolSetting> {
class BoolSettingNode : public GeodeSettingNode<BoolSetting> {
protected:
CCMenuItemToggler* m_toggle;
void onToggle(CCObject*);
void valueChanged(bool updateText) override;
bool setup(std::shared_ptr<BoolSetting> setting, float width) override;
bool setup(BoolSettingValue* setting, float width) override;
public:
IMPL_SETT_CREATE(Bool);
};
class IntSettingNode :
public GeodeSettingNode<IntSettingNode, IntSetting>,
public ImplArrows<IntSettingNode, IntSetting>,
public ImplInput<IntSettingNode, IntSetting>,
public ImplSlider<IntSettingNode, IntSetting> {
public GeodeSettingNode<IntSetting>,
public TextInputDelegate
{
protected:
friend class ImplArrows<IntSettingNode, IntSetting>;
friend class ImplInput<IntSettingNode, IntSetting>;
friend class ImplSlider<IntSettingNode, IntSetting>;
InputNode* m_input = nullptr;
CCLabelBMFont* m_label = nullptr;
Slider* m_slider = nullptr;
CCMenuItemSpriteExtra* m_decArrow = nullptr;
CCMenuItemSpriteExtra* m_incArrow = nullptr;
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
void valueChanged(bool updateText) override;
void textChanged(CCTextInputNode* input) override;
float setupHeight(std::shared_ptr<IntSetting> setting) const override;
bool setup(std::shared_ptr<IntSetting> setting, float width) override;
float setupHeight(IntSettingValue* setting) const override;
bool setup(IntSettingValue* setting, float width) override;
public:
void updateSlider();
void updateLabel();
void onSlider(CCObject* slider);
void onArrow(CCObject* sender);
IMPL_SETT_CREATE(Int);
};
class FloatSettingNode :
public GeodeSettingNode<FloatSettingNode, FloatSetting>,
public ImplArrows<FloatSettingNode, FloatSetting>,
public ImplInput<FloatSettingNode, FloatSetting>,
public ImplSlider<FloatSettingNode, FloatSetting> {
public GeodeSettingNode<FloatSetting>,
public TextInputDelegate
{
protected:
friend class ImplArrows<FloatSettingNode, FloatSetting>;
friend class ImplInput<FloatSettingNode, FloatSetting>;
friend class ImplSlider<FloatSettingNode, FloatSetting>;
InputNode* m_input = nullptr;
CCLabelBMFont* m_label = nullptr;
Slider* m_slider = nullptr;
CCMenuItemSpriteExtra* m_decArrow = nullptr;
CCMenuItemSpriteExtra* m_incArrow = nullptr;
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
void valueChanged(bool updateText) override;
void textChanged(CCTextInputNode* input) override;
float setupHeight(std::shared_ptr<FloatSetting> setting) const override;
bool setup(std::shared_ptr<FloatSetting> setting, float width) override;
float setupHeight(FloatSettingValue* setting) const override;
bool setup(FloatSettingValue* setting, float width) override;
public:
void updateSlider();
void updateLabel();
void onSlider(CCObject* slider);
void onArrow(CCObject* sender);
IMPL_SETT_CREATE(Float);
};
class StringSettingNode :
public GeodeSettingNode<StringSettingNode, StringSetting>,
public TextInputDelegate {
public GeodeSettingNode<StringSetting>,
public TextInputDelegate
{
protected:
InputNode* m_input;
@ -451,12 +272,16 @@ protected:
void valueChanged(bool updateText) override;
void updateLabel();
bool setup(std::shared_ptr<StringSetting> setting, float width) override;
bool setup(StringSettingValue* setting, float width) override;
public:
IMPL_SETT_CREATE(String);
};
class FileSettingNode :
public GeodeSettingNode<FileSettingNode, FileSetting>,
public TextInputDelegate {
public GeodeSettingNode<FileSetting>,
public TextInputDelegate
{
protected:
InputNode* m_input;
@ -466,11 +291,14 @@ protected:
void onPickFile(CCObject*);
bool setup(std::shared_ptr<FileSetting> setting, float width) override;
bool setup(FileSettingValue* setting, float width) override;
public:
IMPL_SETT_CREATE(File);
};
class ColorSettingNode :
public GeodeSettingNode<ColorSettingNode, ColorSetting>,
public GeodeSettingNode<ColorSetting>,
public ColorPickPopupDelegate {
protected:
ColorChannelSprite* m_colorSpr;
@ -480,11 +308,14 @@ protected:
void onSelectColor(CCObject*);
bool setup(std::shared_ptr<ColorSetting> setting, float width) override;
bool setup(ColorSettingValue* setting, float width) override;
public:
IMPL_SETT_CREATE(Color);
};
class ColorAlphaSettingNode :
public GeodeSettingNode<ColorAlphaSettingNode, ColorAlphaSetting>,
public GeodeSettingNode<ColorAlphaSetting>,
public ColorPickPopupDelegate {
protected:
ColorChannelSprite* m_colorSpr;
@ -494,5 +325,23 @@ protected:
void onSelectColor(CCObject*);
bool setup(std::shared_ptr<ColorAlphaSetting> setting, float width) override;
bool setup(ColorAlphaSettingValue* setting, float width) override;
public:
IMPL_SETT_CREATE(ColorAlpha);
};
class CustomSettingPlaceholderNode : public SettingNode {
protected:
void commit() override;
bool hasUncommittedChanges() override;
bool hasNonDefaultValue() override;
void resetToDefault() override;
bool init(std::string const& key, float width);
public:
static CustomSettingPlaceholderNode* create(
std::string const& key, float width
);
};

View file

@ -6,6 +6,7 @@
#include <Geode/ui/ScrollLayer.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/ui/General.hpp>
#include "GeodeSettingNode.hpp"
bool ModSettingsPopup::setup(Mod* mod) {
m_noElasticity = true;
@ -31,8 +32,14 @@ bool ModSettingsPopup::setup(Mod* mod) {
float totalHeight = .0f;
std::vector<CCNode*> rendered;
bool hasBG = true;
for (auto& [_, sett] : mod->getSettings()) {
auto node = sett->createNode(layerSize.width);
for (auto& key : mod->getSettingKeys()) {
SettingNode* node;
if (auto sett = mod->getSetting(key)) {
node = sett->createNode(layerSize.width);
}
else {
node = CustomSettingPlaceholderNode::create(key, layerSize.width);
}
node->setDelegate(this);
totalHeight += node->getScaledContentSize().height;
@ -144,6 +151,17 @@ void ModSettingsPopup::onResetAll(CCObject*) {
);
}
void ModSettingsPopup::settingValueCommitted(SettingNode*) {
if (this->hasUncommitted()) {
m_applyBtnSpr->setColor(cc3x(0xf));
m_applyBtn->setEnabled(true);
}
else {
m_applyBtnSpr->setColor(cc3x(0x4));
m_applyBtn->setEnabled(false);
}
}
void ModSettingsPopup::settingValueChanged(SettingNode*) {
if (this->hasUncommitted()) {
m_applyBtnSpr->setColor(cc3x(0xf));

View file

@ -13,7 +13,8 @@ protected:
CCMenuItemSpriteExtra* m_applyBtn;
ButtonSprite* m_applyBtnSpr;
void settingValueChanged(SettingNode* node) override;
void settingValueChanged(SettingNode*) override;
void settingValueCommitted(SettingNode*) override;
bool setup(Mod* mod) override;
bool hasUncommitted() const;

View file

@ -19,6 +19,11 @@ bool JsonMaybeSomething<Json>::isError() const {
return m_checker.isError() || !m_hasValue;
}
template <class Json>
std::string JsonMaybeSomething<Json>::getError() const {
return m_checker.getError();
}
template <class Json>
JsonMaybeSomething<Json>::operator bool() const {
return !isError();

View file

@ -11,6 +11,14 @@
USE_GEODE_NAMESPACE();
using namespace geode::utils::file;
void ghc::filesystem::to_json(nlohmann::json& json, path const& path) {
json = path.string();
}
void ghc::filesystem::from_json(nlohmann::json const& json, path& path) {
path = json.get<std::string>();
}
Result<std::string> utils::file::readString(ghc::filesystem::path const& path) {
#if _WIN32
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);

View file

@ -18,4 +18,4 @@ target_link_libraries(
geode-sdk
)
create_geode_file(${PROJECT_NAME} DONT_INSTALL)
create_geode_file(${PROJECT_NAME})

View file

@ -3,6 +3,134 @@
USE_GEODE_NAMESPACE();
#include <Geode/modify/MenuLayer.hpp>
#include <Geode/loader/SettingNode.hpp>
enum class Icon {
Steve,
Mike,
LazarithTheDestroyerOfForsakenSouls,
Geoff,
};
constexpr Icon DEFAULT_ICON = Icon::Steve;
class MySettingValue;
class MySettingValue : public SettingValue {
protected:
Icon m_icon;
public:
MySettingValue(std::string const& key, Icon icon)
: SettingValue(key), m_icon(icon) {}
bool load(nlohmann::json const& json) override {
try {
m_icon = static_cast<Icon>(json.get<int>());
return true;
} catch(...) {
return false;
}
}
bool save(nlohmann::json& json) const override {
json = static_cast<int>(m_icon);
return true;
}
SettingNode* createNode(float width) override;
void setIcon(Icon icon) {
m_icon = icon;
}
Icon getIcon() const {
return m_icon;
}
};
class MySettingNode : public SettingNode {
protected:
Icon m_currentIcon;
std::vector<CCSprite*> m_sprites;
bool init(MySettingValue* value, float width) {
if (!SettingNode::init(value))
return false;
m_currentIcon = value->getIcon();
this->setContentSize({ width, 40.f });
auto menu = CCMenu::create();
menu->setPosition(width / 2, 20.f);
float x = -75.f;
for (auto& [spr, icon] : {
std::pair { "player_01_001.png", Icon::Steve, },
std::pair { "player_02_001.png", Icon::Mike, },
std::pair { "player_03_001.png", Icon::LazarithTheDestroyerOfForsakenSouls, },
std::pair { "player_04_001.png", Icon::Geoff, },
}) {
auto btnSpr = CCSprite::createWithSpriteFrameName(spr);
btnSpr->setScale(.7f);
m_sprites.push_back(btnSpr);
if (icon == m_currentIcon) {
btnSpr->setColor({ 0, 255, 0 });
} else {
btnSpr->setColor({ 200, 200, 200 });
}
auto btn = CCMenuItemSpriteExtra::create(
btnSpr, this, menu_selector(MySettingNode::onSelect)
);
btn->setTag(static_cast<int>(icon));
btn->setPosition(x, 0);
menu->addChild(btn);
x += 50.f;
}
this->addChild(menu);
return true;
}
void onSelect(CCObject* sender) {
for (auto& spr : m_sprites) {
spr->setColor({ 200, 200, 200 });
}
m_currentIcon = static_cast<Icon>(sender->getTag());
static_cast<CCSprite*>(
static_cast<CCMenuItemSpriteExtra*>(sender)->getNormalImage()
)->setColor({ 0, 255, 0 });
this->dispatchChanged();
}
public:
void commit() override {
static_cast<MySettingValue*>(m_value)->setIcon(m_currentIcon);
this->dispatchCommitted();
}
bool hasUncommittedChanges() override {
return m_currentIcon != static_cast<MySettingValue*>(m_value)->getIcon();
}
bool hasNonDefaultValue() override {
return m_currentIcon != DEFAULT_ICON;
}
void resetToDefault() override {
m_currentIcon = DEFAULT_ICON;
}
static MySettingNode* create(MySettingValue* value, float width) {
auto ret = new MySettingNode;
if (ret && ret->init(value, width)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
SettingNode* MySettingValue::createNode(float width) {
return MySettingNode::create(this, width);
}
struct MyMenuLayer : Modify<MyMenuLayer, MenuLayer> {
void onMoreGames(CCObject*) {
@ -10,13 +138,21 @@ struct MyMenuLayer : Modify<MyMenuLayer, MenuLayer> {
FLAlertLayer::create("Damn", ":(", "OK")->show();
}
else {
FLAlertLayer::create("Yay", "The weather report said it wouldn't rain today :)", "OK")
->show();
FLAlertLayer::create(
"Yay",
"The weather report said it wouldn't rain today :)",
"OK"
)->show();
}
}
};
GEODE_API bool GEODE_CALL geode_load(Mod*) {
$on_mod(Loaded) {
Mod::get()->registerCustomSetting(
"overcast-skies",
std::make_unique<MySettingValue>("overcast-skies", DEFAULT_ICON)
);
// Dispatcher::get()->addFunction<void(GJGarageLayer*)>("test-garage-open", [](GJGarageLayer*
// gl) { auto label = CCLabelBMFont::create("Dispatcher works!", "bigFont.fnt");
// label->setPosition(100, 80);
@ -24,5 +160,4 @@ GEODE_API bool GEODE_CALL geode_load(Mod*) {
// label->setZOrder(99999);
// gl->addChild(label);
// });
return true;
}

View file

@ -76,6 +76,9 @@
}
]
}
},
"overcast-skies": {
"type": "custom"
}
}
}

View file

@ -16,4 +16,4 @@ target_link_libraries(
geode-sdk
)
create_geode_file(${PROJECT_NAME} DONT_INSTALL)
create_geode_file(${PROJECT_NAME})

View file

@ -8,7 +8,7 @@
"dependencies": [
{
"id": "geode.testdep",
"version": "1.0.*",
"version": ">=1.0.0",
"required": true
}
]