mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* Add command-line option overrides to options system - Extend `OptionsManager` to support storing and displaying reasons for overrides. - Update related code paths to prioritize command-line overrides, ensure correct option resolution, and display override details in the UI. - Add unit tests for override behavior and round-trip validation. - Remove unused legacy functions (`removeVSyncOption`, `removeWindowModeOption`) in favor of the new system. * Add unit tests for `Graphics.Anisotropy` option validation with and without OpenGL * Add support for associating command-line option names to `OptionsManager` overrides - Update HDR, shadow rendering, and graphics API option overrides to include their respective command-line flag names. - Refactor `Graphics.RenderAPI` to use the `GraphicsAPI` enum instead of raw integers for better type safety and clarity. - Ensure Vulkan support logic and fallback behavior function correctly with the updated options system. - Update unit tests to validate override functionality and enum-based API configuration. * Remove unused `removeVSyncOption` function * Apply the PR review feedback for the command-line option overrides Delete the unit-test run artifacts that this branch committed by mistake, and add .gitignore rules for the files that the unittests binary writes. Give an override priority over an in-session edit again. The options UI now shows a disabled control for an overridden option, and setConfigValue() refuses an edit for an overridden key. Before this change, a save wrote the edit to the config file and applied it to the bound global, but getValue() kept returning the override. The game and the options menu then showed different values for the rest of the session. Add a setOverride() overload that takes a JSON value. Every command-line flag now builds its override with json_integer, json_real, json_true, json_false or json_pack, in place of a hand-formatted string. Make Graphics.CockpitFOV and Graphics.CockpitFOVToggle independent of the order in which the options manager loads them, and remove the forced override on the player's toggle preference. The toggle listener also replaces an identical bind_to() call that made the listener dead code. Add 16 to the values of Graphics.MSAASamples. The -msaa flag accepts 16, so an override with that value made printValues() report an invalid value. Keep the render API that the caller gives to gr_init(). Only a Default value now comes from the Graphics.RenderAPI option, so FRED and the unit tests keep the backend that they ask for. Make the default_func of Graphics.Shadows and Graphics.ShadowRenderMethod return their global. A constant discarded the value that -enable_shadows and the mod table set. Set Max_rt_shadow_local_lights to 4, because 3 is not one of the values that the option offers. Also: resolve -lang one time, share the render API name with version.cpp, correct the stale comments, and add unit tests with a fixture that clears the overrides after each test. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr * Fix the order-dependent crash in the MSAA option test The test `MsaaOverrideAcceptsEverySampleCountTheFlagAllows` calls `getValidValues()`. `Option<T>` makes a display string for each value, and the display function of `Graphics.MSAASamples` calls `XSTR()`. `XSTR()` runs `Int3()` if `lcl_xstr_init()` did not run before. Only `FSTestFixture` runs it, and `Xstr_inited` stays set for the remainder of the process. Thus the shuffled test order decided if the test passed or if it stopped the test binary with SIGILL. Move that one test to a new fixture that derives from `FSTestFixture`. The fixture initializes localization, and it also clears the overrides after each test. Refresh the comment that told that the test binary never runs `lcl_init()`. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr --------- Co-authored-by: Claude Opus 5 <[email protected]>
360 lines
11 KiB
C++
360 lines
11 KiB
C++
//Internal use manager to handle seting option data and
|
|
//correctly find options from config or save them back to config
|
|
|
|
#include "OptionsManager.h"
|
|
#include "Option.h"
|
|
#include "mod_table/mod_table.h"
|
|
#include "osapi/osregistry.h"
|
|
#include <optional>
|
|
|
|
namespace {
|
|
|
|
//Parses a full option key, separating it into section and key
|
|
std::pair<SCP_string, SCP_string> parse_key(const SCP_string& key)
|
|
{
|
|
auto point_pos = key.find('.');
|
|
if (point_pos == SCP_string::npos) {
|
|
// Invalid key
|
|
return std::pair<SCP_string, SCP_string>();
|
|
}
|
|
|
|
auto section = key.substr(0, point_pos);
|
|
auto section_key = key.substr(point_pos + 1);
|
|
|
|
return std::make_pair(section, section_key);
|
|
}
|
|
} // namespace
|
|
|
|
namespace options {
|
|
|
|
OptionsManager::OptionsManager() = default;
|
|
OptionsManager::~OptionsManager() = default;
|
|
|
|
OptionsManager* options::OptionsManager::instance()
|
|
{
|
|
static OptionsManager instance;
|
|
return &instance;
|
|
}
|
|
|
|
//Gets the value of an option from the Config using the option key
|
|
std::optional<std::unique_ptr<json_t>> OptionsManager::getValueFromConfig(const SCP_string& key) const
|
|
{
|
|
// An override always wins. The options UI disables the control for an overridden option, and
|
|
// setConfigValue() rejects writes to one, so no edit can be pending here anyway.
|
|
auto override_iter = _config_overrides.find(key);
|
|
if (override_iter != _config_overrides.end()) {
|
|
// We return a reference to an existing object so we need to increment the reference count
|
|
json_incref(override_iter->second.value.get());
|
|
// coverity[multiple_init_smart_ptr:FALSE] - according to m!m, this is most likely a false positive: "I think Coverity does not understand the usage of default_delete" in jansson.h, line 23
|
|
return std::unique_ptr<json_t>(override_iter->second.value.get());
|
|
}
|
|
|
|
auto changed_iter = _changed_values.find(key);
|
|
if (changed_iter != _changed_values.end()) {
|
|
json_incref(changed_iter->second.get());
|
|
// coverity[multiple_init_smart_ptr:FALSE] - according to m!m, this is most likely a false positive: "I think Coverity does not understand the usage of default_delete" in jansson.h, line 23
|
|
return std::unique_ptr<json_t>(changed_iter->second.get());
|
|
}
|
|
|
|
auto parts = parse_key(key);
|
|
if (parts.first.empty()) {
|
|
// Invalid key
|
|
throw std::runtime_error("Invalid key");
|
|
}
|
|
|
|
auto value = os_config_read_string(parts.first.c_str(), parts.second.c_str(), (const char*)0, true);
|
|
|
|
if (value == nullptr) {
|
|
// Signal that there is no value for this key
|
|
return std::nullopt;
|
|
}
|
|
|
|
json_error_t err;
|
|
auto el = json_loads(value, JSON_DECODE_ANY, &err);
|
|
if (el == nullptr) {
|
|
throw json_exception(err);
|
|
}
|
|
|
|
return std::unique_ptr<json_t>(el);
|
|
}
|
|
|
|
//Sets the value for the option
|
|
void OptionsManager::setConfigValue(const SCP_string& key,std::unique_ptr<json_t>&& value)
|
|
{
|
|
// An overridden option is not editable. Accepting the edit here would write it to the config file and
|
|
// live-apply it through valueChanged(), but getValue() would keep returning the override, so the game
|
|
// and the options menu would disagree for the rest of the session.
|
|
if (isOverridden(key)) {
|
|
return;
|
|
}
|
|
|
|
_changed_values[key] = std::move(value);
|
|
}
|
|
|
|
//Provides a method for overriding a built-in option setting
|
|
//Generally used for commandline settings
|
|
void OptionsManager::setOverride(const SCP_string& key, const SCP_string& json, const SCP_string& reason)
|
|
{
|
|
json_error_t err;
|
|
auto el = json_loads(json.c_str(), JSON_DECODE_ANY, &err);
|
|
if (el == nullptr) {
|
|
return;
|
|
}
|
|
setOverride(key, std::unique_ptr<json_t>(el), reason);
|
|
}
|
|
|
|
void OptionsManager::setOverride(const SCP_string& key, std::unique_ptr<json_t>&& value, const SCP_string& reason)
|
|
{
|
|
if (value == nullptr) {
|
|
return;
|
|
}
|
|
// insert_or_assign (rather than emplace) so a later override for the same key, e.g. from a second cmdline
|
|
// flag targeting the same option, replaces the earlier one instead of being silently ignored.
|
|
_config_overrides.insert_or_assign(key, OverrideEntry{std::move(value), reason});
|
|
|
|
// An override wins over a pending edit, so drop any edit that is already pending for this key.
|
|
_changed_values.erase(key);
|
|
}
|
|
|
|
void OptionsManager::clearOverrides() { _config_overrides.clear(); }
|
|
|
|
bool OptionsManager::isOverridden(const SCP_string& key) const
|
|
{
|
|
return _config_overrides.find(key) != _config_overrides.end();
|
|
}
|
|
|
|
std::optional<SCP_string> OptionsManager::getOverrideReason(const SCP_string& key) const
|
|
{
|
|
auto iter = _config_overrides.find(key);
|
|
if (iter == _config_overrides.end()) {
|
|
return std::nullopt;
|
|
}
|
|
return iter->second.reason;
|
|
}
|
|
|
|
//Adds an option to the options vector
|
|
const OptionBase* OptionsManager::addOption(std::shared_ptr<const OptionBase>&& option)
|
|
{
|
|
_options.emplace_back(std::move(option));
|
|
_optionsSorted = false; // Order got invalidated by adding a new option
|
|
|
|
auto ptr = _options.back().get();
|
|
_optionsMapping.emplace(ptr->getConfigKey(), ptr);
|
|
return ptr;
|
|
}
|
|
|
|
//Removes an option from the options vector
|
|
void OptionsManager::removeOption(const std::shared_ptr<const OptionBase>& option)
|
|
{
|
|
if (_optionsMapping.find(option->getConfigKey()) == _optionsMapping.end())
|
|
return;
|
|
|
|
_optionsMapping.erase(option->getConfigKey());
|
|
_options.erase(
|
|
std::remove_if(_options.begin(), _options.end(),
|
|
[option](const std::shared_ptr<const OptionBase>& ptr) { return ptr == option; }));
|
|
}
|
|
|
|
// Returns an option with the specified name
|
|
const OptionBase* OptionsManager::getOptionByKey(SCP_string key)
|
|
{
|
|
for (size_t i = 0; i < _options.size(); i++) {
|
|
if (_options[i].get()->getConfigKey() == key) {
|
|
return _options[i].get();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void OptionsManager::enforceOption(const SCP_string& key)
|
|
{
|
|
_enforcedOptions.insert(key);
|
|
}
|
|
|
|
void OptionsManager::unenforceOption(const SCP_string& key)
|
|
{
|
|
_enforcedOptions.erase(key);
|
|
}
|
|
|
|
bool OptionsManager::isOptionEnforced(const SCP_string& key) const
|
|
{
|
|
return _enforcedOptions.count(key) > 0;
|
|
}
|
|
|
|
//Returns a table of all built-in options available
|
|
const SCP_vector<std::shared_ptr<const options::OptionBase>>& OptionsManager::getOptions()
|
|
{
|
|
if (!_optionsSorted) {
|
|
// Keep options sorted by only sorting them when necessary
|
|
|
|
std::sort(_options.begin(), _options.end(),
|
|
[](const std::shared_ptr<const OptionBase>& left, const std::shared_ptr<const OptionBase>& right) {
|
|
return *left < *right;
|
|
});
|
|
|
|
_optionsSorted = true;
|
|
}
|
|
return _options;
|
|
}
|
|
|
|
//Write the option to the registry and return if it was changed or not
|
|
bool OptionsManager::persistOptionChanges(const options::OptionBase* option)
|
|
{
|
|
auto iter = _changed_values.find(option->getConfigKey());
|
|
|
|
if (iter == _changed_values.end()) {
|
|
// No changes for this option
|
|
return true;
|
|
}
|
|
|
|
auto parts = parse_key(iter->first);
|
|
if (parts.first.empty()) {
|
|
// Invalid key
|
|
return false;
|
|
}
|
|
|
|
auto val = json_dump_string(iter->second.get(), JSON_COMPACT | JSON_ENSURE_ASCII | JSON_ENCODE_ANY);
|
|
|
|
os_config_write_string(parts.first.c_str(), parts.second.c_str(), val.c_str(), true);
|
|
|
|
auto changed = option->valueChanged(iter->second.get());
|
|
|
|
// Always erase even if it wasn't actually changed so later persistChanges() calls don't try to save the value again
|
|
_changed_values.erase(iter);
|
|
|
|
return changed;
|
|
}
|
|
|
|
//Write value changes to disk and return vector of options that do not support changine the value
|
|
SCP_vector<const options::OptionBase*> OptionsManager::persistChanges()
|
|
{
|
|
for (auto& entry : _changed_values) {
|
|
auto parts = parse_key(entry.first);
|
|
if (parts.first.empty()) {
|
|
// Invalid key
|
|
continue;
|
|
}
|
|
|
|
auto val = json_dump_string(entry.second.get(), JSON_COMPACT | JSON_ENSURE_ASCII | JSON_ENCODE_ANY);
|
|
|
|
os_config_write_string(parts.first.c_str(), parts.second.c_str(), val.c_str(), true);
|
|
}
|
|
SCP_vector<const options::OptionBase*> unchanged;
|
|
|
|
// Only notify options once all values have been written to config
|
|
for (auto& entry : _changed_values) {
|
|
auto iter = _optionsMapping.find(entry.first);
|
|
if (iter != _optionsMapping.end()) {
|
|
if (!iter->second->valueChanged(entry.second.get())) {
|
|
unchanged.push_back(iter->second);
|
|
}
|
|
}
|
|
}
|
|
_changed_values.clear();
|
|
|
|
return unchanged;
|
|
}
|
|
|
|
//Discard changes and restore the option to initial value
|
|
void OptionsManager::discardChanges() { _changed_values.clear(); }
|
|
|
|
//Get the initial values of the option as stored on disk
|
|
void OptionsManager::loadInitialValues()
|
|
{
|
|
// Collect options to remove in a separate container
|
|
SCP_vector<std::shared_ptr<const OptionBase>> optionsToRemove;
|
|
|
|
for (const auto& opt : _options) { // Iterate forward to collect
|
|
if (isOptionEnforced(opt->getConfigKey())) {
|
|
optionsToRemove.push_back(opt);
|
|
} else {
|
|
opt->loadInitial();
|
|
}
|
|
}
|
|
|
|
// Now remove the collected options.
|
|
for (const auto& optToRemove : optionsToRemove) {
|
|
removeOption(optToRemove);
|
|
}
|
|
}
|
|
|
|
void OptionsManager::printValues()
|
|
{
|
|
mprintf(("Printing in-game options values!\n"));
|
|
for (auto& opt : _options) {
|
|
// If we're not using in-game options and the option is not a retail option, then skip
|
|
// This ensures we only log options that are actually impacting the current game instance
|
|
if (!Using_in_game_options && !(opt->getFlags()[options::OptionFlags::RetailBuiltinOption])){
|
|
continue;
|
|
}
|
|
|
|
// Log the option
|
|
mprintf(("Option.%s: %s\n",
|
|
opt->getConfigKey().c_str(),
|
|
opt->getCurrentValueDescription().display.c_str()));
|
|
}
|
|
}
|
|
|
|
//Sets the value saved within the option, but does not actually change the variables tied to the option
|
|
//Used for persistence and UI updates
|
|
void OptionsManager::set_ingame_binary_option(SCP_string key, bool value)
|
|
{
|
|
if (!Using_in_game_options) {
|
|
return;
|
|
}
|
|
|
|
const OptionBase* thisOpt = getOptionByKey(std::move(key));
|
|
if (thisOpt != nullptr) {
|
|
auto val = thisOpt->getCurrentValueDescription();
|
|
SCP_string newVal = value ? "true" : "false"; // OptionsManager stores values as serialized strings
|
|
thisOpt->setValueDescription({val.display, newVal.c_str()});
|
|
}
|
|
}
|
|
|
|
//Sets the value saved within the option, but does not actually change the variables tied to the option
|
|
//Used for persistence and UI updates
|
|
void OptionsManager::set_ingame_multi_option(SCP_string key, int value)
|
|
{
|
|
if (!Using_in_game_options) {
|
|
return;
|
|
}
|
|
|
|
const OptionBase* thisOpt = getOptionByKey(std::move(key));
|
|
if (thisOpt != nullptr) {
|
|
auto values = thisOpt->getValidValues();
|
|
thisOpt->setValueDescription(values[value]);
|
|
}
|
|
}
|
|
|
|
//Sets value saved within the option, but does not actually change the variables tied to the option
|
|
//Used for persistence and UI updates
|
|
void OptionsManager::set_ingame_range_option(SCP_string key, int value)
|
|
{
|
|
if (!Using_in_game_options) {
|
|
return;
|
|
}
|
|
|
|
const OptionBase* thisOpt = getOptionByKey(std::move(key));
|
|
if (thisOpt != nullptr) {
|
|
SCP_string newVal = std::to_string(value); // OptionsManager stores values as serialized strings
|
|
thisOpt->setValueDescription({newVal.c_str(), newVal.c_str()});
|
|
}
|
|
}
|
|
|
|
//Sets the value saved within the option, but does not actually change the variables tied to the option
|
|
//Used for persistence and UI updates
|
|
void OptionsManager::set_ingame_range_option(SCP_string key, float value)
|
|
{
|
|
if (!Using_in_game_options) {
|
|
return;
|
|
}
|
|
|
|
const OptionBase* thisOpt = getOptionByKey(std::move(key));
|
|
if (thisOpt != nullptr) {
|
|
SCP_string newVal = std::to_string(value); // OptionsManager stores values as serialized strings
|
|
thisOpt->setValueDescription({newVal.c_str(), newVal.c_str()});
|
|
}
|
|
}
|
|
|
|
} // namespace options
|