geode/loader/include/Geode/loader/Setting.hpp
HJfod 1dad98b235 Result stuff
- add NewResult
 - deprecate and nodiscard old Result
 - silence some warnings
2022-10-22 17:49:09 +03:00

508 lines
17 KiB
C++

#pragma once
#include <Geode/DefaultInclude.hpp>
#include <optional>
#include <unordered_set>
#include <Geode/utils/container.hpp>
#include <Geode/utils/json.hpp>
#include <Geode/utils/Result.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/convert.hpp>
#include <Geode/utils/platform.hpp>
#include <Geode/utils/ranges.hpp>
#include <regex>
#pragma warning(push)
#pragma warning(disable: 4275)
namespace geode {
using ModJson = nlohmann::ordered_json;
class Setting;
class SettingNode;
class BoolSetting;
class IntSetting;
class FloatSetting;
class StringSetting;
struct ModInfo;
enum class SettingType {
Bool,
Int,
Float,
String,
File,
Color,
ColorAlpha,
User,
};
/**
* 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 GEODE_DLL Setting :
public std::enable_shared_from_this<Setting>
{
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
);
public:
virtual ~Setting() = default;
// Load from mod.json
static Result<std::shared_ptr<Setting>> parse(
std::string const& key,
ModJson const& json
);
// Load value from saved settings
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;
void valueChanged();
std::string getKey() const;
virtual SettingType getType() const = 0;
};
// 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.error());\
}
#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;
friend class Setting;
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);
}
public:
using value_t = ValueType;
std::optional<std::string> getName() const {
return m_name;
}
std::string getDisplayName() const {
return m_name.value_or(m_key);
}
std::optional<std::string> getDescription() const {
return m_description;
}
ValueType getDefault() const {
return m_default;
}
ValueType getValue() const {
return m_value;
}
void setValue(ValueType const& value) {
m_value = value;
if constexpr (std::is_base_of_v<IMinMax<ValueType>, Class>) {
(void)static_cast<Class*>(this)->constrainMinMax(m_value);
}
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())
);
}
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;
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 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 GEODE_DLL BoolSetting :
public GeodeSetting<BoolSetting, bool, SettingType::Bool>
{
public:
SettingNode* createNode(float width) override;
};
class GEODE_DLL 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:
SettingNode* createNode(float width) override;
};
class GEODE_DLL FloatSetting :
public GeodeSetting<FloatSetting, double, SettingType::Float>,
public IOneOf<FloatSetting, double>,
public IMinMax<double>,
public ICArrows, public ICSlider<double>, public ICInput
{
public:
SettingNode* createNode(float width) override;
};
class GEODE_DLL StringSetting :
public GeodeSetting<StringSetting, std::string, SettingType::String>,
public IOneOf<StringSetting, std::string>,
public IMatch<StringSetting, std::string>
{
public:
SettingNode* createNode(float width) override;
};
class GEODE_DLL FileSetting :
public GeodeSetting<FileSetting, ghc::filesystem::path, SettingType::File>,
public ICFileFilters
{
public:
SettingNode* createNode(float width) override;
};
class GEODE_DLL ColorSetting :
public GeodeSetting<ColorSetting, cocos2d::ccColor3B, SettingType::Color>
{
public:
SettingNode* createNode(float width) override;
};
class GEODE_DLL ColorAlphaSetting :
public GeodeSetting<ColorAlphaSetting, cocos2d::ccColor4B, SettingType::ColorAlpha>
{
public:
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;\
}\
}
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 {
static_assert(!std::is_same_v<T, T>, "todo: implement");
}
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 {
static_assert(!std::is_same_v<T, T>, "todo: implement");
}
}
}
#pragma warning(pop)