Files
Goober5000andClaude Opus 4.6 3f85d6172a use std::move() for local variables that are copied but not reused (#7308)
* use std::move() for local variables that are copied but not reused

Addresses ~140 Coverity "Variable copied when it could be moved"
defects across 74 files.  Every std::move() targets a local variable
whose value is consumed by push_back, emplace, assignment, lambda
capture, or a function call and is never read again afterward.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* clang: use std::make_shared

* clang: use std::make_unique

* address feedback

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-05-07 14:40:38 +09:00

629 lines
19 KiB
C++

#pragma once
#include "globalincs/flagset.h"
#include "globalincs/pstypes.h"
#include "libs/jansson.h"
#include "options/OptionsManager.h"
#include "localization/localize.h"
#include "parse/parselo.h"
#include <functional>
#include <utility>
#include <variant>
#include <optional>
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,
/**
* @brief Whether or not the option is built-in to FSO's retail Options UI
*
* This can be useful if an option should be ignored in cases like the SCP Options UI where
* it's only appropriate to display options that are not otherwise accessible to retail players.
*/
RetailBuiltinOption = 1,
/**
* @brief Forces the range type slider to use an integer instead of a float
*
* This can be useful if an option cannot accept float values.
*/
RangeTypeInteger = 2,
NUM_VALUES
};
// clang-format on
struct ValueDescription {
const SCP_string display;
const SCP_string serialized;
ValueDescription(SCP_string _display, SCP_string _serialized);
};
class OptionBase {
template <typename T>
friend class OptionBuilder;
protected:
OptionsManager* _parent = nullptr;
ExpertLevel _expert_level = ExpertLevel::Expert;
SCP_string _config_key;
std::pair<const char*, int> _category = {"Other", 1829};
SCP_string _title;
SCP_string _description;
int _importance = 0;
float _min = 0;
float _max = 1;
bool _is_once = false;
SCP_unordered_map<PresetKind, SCP_string> _preset_values;
flagset<OptionFlags> _flags;
std::optional<std::unique_ptr<json_t>> 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 std::pair<const char*, int>& category);
int getImportance() const;
void setImportance(int importance);
std::pair<float, float> getRangeValues() const;
void setRangeValues(float min, float max);
const flagset<OptionFlags>& getFlags() const;
void setFlags(const flagset<OptionFlags>& flags);
bool getIsOnce() const;
void setIsOnce(bool is_once);
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 void parseDefaultSetting() const = 0;
virtual SCP_vector<ValueDescription> 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 <typename T>
using ValueEnumerator = std::function<SCP_vector<T>()>;
template <typename T>
using ValueDeserializer = std::function<T(const json_t*)>;
template <typename T>
using ValueSerializer = std::function<json_t*(const T&)>;
template <typename T>
using ValueDisplay = std::function<SCP_string(const T&)>;
template <typename T>
using DefaultValueFunctor = std::function<T()>;
template <typename T>
using ValueChangeListener = std::function<bool(const T&, bool initial)>;
template <typename T>
using ValueInterpolator = std::function<T(float)>;
template <typename T>
using ValueDeinterpolator = std::function<float(const T&)>;
using ValueParser = std::function<void()>;
template <typename T>
class ValueFunctor {
T _value;
public:
ValueFunctor() = default;
explicit ValueFunctor(T value) : _value(value) {}
T operator()() { return _value; }
};
template <typename T>
class Option : public OptionBase {
protected:
ValueEnumerator<T> _valueEnumerator;
ValueDeserializer<T> _deserializer;
ValueSerializer<T> _serializer;
ValueDisplay<T> _displayFunc;
DefaultValueFunctor<T> _defaultValueFunc = ValueFunctor<T>();
ValueChangeListener<T> _changeListener;
ValueInterpolator<T> _interpolator;
ValueDeinterpolator<T> _deinterpolator;
ValueParser _parser;
bool _hasCustomDefaultFunc = false;
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) {
try {
return ValueDescription(_displayFunc(val), json_str);
} catch (const std::exception& e) {
static bool rel_warning_triggered = false;
if (!rel_warning_triggered) {
rel_warning_triggered = true;
ReleaseWarning(LOCATION, "Option %s has an invalid value %s! Please change the value!", _title.c_str(), json_str.c_str());
}
//This will logspam once a frame, but it's super rare, so it should be fine
mprintf(("Tried to show an invalid options value %s for option %s! Likely due to a malformed ini: %s\n", json_str.c_str(), _title.c_str(), e.what()));
return ValueDescription(json_str, json_str);
}
} else {
return ValueDescription(json_str, json_str);
}
}
std::unique_ptr<json_t> fromDescription(const ValueDescription& desc) const
{
json_error_t err;
std::unique_ptr<json_t> 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; }
void parseDefaultSetting() const override
{
if (!_parser) {
Warning(LOCATION, "%s cannot set default values", _config_key.c_str());
skip_to_start_of_string_either("$Option Key:", "#END");
} else {
Assertion(_hasCustomDefaultFunc, "Option must have a default value function in order to use parser!");
_parser();
}
}
SCP_vector<ValueDescription> 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<ValueDescription> 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 {
std::optional<std::unique_ptr<json_t>> config_value = getConfigValue();
//missing keys are signalled via the optional
if (!config_value.has_value()) {
return _defaultValueFunc();
}
return _deserializer(config_value->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<json_t> 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<T>& getDefaultValueFunc() const { return _defaultValueFunc; }
void setDefaultValueFunc(const DefaultValueFunctor<T>& defaultValueFunc) { _defaultValueFunc = defaultValueFunc; }
void setHasCustomDefaultFunc(bool value) { _hasCustomDefaultFunc = value; }
const ValueDeserializer<T>& getDeserializer() const { return _deserializer; }
void setDeserializer(const ValueDeserializer<T>& converter) { _deserializer = converter; }
const ValueSerializer<T>& getSerializer() const { return _serializer; }
void setSerializer(const ValueSerializer<T>& serializer) { _serializer = serializer; }
const ValueDisplay<T>& getDisplayFunc() const { return _displayFunc; }
void setDisplayFunc(const ValueDisplay<T>& displayFunc) { _displayFunc = displayFunc; }
const ValueEnumerator<T>& getValueEnumerator() const { return _valueEnumerator; }
void setValueEnumerator(const ValueEnumerator<T>& valueEnumerator)
{
_valueEnumerator = valueEnumerator;
_type = OptionType::Selection;
}
const ValueChangeListener<T>& getChangeListener() const { return _changeListener; }
void setChangeListener(const ValueChangeListener<T>& changeListener) { _changeListener = changeListener; }
void setParser(ValueParser parser) { _parser = std::move(parser); }
const ValueInterpolator<T>& getInterpolator() const { return _interpolator; }
const ValueDeinterpolator<T>& getDeinterpolator() const { return _deinterpolator; }
void setInterpolator(const ValueInterpolator<T>& interpolator, const ValueDeinterpolator<T>& deinterpolator)
{
_interpolator = interpolator;
_deinterpolator = deinterpolator;
_type = OptionType::Range;
}
};
template <typename T>
class VectorEnumerator {
SCP_vector<T> _values;
public:
explicit VectorEnumerator(SCP_vector<T> values) : _values(std::move(values)) {}
SCP_vector<T> operator()() { return _values; }
};
template <typename T>
class MapValueDisplay {
SCP_unordered_map<T, std::pair<const char*, int>> _mapping;
public:
explicit MapValueDisplay(SCP_unordered_map<T, std::pair<const char*, int>> 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 XSTR(iter->second.first, iter->second.second);
}
};
namespace internal {
// This would be so much easier with Concepts...
template <typename T>
typename std::enable_if<!std::is_enum<T>::value, void>::type set_defaults(Option<T>& /*opt*/)
{
// No defaults for the generic case since we don't know anything about this
}
template <>
void set_defaults<SCP_string>(Option<SCP_string>& opt);
template <>
void set_defaults<float>(Option<float>& opt);
template <>
void set_defaults<int>(Option<int>& opt);
template <>
void set_defaults<bool>(Option<bool>& opt);
template <typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type set_defaults(Option<T>& opt)
{
opt.setSerializer([](T val) { return json_pack("I", static_cast<json_int_t>(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<T>(i);
});
}
template <typename T, size_t N>
void set_defaults(Option<flagset<T, N>>& opt)
{
opt.setSerializer([](const flagset<T, N>& val) { return json_pack("I", static_cast<json_int_t>(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<T, N> out;
out.from_u64((std::uint64_t)i);
return out;
});
}
} // namespace internal
template <typename T>
class OptionBuilder {
Option<T> _instance;
SCP_unordered_map<PresetKind, T> _preset_values;
const std::variant<SCP_string, std::pair<const char*, int>>&_title, &_description;
public:
OptionBuilder(const SCP_string& config_key, const std::variant<SCP_string, std::pair<const char*, int>>& title, const std::variant<SCP_string, std::pair<const char*, int>>& description)
: _instance(
config_key,
std::holds_alternative<SCP_string>(title) ? std::get<SCP_string>(title) : std::get<std::pair<const char*, int>>(title).first,
std::holds_alternative<SCP_string>(description) ? std::get<SCP_string>(description) : std::get<std::pair<const char*, int>>(description).first),
_title(title),
_description(description)
{
internal::set_defaults(_instance);
}
OptionBuilder(const OptionBuilder&) = delete;
OptionBuilder& operator=(const OptionBuilder&) = delete;
OptionBuilder(OptionBuilder&&) noexcept = delete;
OptionBuilder& operator=(OptionBuilder&&) noexcept = delete;
//Set the category of the option
OptionBuilder& category(const std::pair<const char*, int>& category)
{
_instance.setCategory(category);
return *this;
}
//Set the level value of the option. Can be "Beginniner", "Advanced", or "Expert"
OptionBuilder& level(ExpertLevel level)
{
_instance.setExpertLevel(level);
return *this;
}
//Causes the default value of the option to be permanently set to a specific value
OptionBuilder& default_val(const T& val)
{
_instance.setDefaultValueFunc(ValueFunctor<T>(val));
_instance.setHasCustomDefaultFunc(false);
return *this;
}
//Causes the default value of the option to be set dynamically using a function
OptionBuilder& default_func(const DefaultValueFunctor<T>& func)
{
_instance.setDefaultValueFunc(func);
_instance.setHasCustomDefaultFunc(true);
return *this;
}
//Deserialize an option for persisting changes
OptionBuilder& deserializer(const ValueDeserializer<T>& converter)
{
_instance.setDeserializer(converter);
return *this;
}
//Serialize an option value for display
OptionBuilder& serializer(const ValueSerializer<T>& serializer)
{
_instance.setSerializer(serializer);
return *this;
}
//Builds an enum list of values for the option
OptionBuilder& enumerator(const ValueEnumerator<T>& enumerator)
{
_instance.setValueEnumerator(enumerator);
return *this;
}
//The valid values for the option
OptionBuilder& values(const SCP_vector<std::pair<T, std::pair<const char*, int>>>& value_display_pairs)
{
SCP_vector<T> values;
SCP_unordered_map<T, std::pair<const char*, int>> display_mapping;
for (auto& p : value_display_pairs) {
values.push_back(p.first);
display_mapping.emplace(p.first, p.second);
}
_instance.setValueEnumerator(VectorEnumerator<T>(std::move(values)));
_instance.setDisplayFunc(MapValueDisplay<T>(std::move(display_mapping)));
return *this;
}
//The string to display for the option values, usually a function that returns a string
OptionBuilder& display(const ValueDisplay<T>& display)
{
_instance.setDisplayFunc(display);
return *this;
}
//The code to run if the option value is changed
OptionBuilder& change_listener(const ValueChangeListener<T>& listener)
{
_instance.setChangeListener(listener);
return *this;
}
//The code to run in order to parse the default setting. Option must use default_func instead of defaul_val!
OptionBuilder& parser(const ValueParser& parser)
{
_instance.setParser(parser);
return *this;
}
//The global variable to bind the option to immediately.
OptionBuilder& bind_to(T* dest)
{
return change_listener([dest](const T& val, bool) {
*dest = val;
return true;
});
}
//The global variable to bind the option to once. Will require game restart to persist changes.
OptionBuilder& bind_to_once(T* dest)
{
_instance.setIsOnce(true);
return change_listener([dest](const T& val, bool initial) {
if (initial) {
*dest = val;
}
return initial;
});
}
//Set the minimum and maximum range for a "slider" type option
OptionBuilder& range(T min, T max)
{
Assertion(min <= max, "Invalid number range!");
_instance.setRangeValues(static_cast<float>(min), static_cast<float>(max));
_instance.setInterpolator(
[min, max](float f) { return min + static_cast<T>((max - min) * f); },
[min, max](T f) { return static_cast<float>(f - min) / static_cast<float>(max - min); });
return *this;
}
//Set the preset value for the option
OptionBuilder& preset(PresetKind kind, const T& value)
{
_preset_values.emplace(kind, value);
return *this;
}
//Set the option importance, used for sorting
OptionBuilder& importance(int value)
{
_instance.setImportance(value);
return *this;
}
//Set the option flags
OptionBuilder& flags(const flagset<OptionFlags>& flags)
{
_instance.setFlags(flags);
return *this;
}
//Finishes building the option and returns a pointer to it
std::shared_ptr<const Option<T>> 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));
}
auto opt_ptr = std::make_shared<Option<T>>(_instance);
if (std::holds_alternative<std::pair<const char*, int>>(_title)) {
const auto& xstr_info = std::get<std::pair<const char*, int>>(_title);
lcl_delayed_xstr(opt_ptr->_title, xstr_info.first, xstr_info.second);
}
if (std::holds_alternative<std::pair<const char*, int>>(_description)) {
const auto& xstr_info = std::get<std::pair<const char*, int>>(_description);
lcl_delayed_xstr(opt_ptr->_description, xstr_info.first, xstr_info.second);
}
OptionsManager::instance()->addOption(opt_ptr);
return opt_ptr;
}
};
} // namespace options