Files
Fabian WoltermannandClaude Opus 5 9ec157833b Add command-line option overrides to options system (#7561)
* 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]>
2026-09-10 13:14:18 +02:00

100 lines
3.4 KiB
C++

#pragma once
#include "globalincs/pstypes.h"
#include "libs/jansson.h"
#include <memory>
#include <optional>
namespace options {
// Forward declaration
class OptionBase;
class OptionsManager {
OptionsManager();
// A value forced by something outside of the options system itself (currently only the command line).
// This takes priority over the persisted config value and over any in-session edit. The options UI shows
// an overridden option as a disabled control with the reason in its tooltip.
struct OverrideEntry {
std::unique_ptr<json_t> value;
SCP_string reason; // human-readable source, e.g. "-no_vsync", shown in the options UI
};
SCP_unordered_map<SCP_string, OverrideEntry> _config_overrides;
SCP_unordered_map<SCP_string, std::unique_ptr<json_t>> _changed_values;
SCP_unordered_map<SCP_string, const OptionBase*> _optionsMapping;
// Enforced options are hidden from the player and do not load values from user settings
SCP_unordered_set<SCP_string> _enforcedOptions;
SCP_vector<std::shared_ptr<const OptionBase>> _options;
bool _optionsSorted = false;
public:
static OptionsManager* instance();
~OptionsManager();
std::optional<std::unique_ptr<json_t>> getValueFromConfig(const SCP_string& key) const;
void setConfigValue(const SCP_string& key, std::unique_ptr<json_t>&& value);
void setOverride(const SCP_string& key, const SCP_string& json, const SCP_string& reason);
// Overload that takes an already-built JSON value. Prefer this one: it lets a caller reuse the option's
// own serializer instead of formatting the JSON by hand, which can produce a value the option's
// deserializer then rejects.
void setOverride(const SCP_string& key, std::unique_ptr<json_t>&& value, const SCP_string& reason);
// Returns true if this key currently has an override.
bool isOverridden(const SCP_string& key) const;
// Returns the reason string passed to setOverride() for this key, if it is currently overridden.
std::optional<SCP_string> getOverrideReason(const SCP_string& key) const;
// Removes all overrides. For tests, so that one test's overrides do not leak into the next.
void clearOverrides();
const OptionBase* addOption(std::shared_ptr<const OptionBase>&& option);
void removeOption(const std::shared_ptr<const OptionBase>& option);
const OptionBase* getOptionByKey(SCP_string name);
void enforceOption(const SCP_string& key);
void unenforceOption(const SCP_string& key);
bool isOptionEnforced(const SCP_string& key) const;
const SCP_vector<std::shared_ptr<const options::OptionBase>>& getOptions();
bool persistOptionChanges(const options::OptionBase* option);
/**
* @brief Persists changes to the disk
*
* This writes all the outstanding changes to the config file and returns a vector of all the options that indicated
* that they could not change the value. If the return value is empty then all changes were applied but if it is not
* empty then a restart is required for all the options to take effect.
*
* @return A vector of options that did not support changing the value
*/
SCP_vector<const options::OptionBase*> persistChanges();
void discardChanges();
void loadInitialValues();
void printValues();
void set_ingame_binary_option(SCP_string key, bool value);
void set_ingame_multi_option(SCP_string key, int value);
void set_ingame_range_option(SCP_string key, int value);
void set_ingame_range_option(SCP_string key, float value);
};
}