#pragma once #include "globalincs/flagset.h" #include "globalincs/pstypes.h" #include "libs/jansson.h" #include "options/OptionsManager.h" #include #include namespace options { enum class OptionType { Range, Selection }; enum class ExpertLevel { Beginner, Advanced, Expert }; enum class PresetKind { Basic, Low, Medium, High, Ultra }; // clang-format off FLAG_LIST(OptionFlags) { /** * @brief Force the option menu to display this as a multi value selection. * * This can be useful if an option typically has multiple values but it is also possible for * this option to have exactly two values which may cause the option menu to display it as a * boolean option even though the option is not designed for that. */ ForceMultiValueSelection = 0, NUM_VALUES }; // clang-format on struct ValueDescription { const SCP_string display; const SCP_string serialized; ValueDescription(SCP_string _display, SCP_string _serialized); }; class OptionBase { protected: OptionsManager* _parent = nullptr; ExpertLevel _expert_level = ExpertLevel::Expert; SCP_string _config_key; SCP_string _category = "Other"; SCP_string _title; SCP_string _description; int _importance = 0; SCP_unordered_map _preset_values; flagset _flags; std::unique_ptr getConfigValue() const; OptionBase(SCP_string config_key, SCP_string title, SCP_string description); public: virtual ~OptionBase() = 0; ExpertLevel getExpertLevel() const; void setExpertLevel(ExpertLevel expert_level); const SCP_string& getCategory() const; void setCategory(const SCP_string& category); int getImportance() const; void setImportance(int importance); const flagset& getFlags() const; void setFlags(const flagset& flags); const SCP_string& getConfigKey() const; const SCP_string& getTitle() const; const SCP_string& getDescription() const; void setPreset(PresetKind preset, const SCP_string& value); bool persistChanges() const; virtual bool valueChanged(const json_t* val) const = 0; virtual ValueDescription getCurrentValueDescription() const = 0; virtual void setValueDescription(const ValueDescription& desc) const = 0; virtual OptionType getType() const = 0; virtual SCP_vector getValidValues() const = 0; virtual ValueDescription getValueFromRange(float interpolant) const = 0; virtual float getInterpolantFromValue(const ValueDescription& val) const = 0; virtual void loadInitial() const = 0; friend bool operator<(const OptionBase& lhs, const OptionBase& rhs); friend bool operator>(const OptionBase& lhs, const OptionBase& rhs); friend bool operator<=(const OptionBase& lhs, const OptionBase& rhs); friend bool operator>=(const OptionBase& lhs, const OptionBase& rhs); }; template using ValueEnumerator = std::function()>; template using ValueDeserializer = std::function; template using ValueSerializer = std::function; template using ValueDisplay = std::function; template using DefaultValueFunctor = std::function; template using ValueChangeListener = std::function; template using ValueInterpolator = std::function; template using ValueDeinterpolator = std::function; template class ValueFunctor { T _value; public: ValueFunctor() = default; explicit ValueFunctor(T value) : _value(value) {} T operator()() { return _value; } }; template class Option : public OptionBase { protected: ValueEnumerator _valueEnumerator; ValueDeserializer _deserializer; ValueSerializer _serializer; ValueDisplay _displayFunc; DefaultValueFunctor _defaultValueFunc = ValueFunctor(); ValueChangeListener _changeListener; ValueInterpolator _interpolator; ValueDeinterpolator _deinterpolator; OptionType _type = OptionType::Selection; // By default the generic type is always a selection ValueDescription toDescription(const T& val) const { auto json = _serializer(val); auto json_str = json_dump_string_new(json, JSON_COMPACT | JSON_ENSURE_ASCII | JSON_ENCODE_ANY); if (_displayFunc) { return ValueDescription(_displayFunc(val), json_str); } else { return ValueDescription(json_str, json_str); } } std::unique_ptr fromDescription(const ValueDescription& desc) const { json_error_t err; std::unique_ptr el(json_loads(desc.serialized.c_str(), JSON_DECODE_ANY, &err)); if (el == nullptr) { // Shouldn't really happen... throw json_exception(err); } return el; } public: Option(const SCP_string& config_key, const SCP_string& title, const SCP_string& description) : OptionBase(config_key, title, description) { } ~Option() override = default; OptionType getType() const override { return _type; } void setType(OptionType type) { _type = type; } SCP_vector getValidValues() const override { Assertion(_valueEnumerator, "Invalid value enumerator detected!"); Assertion(_type == OptionType::Selection, "getValidValues may only be called on selection option!"); auto values = _valueEnumerator(); SCP_vector descriptions; descriptions.reserve(values.size()); for (const auto& val : values) { descriptions.emplace_back(toDescription(val)); } return descriptions; } ValueDescription getValueFromRange(float interpolant) const override { Assertion(_interpolator, "Need a valid interpolator!"); Assertion(_type == OptionType::Range, "getValidValues may only be called on range option!"); try { return toDescription(_interpolator(interpolant)); } catch (const std::exception&) { return toDescription(_defaultValueFunc()); } } float getInterpolantFromValue(const ValueDescription& val) const override { Assertion(_deinterpolator, "Need a valid deinterpolator!"); Assertion(_type == OptionType::Range, "getInterpolantFromValue may only be called on range option!"); try { auto el = fromDescription(val); return _deinterpolator(_deserializer(el.get())); } catch (const std::exception&) { return 0.0f; } } T getValue() const { try { return _deserializer(getConfigValue().get()); } catch (const std::exception&) { // deserializers are allowed to throw on error return _defaultValueFunc(); } } ValueDescription getCurrentValueDescription() const override { auto val = getValue(); return toDescription(val); } void setValueDescription(const ValueDescription& desc) const override { std::unique_ptr el; try { el = fromDescription(desc); } catch (const std::exception&) { // Functions are allowed to throw } if (el) { _parent->setConfigValue(_config_key, std::move(el)); } } bool valueChanged(const json_t* val) const override { try { if (_changeListener) { return _changeListener(_deserializer(val), false); } return false; // Value not changed, requires restart } catch (const std::exception&) { // Functions are allowed to throw return false; } } void loadInitial() const override { if (!_changeListener) { // This requires a change listener return; } try { _changeListener(getValue(), true); } catch (const std::exception&) { } } const DefaultValueFunctor& getDefaultValueFunc() const { return _defaultValueFunc; } void setDefaultValueFunc(const DefaultValueFunctor& defaultValueFunc) { _defaultValueFunc = defaultValueFunc; } const ValueDeserializer& getDeserializer() const { return _deserializer; } void setDeserializer(const ValueDeserializer& converter) { _deserializer = converter; } const ValueSerializer& getSerializer() const { return _serializer; } void setSerializer(const ValueSerializer& serializer) { _serializer = serializer; } const ValueDisplay& getDisplayFunc() const { return _displayFunc; } void setDisplayFunc(const ValueDisplay& displayFunc) { _displayFunc = displayFunc; } const ValueEnumerator& getValueEnumerator() const { return _valueEnumerator; } void setValueEnumerator(const ValueEnumerator& valueEnumerator) { _valueEnumerator = valueEnumerator; _type = OptionType::Selection; } const ValueChangeListener& getChangeListener() const { return _changeListener; } void setChangeListener(const ValueChangeListener& changeListener) { _changeListener = changeListener; } const ValueInterpolator& getInterpolator() const { return _interpolator; } const ValueDeinterpolator& getDeinterpolator() const { return _deinterpolator; } void setInterpolator(const ValueInterpolator& interpolator, const ValueDeinterpolator& deinterpolator) { _interpolator = interpolator; _deinterpolator = deinterpolator; _type = OptionType::Range; } }; template class VectorEnumerator { SCP_vector _values; public: explicit VectorEnumerator(SCP_vector values) : _values(std::move(values)) {} SCP_vector operator()() { return _values; } }; template class MapValueDisplay { SCP_unordered_map _mapping; public: explicit MapValueDisplay(SCP_unordered_map mapping) : _mapping(std::move(mapping)) { } SCP_string operator()(const T& value) const { auto iter = _mapping.find(value); if (iter == _mapping.end()) { throw std::runtime_error("Display called with invalid value!"); } return iter->second; } }; namespace internal { // This would be so much easier with Concepts... template typename std::enable_if::value, void>::type set_defaults(Option& /*opt*/) { // No defaults for the generic case since we don't know anything about this } template <> void set_defaults(Option& opt); template <> void set_defaults(Option& opt); template <> void set_defaults(Option& opt); template <> void set_defaults(Option& opt); template typename std::enable_if::value, void>::type set_defaults(Option& opt) { opt.setSerializer([](T val) { return json_pack("I", static_cast(val)); }); opt.setDeserializer([](const json_t* value) { json_int_t i; json_error_t err; if (json_unpack_ex((json_t*)value, &err, 0, "I", &i) != 0) { throw json_exception(err); } return static_cast(i); }); } template void set_defaults(Option>& opt) { opt.setSerializer([](const flagset& val) { return json_pack("I", static_cast(val.to_u64())); }); opt.setDeserializer([](const json_t* value) { json_int_t i; json_error_t err; if (json_unpack_ex((json_t*)value, &err, 0, "I", &i) != 0) { throw json_exception(err); } flagset out; out.from_u64((std::uint64_t)i); return out; }); } } // namespace internal template class OptionBuilder { Option _instance; SCP_unordered_map _preset_values; public: OptionBuilder(const SCP_string& config_key, const SCP_string& title, const SCP_string& description) : _instance(config_key, title, description) { internal::set_defaults(_instance); } OptionBuilder(const OptionBuilder&) = delete; OptionBuilder& operator=(const OptionBuilder&) = delete; OptionBuilder(OptionBuilder&&) noexcept = delete; OptionBuilder& operator=(OptionBuilder&&) noexcept = delete; OptionBuilder& category(const SCP_string& category) { _instance.setCategory(category); return *this; } OptionBuilder& level(ExpertLevel level) { _instance.setExpertLevel(level); return *this; } OptionBuilder& default_val(const T& val) { _instance.setDefaultValueFunc(ValueFunctor(val)); return *this; } OptionBuilder& default_func(const DefaultValueFunctor& func) { _instance.setDefaultValueFunc(func); return *this; } OptionBuilder& deserializer(const ValueDeserializer& converter) { _instance.setDeserializer(converter); return *this; } OptionBuilder& serializer(const ValueSerializer& serializer) { _instance.setSerializer(serializer); return *this; } OptionBuilder& enumerator(const ValueEnumerator& enumerator) { _instance.setValueEnumerator(enumerator); return *this; } OptionBuilder& values(const SCP_vector>& value_display_pairs) { SCP_vector values; SCP_unordered_map display_mapping; for (auto& p : value_display_pairs) { values.push_back(p.first); display_mapping.emplace(p.first, p.second); } _instance.setValueEnumerator(VectorEnumerator(values)); _instance.setDisplayFunc(MapValueDisplay(display_mapping)); return *this; } OptionBuilder& display(const ValueDisplay& display) { _instance.setDisplayFunc(display); return *this; } OptionBuilder& change_listener(const ValueChangeListener& listener) { _instance.setChangeListener(listener); return *this; } OptionBuilder& bind_to(T* dest) { return change_listener([dest](const T& val, bool) { *dest = val; return true; }); } OptionBuilder& bind_to_once(T* dest) { return change_listener([dest](const T& val, bool initial) { if (initial) { *dest = val; } return initial; }); } OptionBuilder& range(T min, T max) { Assertion(min <= max, "Invalid number range!"); _instance.setInterpolator( [min, max](float f) { return min + static_cast((max - min) * f); }, [min, max](T f) { return static_cast(f - min) / static_cast(max - min); }); return *this; } OptionBuilder& preset(PresetKind kind, const T& value) { _preset_values.emplace(kind, value); return *this; } OptionBuilder& importance(int value) { _instance.setImportance(value); return *this; } OptionBuilder& flags(const flagset& flags) { _instance.setFlags(flags); return *this; } const Option* finish() { for (auto& val : _preset_values) { _instance.setPreset(val.first, json_dump_string_new(_instance.getSerializer()(val.second), JSON_COMPACT | JSON_ENSURE_ASCII | JSON_ENCODE_ANY)); } std::unique_ptr> opt_ptr(new Option(_instance)); auto ptr = opt_ptr.get(); // We need to get the pointer now since we loose the type information otherwise OptionsManager::instance()->addOption(std::unique_ptr(opt_ptr.release())); return ptr; } }; } // namespace options