From c7dcf11b697bff2615a0c1a26da543de8a8b7a13 Mon Sep 17 00:00:00 2001 From: the-e Date: Sat, 4 Jul 2026 17:37:48 +0200 Subject: [PATCH 1/3] 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. --- code/cmdline/cmdline.cpp | 86 +- code/cmdline/cmdline.h | 2 - code/graphics/2d.cpp | 69 +- code/graphics/2d.h | 4 + code/localization/localize.cpp | 21 +- code/options/OptionsManager.cpp | 34 +- code/options/OptionsManager.h | 14 +- code/options/dialogs/ingame_options_ui.cpp | 19 +- code/scripting/api/objs/option.cpp | 22 + freespace2/freespace.cpp | 4 + test/src/options/test_options_manager.cpp | 246 +++ test/src/source_groups.cmake | 4 + test/test_data/data/cfile_settings.ini | 0 test/test_data/data/graphics_settings.ini | 0 test/test_data/data/menuui_settings.ini | 0 test/test_data/data/mod_settings.ini | 0 test/test_data/data/model_settings.ini | 0 test/test_data/data/parselo_settings.ini | 0 test/test_data/data/pilotfile_settings.ini | 0 test/test_data/data/players/presets/asdf.json | 1826 +++++++++++++++++ test/test_data/data/scripting_settings.ini | 0 test/test_data/data/vecmat_settings.ini | 0 test/test_data/data/weapon_settings.ini | 0 test/test_data/pilotfile/data/asdf.hdp | 197 ++ 24 files changed, 2513 insertions(+), 35 deletions(-) create mode 100644 test/src/options/test_options_manager.cpp create mode 100644 test/test_data/data/cfile_settings.ini create mode 100644 test/test_data/data/graphics_settings.ini create mode 100644 test/test_data/data/menuui_settings.ini create mode 100644 test/test_data/data/mod_settings.ini create mode 100644 test/test_data/data/model_settings.ini create mode 100644 test/test_data/data/parselo_settings.ini create mode 100644 test/test_data/data/pilotfile_settings.ini create mode 100644 test/test_data/data/players/presets/asdf.json create mode 100644 test/test_data/data/scripting_settings.ini create mode 100644 test/test_data/data/vecmat_settings.ini create mode 100644 test/test_data/data/weapon_settings.ini create mode 100644 test/test_data/pilotfile/data/asdf.hdp diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index 7d3cd575ca1..d7b9d1013f1 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -15,6 +15,7 @@ #include "globalincs/pstypes.h" #include "globalincs/systemvars.h" #include "globalincs/version.h" +#include "graphics/2d.h" #include "graphics/openxr.h" #include "graphics/shadows.h" #include "hud/hudconfig.h" @@ -1780,38 +1781,42 @@ bool SetCmdlineParams() } // d3d windowed + // We need to set both the raw global (for the legacy !Using_in_game_options path) and the + // Option override (for the modern path) since either backend may be the one actually consulted. if(window_arg.found()) { - // We need to set both values since we don't know if we are going to use the new config system - //options::OptionsManager::instance()->setOverride("Graphics.WindowMode", "0"); + options::OptionsManager::instance()->setOverride("Graphics.WindowMode", "0", "-window"); Cmdline_window = true; } if ( fullscreen_window_arg.found( ) ) { - //options::OptionsManager::instance()->setOverride("Graphics.WindowMode", "1"); + options::OptionsManager::instance()->setOverride("Graphics.WindowMode", "1", "-fullscreen_window"); Cmdline_fullscreen_window = true; Cmdline_window = false; /* Make sure no-one sets both */ } if(render_res_arg.found() || deprecated_res_arg.found()){ + const char* res_flag_name; if (render_res_arg.found()) { Cmdline_res = render_res_arg.str(); + res_flag_name = "-render_res"; } else { Cmdline_res = deprecated_res_arg.str(); + res_flag_name = "-res"; mprintf(("Deprecated -res argument. Use -render_res instead...\n")); } - /*int width = 0; + int width = 0; int height = 0; if (sscanf(Cmdline_res, "%dx%d", &width, &height) == 2) { SCP_string override; sprintf(override, "{\"width\":%d,\"height\":%d}", width, height); - options::OptionsManager::instance()->setOverride("Graphics.Resolution", override); + options::OptionsManager::instance()->setOverride("Graphics.Resolution", override, res_flag_name); } else { Warning(LOCATION, "Failed to parse -res parameter \"%s\". Must be in format \"x\".\n", Cmdline_res); - }*/ + } } if(window_res_arg.found()){ int width = 0; @@ -1940,6 +1945,14 @@ bool SetCmdlineParams() } else { VIEWER_ZOOM_DEFAULT = DEFAULT_FOV; } + + // VIEWER_ZOOM_DEFAULT is only ever a bare float this early (before any VR/asymmetric FOV setup runs), + // but guard the variant access defensively rather than assume it. + if (std::holds_alternative(VIEWER_ZOOM_DEFAULT)) { + SCP_string override; + sprintf(override, "%f", std::get(VIEWER_ZOOM_DEFAULT)); + options::OptionsManager::instance()->setOverride("Graphics.FOV", override, "-fov"); + } } if ( fov_cockpit_arg.found() ) { @@ -1950,6 +1963,16 @@ bool SetCmdlineParams() else { COCKPIT_ZOOM_DEFAULT = VIEWER_ZOOM_DEFAULT; } + + // Graphics.CockpitFOV's change listener only applies its value when Graphics.CockpitFOVToggle is on, so + // force that on too -- otherwise the modern options path would silently ignore -fov_cockpit. + options::OptionsManager::instance()->setOverride("Graphics.CockpitFOVToggle", "true", "-fov_cockpit"); + + if (std::holds_alternative(COCKPIT_ZOOM_DEFAULT)) { + SCP_string override; + sprintf(override, "%f", std::get(COCKPIT_ZOOM_DEFAULT)); + options::OptionsManager::instance()->setOverride("Graphics.CockpitFOV", override, "-fov_cockpit"); + } } if (vr.found()) { @@ -1968,13 +1991,13 @@ bool SetCmdlineParams() Cmdline_fullscreen_window = false; Cmdline_window = true; /* Make sure no-one sets both */ - removeWindowModeOption(); + options::OptionsManager::instance()->setOverride("Graphics.WindowMode", "0", "-vr"); // Turn off VSync, since we definitely don't want to be capped by the main monitor // the OpenXR runtime should have a buffered wait which will behave, effectively, // as a VSync to VR goggle driver Gr_enable_vsync = false; - removeVSyncOption(); + options::OptionsManager::instance()->setOverride("Graphics.VSync", "false", "-vr"); openxr_prepare(); } @@ -2029,6 +2052,7 @@ bool SetCmdlineParams() if(no_vsync_arg.found() ) { Gr_enable_vsync = false; + options::OptionsManager::instance()->setOverride("Graphics.VSync", "false", "-no_vsync"); } if ( normal_arg.found() ) { @@ -2062,11 +2086,16 @@ bool SetCmdlineParams() case 5: Gr_aa_mode = AntiAliasMode::SMAA_High; break; - case 6: + case 6: Gr_aa_mode = AntiAliasMode::SMAA_Ultra; break; } } + + SCP_string override; + sprintf(override, "%d", static_cast(Gr_aa_mode)); + options::OptionsManager::instance()->setOverride("Graphics.AAMode", override, + post_process_aa_preset_arg.found() ? "-aa_preset" : "-aa"); } if (msaa_enabled_arg.found()) { @@ -2082,6 +2111,10 @@ bool SetCmdlineParams() Cmdline_msaa_enabled = 0; break; } + + SCP_string override; + sprintf(override, "%d", Cmdline_msaa_enabled); + options::OptionsManager::instance()->setOverride("Graphics.MSAASamples", override, "-msaa"); } if ( glow_arg.found() ) @@ -2207,6 +2240,7 @@ bool SetCmdlineParams() if ( softparticles_arg.found() ) { Cmdline_softparticles = 1; + options::OptionsManager::instance()->setOverride("Graphics.SoftParticles", "true", "-soft_particles"); } if ( fb_explosions_arg.found() ) @@ -2214,14 +2248,23 @@ bool SetCmdlineParams() Gr_framebuffer_effects.set(FramebufferEffects::Shockwaves, true); } - if (fb_thrusters_arg.found()) + if (fb_thrusters_arg.found()) { Gr_framebuffer_effects.set(FramebufferEffects::Thrusters, true); } + if (fb_explosions_arg.found() || fb_thrusters_arg.found()) { + SCP_string override; + sprintf(override, "%llu", static_cast(Gr_framebuffer_effects.to_u64())); + options::OptionsManager::instance()->setOverride("Graphics.FramebufferEffects", override, + fb_explosions_arg.found() && fb_thrusters_arg.found() ? "-fb_explosions -fb_thrusters" + : fb_explosions_arg.found() ? "-fb_explosions" : "-fb_thrusters"); + } + if ( no_postprocess_arg.found() ) { Gr_post_processing_enabled = false; + options::OptionsManager::instance()->setOverride("Graphics.PostProcessing", "false", "-no_post_process"); } if( reparse_mainhall_arg.found() ) @@ -2235,12 +2278,17 @@ bool SetCmdlineParams() Shadow_quality_uses_mod_option = true; Shadow_quality = ShadowQuality::Medium; + + // Deliberately not calling setOverride() here: mod_table_init() (which runs after cmdline parsing but + // before OptionsManager::loadInitialValues()) may still adjust Shadow_quality via game_settings.tbl's + // "$Shadow Quality Default:" when Shadow_quality_uses_mod_option is set, and an override registered now + // would freeze in the Medium fallback and ignore that later adjustment. } if( shadow_quality_arg.found() ) { // set that we are not using default shadow quality level --wookieejedi - Shadow_quality_uses_mod_option = false; + Shadow_quality_uses_mod_option = false; switch (shadow_quality_arg.get_int()) { case 0: @@ -2263,6 +2311,12 @@ bool SetCmdlineParams() Shadow_quality = ShadowQuality::Disabled; break; } + + // Unlike `-enable_shadows` alone, an explicit quality level is an unambiguous hard override that + // nothing else (mod defaults included) should be able to silently override. + SCP_string override; + sprintf(override, "%d", static_cast(Shadow_quality)); + options::OptionsManager::instance()->setOverride("Graphics.Shadows", override, "-shadow_quality"); } if( rt_shadows_arg.found() ) @@ -2276,16 +2330,22 @@ bool SetCmdlineParams() if( no_deferred_lighting_arg.found() ) { Cmdline_no_deferred_lighting = 1; + options::OptionsManager::instance()->setOverride("Graphics.DeferredLighting", "false", "-no_deferred"); } if( deferred_lighting_cockpit_arg.found() ) { Cmdline_deferred_lighting_cockpit = true; + options::OptionsManager::instance()->setOverride("Graphics.DeferredCockpitLighting", "true", "-deferred_cockpit"); } - if (anisotropy_level_arg.found()) + if (anisotropy_level_arg.found()) { Cmdline_aniso_level = anisotropy_level_arg.get_int(); + + SCP_string override; + sprintf(override, "%.1f", static_cast(Cmdline_aniso_level)); + options::OptionsManager::instance()->setOverride("Graphics.Anisotropy", override, "-anisotropic_filter"); } if (frame_profile_write_file.found()) @@ -2372,9 +2432,11 @@ bool SetCmdlineParams() if (vulkan.found()) { Cmdline_graphics_api = GraphicsAPI::Vulkan; + options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-vulkan"); } else if (opengl.found()) { Cmdline_graphics_api = GraphicsAPI::OpenGL; + options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-opengl"); } //Deprecated flags - CommanderDJ diff --git a/code/cmdline/cmdline.h b/code/cmdline/cmdline.h index c2c1c4911b3..43a698b6d6c 100644 --- a/code/cmdline/cmdline.h +++ b/code/cmdline/cmdline.h @@ -166,8 +166,6 @@ extern int Cmdline_multithreading; enum class WeaponSpewType { NONE = 0, STANDARD, ALL }; extern WeaponSpewType Cmdline_spew_weapon_stats; -extern void removeVSyncOption(); -extern void removeWindowModeOption(); extern void removeResolutionOption(); extern void removeResolutionVROption(); diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index d76b06f6365..1a39c297396 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -223,6 +223,70 @@ const auto LightingOption __UNUSED = options::OptionBuilder("Graphics.Light .parser(parse_lighting_func) .finish(); +// The Graphics.RenderAPI option only exists in builds that actually support choosing a backend at runtime. +// In an OpenGL-only build there's nothing to choose between, so there's no point cluttering the options menu +// with a one-item dropdown -- gr_get_configured_render_api() just returns the fixed default in that case. +#ifdef WITH_VULKAN + +int Gr_configured_render_api = GR_OPENGL; + +static void parse_render_api_func() +{ + SCP_string value; + stuff_string(value, F_NAME); + if (lcase_equal(value, "opengl")) { + Gr_configured_render_api = GR_OPENGL; + } else if (lcase_equal(value, "vulkan")) { + Gr_configured_render_api = GR_VULKAN; + } else { + error_display(0, "%s is an invalid render API", value.c_str()); + } +} + +static SCP_vector render_api_enumerator() { return {GR_OPENGL, GR_VULKAN}; } + +static SCP_string render_api_display(const int& api) +{ + switch (api) { + case GR_VULKAN: + return "Vulkan"; + case GR_OPENGL: + default: + return "OpenGL"; + } +} + +// Read directly via getValue() at the one call site that needs it (freespace.cpp, right before gr_init()), +// rather than bound to a global via a change listener -- like Resolution/Anisotropy, this can't take effect +// without a restart, so there is no "live" value to keep in sync. +// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton +static auto RenderAPIOption __UNUSED = options::OptionBuilder("Graphics.RenderAPI", + SCP_string("Render API"), + SCP_string("Selects the rendering backend used by the engine. Requires a restart to take effect.")) + .category(std::make_pair("Graphics", 1825)) + .level(options::ExpertLevel::Advanced) + .enumerator(render_api_enumerator) + .display(render_api_display) + .flags({options::OptionFlags::ForceMultiValueSelection}) + .default_func([]() { return Gr_configured_render_api; }) + .importance(99) + .parser(parse_render_api_func) + .finish(); + +int gr_get_configured_render_api() +{ + return RenderAPIOption->getValue(); +} + +#else + +int gr_get_configured_render_api() +{ + return GR_DEFAULT; +} + +#endif + os::ViewportState Gr_configured_window_state = os::ViewportState::Fullscreen; static bool mode_change_func(os::ViewportState state, bool initial) @@ -273,11 +337,6 @@ static auto WindowModeOption __UNUSED = options::OptionBuilderremoveOption(WindowModeOption); -} - // coverity[GLOBAL_INIT_ORDER] -- safe; Hook::Factory() uses Meyers singleton const std::shared_ptr> OnFrameHook = scripting::OverridableHook<>::Factory( "On Frame", "Called every frame as the last action before showing the frame result to the user.", {}, std::nullopt, CHA_ONFRAME); diff --git a/code/graphics/2d.h b/code/graphics/2d.h index facfd95ac35..423c05e3d58 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -1054,6 +1054,10 @@ extern const char *Resolution_prefixes[GR_NUM_RESOLUTIONS]; extern bool gr_init(std::unique_ptr&& graphicsOps, GraphicsAPI d_mode = GraphicsAPI::Default, int d_width = GR_DEFAULT, int d_height = GR_DEFAULT, int d_depth = GR_DEFAULT); +// The render API (GR_OPENGL/GR_VULKAN) selected via the "Graphics.RenderAPI" in-game option (or a mod's +// default settings table). Does not account for the -vulkan command line override; see Cmdline_vulkan for that. +extern int gr_get_configured_render_api(); + extern void gr_screen_resize(int width, int height); extern int gr_get_resolution_class(int width, int height); diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 2387709d4c9..3522859a600 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -217,7 +217,23 @@ void lcl_init(int lang_init) int lang = -1; if (lang_init < 0) { - // first we start with any persisted in-game option choice + // Register a -lang command line flag as an override *before* consulting the in-game option below, so + // that lookup already returns the cmdline-selected language on the modern (Using_in_game_options) path, + // instead of the override only being reachable via the redundant "now try the commandline" tier further + // down (which only ever ran on the legacy, !Using_in_game_options path anyway, since the option lookup's + // always-valid default index meant `lang` was never still negative by the time execution reached it). + if (!Cmdline_lang.empty()) { + int cmdline_lang_idx = lcl_find_lang_index_by_name(Cmdline_lang); + if (SCP_vector_inbounds(Lcl_languages, cmdline_lang_idx)) { + SCP_string override_json = + json_dump_string_new(language_serializer(cmdline_lang_idx), JSON_COMPACT | JSON_ENSURE_ASCII); + options::OptionsManager::instance()->setOverride("Game.Language", override_json, "-lang"); + } else { + Warning(LOCATION, "Unrecognized -lang value \"%s\"; ignoring.", Cmdline_lang.c_str()); + } + } + + // first we start with any persisted in-game option choice (or the -lang override registered above) if (Using_in_game_options) { lang = LanguageOption->getValue(); @@ -227,7 +243,8 @@ void lcl_init(int lang_init) } } - // now try the the commandline + // now try the the commandline (only still reachable on the legacy !Using_in_game_options path, since + // the override above already resolves this for the modern path) if (lang < 0) { if (!Cmdline_lang.empty()) { lang = lcl_find_lang_index_by_name(Cmdline_lang); diff --git a/code/options/OptionsManager.cpp b/code/options/OptionsManager.cpp index b18bebebb10..0c54556dcc4 100644 --- a/code/options/OptionsManager.cpp +++ b/code/options/OptionsManager.cpp @@ -39,14 +39,9 @@ OptionsManager* options::OptionsManager::instance() //Gets the value of an option from the Config using the option key std::optional> OptionsManager::getValueFromConfig(const SCP_string& key) const { - 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.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(override_iter->second.get()); - } - + // An in-session edit the player made but hasn't saved/discarded yet always wins, even over a command-line + // override, so that an overridden control in the options menu remains meaningfully editable rather than + // silently reverting on every read. auto changed_iter = _changed_values.find(key); if (changed_iter != _changed_values.end()) { json_incref(changed_iter->second.get()); @@ -54,6 +49,14 @@ std::optional> OptionsManager::getValueFromConfig(const return std::unique_ptr(changed_iter->second.get()); } + 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(override_iter->second.value.get()); + } + auto parts = parse_key(key); if (parts.first.empty()) { // Invalid key @@ -84,14 +87,25 @@ void OptionsManager::setConfigValue(const SCP_string& key,std::unique_ptr(el)); + // 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::unique_ptr(el), reason}); +} + +std::optional 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 diff --git a/code/options/OptionsManager.h b/code/options/OptionsManager.h index 1ab96662126..ca489a134cb 100644 --- a/code/options/OptionsManager.h +++ b/code/options/OptionsManager.h @@ -13,7 +13,14 @@ class OptionBase; class OptionsManager { OptionsManager(); - SCP_unordered_map> _config_overrides; + // A value forced by something outside of the options system itself (currently only the command line). + // This takes priority over the persisted config value, but not over an in-session edit the player hasn't + // saved or discarded yet (see getValueFromConfig()). + struct OverrideEntry { + std::unique_ptr value; + SCP_string reason; // human-readable source, e.g. "-no_vsync", shown in the options UI + }; + SCP_unordered_map _config_overrides; SCP_unordered_map> _changed_values; @@ -34,7 +41,10 @@ class OptionsManager { void setConfigValue(const SCP_string& key, std::unique_ptr&& value); - void setOverride(const SCP_string& key, const SCP_string& json); + void setOverride(const SCP_string& key, const SCP_string& json, const SCP_string& reason); + + // Returns the reason string passed to setOverride() for this key, if it is currently overridden. + std::optional getOverrideReason(const SCP_string& key) const; const OptionBase* addOption(std::shared_ptr&& option); diff --git a/code/options/dialogs/ingame_options_ui.cpp b/code/options/dialogs/ingame_options_ui.cpp index fd6aa3fa9bd..2fffa68dcc7 100644 --- a/code/options/dialogs/ingame_options_ui.cpp +++ b/code/options/dialogs/ingame_options_ui.cpp @@ -116,9 +116,24 @@ void OptUi::build_options_list(const char* category) const } } + // Capture hover state now, since the last ImGui item is still the control itself -- adding the + // override marker below would otherwise become the "last item" and break IsItemHovered() for the tooltip. + bool isHovered = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay); + + auto overrideReason = options::OptionsManager::instance()->getOverrideReason(thisOpt->getConfigKey()); + if (overrideReason.has_value()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "(cmdline)"); + } + // Add a tooltip with the option description on mouseover - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) - ImGui::SetTooltip("%s", thisOpt->getDescription().c_str()); + if (isHovered) { + if (overrideReason.has_value()) { + ImGui::SetTooltip("%s\n\nSet via command line: %s", thisOpt->getDescription().c_str(), overrideReason->c_str()); + } else { + ImGui::SetTooltip("%s", thisOpt->getDescription().c_str()); + } + } } } } diff --git a/code/scripting/api/objs/option.cpp b/code/scripting/api/objs/option.cpp index 05449218705..7adb66963d3 100644 --- a/code/scripting/api/objs/option.cpp +++ b/code/scripting/api/objs/option.cpp @@ -250,6 +250,28 @@ ADE_FUNC(getValidValues, return ade_set_args(L, "t", &table); } +ADE_VIRTVAR(OverrideReason, + l_Option, + nullptr, + "If this option's value is currently forced by something outside the options system (e.g. a command line " + "flag), this is a human-readable description of the source. nil if the option is not currently overridden.", + "string", + "The override reason, or nil if not overridden or on error") +{ + option_h* opt; + if (!ade_get_args(L, "o", l_Option.GetPtr(&opt))) { + return ADE_RETURN_NIL; + } + if (!opt->isValid()) { + return ADE_RETURN_NIL; + } + + auto reason = options::OptionsManager::instance()->getOverrideReason(opt->get()->getConfigKey()); + if (!reason.has_value()) { + return ADE_RETURN_NIL; + } + return ade_set_args(L, "s", reason->c_str()); +} ADE_FUNC(persistChanges, l_Option, nullptr, "Immediately persists any changes made to this specific option.", "boolean", "true if the change was applied successfully, false otherwise. nil on error.") diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index 209006bd2d3..2445ba3bcb2 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -1911,6 +1911,10 @@ void game_init() sdlGraphicsOperations.reset(new SDLGraphicsOperations()); } + if (Using_in_game_options) { + graphics_api = gr_get_configured_render_api(); + } + if (!gr_init(std::move(sdlGraphicsOperations))) { os::dialogs::Message(os::dialogs::MESSAGEBOX_ERROR, "Error initializing graphics!"); exit(1); diff --git a/test/src/options/test_options_manager.cpp b/test/src/options/test_options_manager.cpp new file mode 100644 index 00000000000..bd9405cd5a1 --- /dev/null +++ b/test/src/options/test_options_manager.cpp @@ -0,0 +1,246 @@ + +#include "options/OptionsManager.h" +#include "options/Option.h" +#include "graphics/2d.h" +#include "graphics/shadows.h" +#include "localization/localize.h" + +#include + +using namespace options; + +// Probe: real Option instances are defined via static initializers scattered across many translation +// units in libcode.a. Confirm the linker actually pulled at least one of them into the unittests binary +// (rather than static init being stripped as unreferenced) before trusting any test that exercises a real +// option's deserializer. +TEST(OptionsManagerOverride, RealOptionsAreRegistered) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.VSync"); + ASSERT_NE(opt, nullptr) << "Graphics.VSync was not registered -- static Option initializers are not " + "linked into this test binary, so override round-trip tests against real " + "options are not possible here."; +} + +// Regression coverage for the cmdline-priority-rework: OptionsManager::getValueFromConfig() must resolve +// a command-line override ahead of a persisted ini value, but a live, unsaved in-session edit (_changed_values) +// must win over that override so an overridden control in the options UI stays meaningfully editable. + +TEST(OptionsManagerOverride, OverrideIsReturnedWhenNoEditIsPending) +{ + auto* mgr = OptionsManager::instance(); + + mgr->setOverride("Test.OverridePriority.Fresh", "42", "-test_flag"); + + auto result = mgr->getValueFromConfig("Test.OverridePriority.Fresh"); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(json_integer_value(result->get()), 42); +} + +TEST(OptionsManagerOverride, SessionEditWinsOverOverride) +{ + auto* mgr = OptionsManager::instance(); + + mgr->setOverride("Test.OverridePriority.Edited", "42", "-test_flag"); + mgr->setConfigValue("Test.OverridePriority.Edited", std::unique_ptr(json_integer(7))); + + auto result = mgr->getValueFromConfig("Test.OverridePriority.Edited"); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(json_integer_value(result->get()), 7); + + // The override should resume winning once the pending edit is discarded (e.g. the player closed the + // options menu without saving), so it survives for the rest of the session. + mgr->discardChanges(); + + result = mgr->getValueFromConfig("Test.OverridePriority.Edited"); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(json_integer_value(result->get()), 42); +} + +TEST(OptionsManagerOverride, OverrideReasonIsQueryable) +{ + auto* mgr = OptionsManager::instance(); + + ASSERT_FALSE(mgr->getOverrideReason("Test.OverridePriority.NeverSet").has_value()); + + mgr->setOverride("Test.OverridePriority.Reason", "1", "-my_flag"); + + auto reason = mgr->getOverrideReason("Test.OverridePriority.Reason"); + ASSERT_TRUE(reason.has_value()); + ASSERT_EQ(*reason, "-my_flag"); +} + +// The tests above exercise getValueFromConfig() directly with synthetic integer payloads. The tests below +// instead go through real, statically-registered Option instances (confirmed live by the probe test +// above) using the exact JSON payload shapes cmdline.cpp constructs for each type, so a malformed payload +// shows up as a deserialization failure here instead of silently falling back to that option's default_func +// and masking the bug. +// +// These call the typed Option::getValue() (via a static_cast down to the option's real, statically-known +// type) rather than getCurrentValueDescription(), because several options' display functions call XSTR() for +// localized display strings (e.g. "On"/"Off", the shadow quality tiers), and this test binary never runs +// lcl_init() to set up the localization tables XSTR needs. getValue() only exercises the deserializer, which +// is exactly the contract these tests are checking anyway. + +TEST(OptionsManagerOverride, BoolOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.VSync"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast*>(opt); + + auto* mgr = OptionsManager::instance(); + + mgr->setOverride("Graphics.VSync", "false", "-no_vsync"); + ASSERT_FALSE(typedOpt->getValue()); + + mgr->setOverride("Graphics.VSync", "true", "-no_vsync"); + ASSERT_TRUE(typedOpt->getValue()); +} + +TEST(OptionsManagerOverride, ResolutionObjectOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.Resolution"); + ASSERT_NE(opt, nullptr); + + // ResolutionInfo is a file-local type in 2d.cpp, not reachable from here, so this test goes through + // getCurrentValueDescription() instead -- safe because resolution_display() is a plain sprintf with no + // XSTR() call. + SCP_string override; + sprintf(override, "{\"width\":%d,\"height\":%d}", 1920, 1080); + OptionsManager::instance()->setOverride("Graphics.Resolution", override, "-res"); + + ASSERT_EQ(opt->getCurrentValueDescription().display, "1920x1080"); +} + +TEST(OptionsManagerOverride, EnumIntOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.Shadows"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast*>(opt); + + // Exactly the payload shape built in cmdline.cpp's -shadow_quality handling (static_cast(enum) as a + // plain decimal string). + OptionsManager::instance()->setOverride("Graphics.Shadows", "2", "-shadow_quality"); + + ASSERT_EQ(typedOpt->getValue(), ShadowQuality::Medium); +} + +// Graphics.RenderAPI only exists in builds compiled with Vulkan support (code/graphics/2d.cpp gates the whole +// option definition on #ifdef WITH_VULKAN) -- in an OpenGL-only build there's nothing to choose between, so +// gr_get_configured_render_api() just returns GR_DEFAULT and no option is registered at all. +#ifdef WITH_VULKAN + +TEST(OptionsManagerOverride, RenderAPIOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.RenderAPI"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast*>(opt); + + // Exactly the payload shape built in cmdline.cpp's -vulkan handling. + SCP_string override; + sprintf(override, "%d", GR_VULKAN); + OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-vulkan"); + + ASSERT_EQ(typedOpt->getValue(), GR_VULKAN); + ASSERT_EQ(gr_get_configured_render_api(), GR_VULKAN); +} + +TEST(OptionsManagerOverride, RenderAPIMenuInitPathDoesNotThrow) +{ + // Unlike VSync/Shadows/etc, RenderAPI's display function is plain text (no XSTR()), so -- unlike those -- + // it's safe to exercise the exact calls ingame_options_init() makes when building the options menu + // (getValidValues() for the dropdown entries, getCurrentValueDescription() for the current selection), + // giving at least some coverage of the menu-construction path itself, not just the deserializer. + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.RenderAPI"); + ASSERT_NE(opt, nullptr); + ASSERT_EQ(opt->getType(), OptionType::Selection); + ASSERT_EQ(opt->getValidValues().size(), 2u); + + OptionsManager::instance()->setOverride("Graphics.RenderAPI", std::to_string(GR_OPENGL), "-test"); + ASSERT_EQ(opt->getCurrentValueDescription().display, "OpenGL"); + + OptionsManager::instance()->setOverride("Graphics.RenderAPI", std::to_string(GR_VULKAN), "-vulkan"); + ASSERT_EQ(opt->getCurrentValueDescription().display, "Vulkan"); +} + +#else + +TEST(OptionsManagerOverride, RenderAPIIsAbsentWithoutVulkanSupport) +{ + ASSERT_EQ(OptionsManager::instance()->getOptionByKey("Graphics.RenderAPI"), nullptr); + ASSERT_EQ(gr_get_configured_render_api(), GR_DEFAULT); +} + +#endif + +TEST(OptionsManagerOverride, FlagsetOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.FramebufferEffects"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast>*>(opt); + + // Exactly the payload shape built in cmdline.cpp's -fb_explosions/-fb_thrusters handling + // (flagset::to_u64() as a plain decimal string). Bits 0 (Thrusters) and 1 (Shockwaves) both set == 3. + OptionsManager::instance()->setOverride("Graphics.FramebufferEffects", "3", "-fb_explosions -fb_thrusters"); + + auto value = typedOpt->getValue(); + ASSERT_TRUE(value[FramebufferEffects::Thrusters]); + ASSERT_TRUE(value[FramebufferEffects::Shockwaves]); +} + +TEST(OptionsManagerOverride, FloatOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.Anisotropy"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast*>(opt); + + // Exactly the payload shape built in cmdline.cpp's -anisotropic_filter handling. + OptionsManager::instance()->setOverride("Graphics.Anisotropy", "16.0", "-anisotropic_filter"); + + ASSERT_FLOAT_EQ(typedOpt->getValue(), 16.0f); +} + +TEST(OptionsManagerOverride, LanguageObjectOverrideRoundTrips) +{ + auto* opt = OptionsManager::instance()->getOptionByKey("Game.Language"); + ASSERT_NE(opt, nullptr); + auto* typedOpt = static_cast*>(opt); + + // Game.Language's real deserializer resolves a {"name","ext"} pair against Lcl_languages via + // lcl_find_lang_index_by_name(), rather than storing a plain index -- so unlike the other round-trip + // tests, this one needs at least one entry in that table to resolve against. A full lcl_init() is too + // heavy for this test binary, so a single throwaway entry is pushed directly and restored afterwards, + // since Lcl_languages is a shared global other tests may also touch. + auto original_languages = Lcl_languages; + + lang_info test_lang{}; + strcpy_s(test_lang.lang_name, "OverrideRoundTripTestLang"); + strcpy_s(test_lang.lang_ext, "orttl"); + Lcl_languages.push_back(test_lang); + int expected_idx = static_cast(Lcl_languages.size()) - 1; + + // Exactly the payload shape built in localize.cpp's detect_lang() (matching language_serializer()'s + // {"name":..,"ext":..} object). + SCP_string override; + sprintf(override, "{\"name\":\"%s\",\"ext\":\"%s\"}", test_lang.lang_name, test_lang.lang_ext); + OptionsManager::instance()->setOverride("Game.Language", override, "-lang"); + + ASSERT_EQ(typedOpt->getValue(), expected_idx); + + Lcl_languages = original_languages; +} + +TEST(OptionsManagerOverride, LaterOverrideReplacesEarlierOne) +{ + auto* mgr = OptionsManager::instance(); + + mgr->setOverride("Test.OverridePriority.Replaced", "1", "-first_flag"); + mgr->setOverride("Test.OverridePriority.Replaced", "2", "-second_flag"); + + auto result = mgr->getValueFromConfig("Test.OverridePriority.Replaced"); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(json_integer_value(result->get()), 2); + + auto reason = mgr->getOverrideReason("Test.OverridePriority.Replaced"); + ASSERT_TRUE(reason.has_value()); + ASSERT_EQ(*reason, "-second_flag"); +} diff --git a/test/src/source_groups.cmake b/test/src/source_groups.cmake index fa4c1c81d39..1c268dda86e 100644 --- a/test/src/source_groups.cmake +++ b/test/src/source_groups.cmake @@ -49,6 +49,10 @@ add_file_folder("model" model/test_modelread.cpp ) +add_file_folder("Options" + options/test_options_manager.cpp +) + add_file_folder("Parse" parse/test_parselo.cpp parse/test_replace.cpp diff --git a/test/test_data/data/cfile_settings.ini b/test/test_data/data/cfile_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/graphics_settings.ini b/test/test_data/data/graphics_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/menuui_settings.ini b/test/test_data/data/menuui_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/mod_settings.ini b/test/test_data/data/mod_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/model_settings.ini b/test/test_data/data/model_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/parselo_settings.ini b/test/test_data/data/parselo_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/pilotfile_settings.ini b/test/test_data/data/pilotfile_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/players/presets/asdf.json b/test/test_data/data/players/presets/asdf.json new file mode 100644 index 00000000000..3dc71e41d54 --- /dev/null +++ b/test/test_data/data/players/presets/asdf.json @@ -0,0 +1,1826 @@ +{ + "signature": 1599361872, + "version": 0, + "actions": [ + { + "bind": "TARGET_NEXT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_T" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_T" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_CLOSEST_HOSTILE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_H" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "2" + } + }, + { + "bind": "TARGET_PREV_CLOSEST_HOSTILE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_H" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_AUTO_TARGETING", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_H" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_CLOSEST_FRIENDLY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_F" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV_CLOSEST_FRIENDLY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_F" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_SHIP_IN_RETICLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_Y" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "4" + } + }, + { + "bind": "TARGET_CLOSEST_SHIP_ATTACKING_TARGET", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_G" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_LAST_TRANMISSION_SENDER", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_Y" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "STOP_TARGETING_SHIP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_T" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_SUBOBJECT_IN_RETICLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_V" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_SUBOBJECT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_S" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV_SUBOBJECT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_S" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "STOP_TARGETING_SUBSYSTEM", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_S" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MATCH_TARGET_SPEED", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_M" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_AUTO_MATCH_TARGET_SPEED", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_M" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "FIRE_PRIMARY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_LCTRL" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "0" + } + }, + { + "bind": "FIRE_SECONDARY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_SPACEBAR" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "1" + } + }, + { + "bind": "CYCLE_NEXT_PRIMARY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PERIOD" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CYCLE_PREV_PRIMARY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_COMMA" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CYCLE_SECONDARY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_DIVIDE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CYCLE_NUM_MISSLES", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_DIVIDE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "LAUNCH_COUNTERMEASURE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_X" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "3" + } + }, + { + "bind": "FORWARD_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_A" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "REVERSE_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_Z" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "BANK_LEFT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD7" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "BANK_RIGHT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD9" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PITCH_FORWARD", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD8" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PITCH_BACK", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD2" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "YAW_LEFT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD4" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "YAW_RIGHT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD6" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ZERO_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_BACKSP" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MAX_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_SLASH" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ONE_THIRD_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_LBRACKET" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TWO_THIRDS_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_RBRACKET" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PLUS_5_PERCENT_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_EQUAL" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MINUS_5_PERCENT_THROTTLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_MINUS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ATTACK_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_A" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DISARM_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_Z" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DISABLE_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_D" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ATTACK_SUBSYSTEM_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_V" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CAPTURE_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_X" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ENGAGE_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_E" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "FORM_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_W" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "IGNORE_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_I" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PROTECT_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_P" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "COVER_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_C" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "WARP_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_J" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "REARM_MESSAGE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_R" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_CLOSEST_SHIP_ATTACKING_SELF", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_R" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "6" + } + }, + { + "bind": "VIEW_CHASE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADMULTIPLY" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_EXTERNAL", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADPERIOD" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_EXTERNAL_TOGGLE_CAMERA_LOCK", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADENTER" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_SLEW", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD0" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_OTHER_SHIP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADDIVIDE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_DIST_INCREASE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADPLUS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_DIST_DECREASE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PADMINUS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_CENTER", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAD5" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PADLOCK_UP", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "33" + } + }, + { + "bind": "PADLOCK_DOWN", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "32" + } + }, + { + "bind": "PADLOCK_LEFT", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "34" + } + }, + { + "bind": "PADLOCK_RIGHT", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "35" + } + }, + { + "bind": "RADAR_RANGE_CYCLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_RAPOSTRO" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SQUADMSG_MENU", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_C" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHOW_GOALS", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "END_MISSION", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_J" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_TARGETS_TARGET", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_J" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "AFTERBURNER", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_TAB" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "5" + } + }, + { + "bind": "INCREASE_WEAPON", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_INSERT" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DECREASE_WEAPON", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_DELETE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "INCREASE_SHIELD", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_HOME" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DECREASE_SHIELD", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_END" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "INCREASE_ENGINE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAGEUP" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DECREASE_ENGINE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_PAGEDOWN" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ETS_EQUALIZE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_D" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHIELD_EQUALIZE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_Q" + }, + "secondary": { + "cid": "CID_JOY0", + "flags": "NONE", + "input": "7" + } + }, + { + "bind": "SHIELD_XFER_TOP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_UP" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHIELD_XFER_BOTTOM", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_DOWN" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHIELD_XFER_LEFT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_LEFT" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHIELD_XFER_RIGHT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_RIGHT" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "XFER_SHIELD", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_SCROLLOCK" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "XFER_LASER", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_SCROLLOCK" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "GLIDE_WHEN_PRESSED", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "BANK_WHEN_PRESSED", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "SHOW_NAVMAP", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ADD_REMOVE_ESCORT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_E" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "ESCORT_CLEAR", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-SHIFT-KEY_E" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_ESCORT_SHIP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_E" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_CLOSEST_REPAIR_SHIP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_R" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_UNINSPECTED_CARGO", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_U" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV_UNINSPECTED_CARGO", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_U" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEWEST_SHIP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_N" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_LIVE_TURRET", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_K" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV_LIVE_TURRET", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_K" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_NEXT_BOMB", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_B" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TARGET_PREV_BOMB", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_B" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_MESSAGE_ALL", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_1" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_MESSAGE_FRIENDLY", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_2" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_MESSAGE_HOSTILE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_3" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_MESSAGE_TARGET", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_4" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_OBSERVER_ZOOM_TO", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_X" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TIME_SPEED_UP", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_PERIOD" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TIME_SLOW_DOWN", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_COMMA" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_HUD_CONTRAST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_L" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_TOGGLE_NETINFO", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_N" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "MULTI_SELF_DESTRUCT", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_END" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_HUD", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_O" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "RIGHT_SLIDE_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_3" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "LEFT_SLIDE_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_1" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "UP_SLIDE_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_PADPLUS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "DOWN_SLIDE_THRUST", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "SHIFT-KEY_PADENTER" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "HUD_TARGETBOX_TOGGLE_WIREFRAME", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-SHIFT-KEY_Q" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_TOPDOWN", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "VIEW_TRACK_TARGET", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "AUTO_PILOT_TOGGLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_A" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "NAV_CYCLE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_N" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_GLIDING", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "ALT-KEY_G" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CYCLE_PRIMARY_WEAPON_SEQUENCE", + "primary": { + "cid": "CID_KEYBOARD", + "flags": "NONE", + "input": "KEY_O" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CUSTOM_CONTROL_1", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CUSTOM_CONTROL_2", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CUSTOM_CONTROL_3", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CUSTOM_CONTROL_4", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CUSTOM_CONTROL_5", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "JOY_HEADING_AXIS", + "primary": { + "cid": "CID_JOY0", + "flags": "AXIS", + "input": "X_AXIS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "JOY_PITCH_AXIS", + "primary": { + "cid": "CID_JOY0", + "flags": "AXIS", + "input": "Y_AXIS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "JOY_BANK_AXIS", + "primary": { + "cid": "CID_JOY0", + "flags": "AXIS", + "input": "RX_AXIS" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "JOY_ABS_THROTTLE_AXIS", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "JOY_REL_THROTTLE_AXIS", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "COMMS_MENU_MOVE_UP", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "COMMS_MENU_MOVE_DOWN", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "COMMS_MENU_SELECT", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_HUD_SHADOWS", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "CYCLE_PRIMARY_WEAPON_PATTERN", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "TOGGLE_PHOTO_MODE", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PHOTO_MODE_FILTER_PREV", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PHOTO_MODE_FILTER_NEXT", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PHOTO_MODE_FILTER_RESET", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PHOTO_MODE_PARAM_DECREASE", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + }, + { + "bind": "PHOTO_MODE_PARAM_INCREASE", + "primary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + }, + "secondary": { + "cid": "CID_NONE", + "flags": "NONE", + "input": "NONE" + } + } + ] +} \ No newline at end of file diff --git a/test/test_data/data/scripting_settings.ini b/test/test_data/data/scripting_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/vecmat_settings.ini b/test/test_data/data/vecmat_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/data/weapon_settings.ini b/test/test_data/data/weapon_settings.ini new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_data/pilotfile/data/asdf.hdp b/test/test_data/pilotfile/data/asdf.hdp new file mode 100644 index 00000000000..f7a38812f35 --- /dev/null +++ b/test/test_data/pilotfile/data/asdf.hdp @@ -0,0 +1,197 @@ ++Version: 1 + ++Gauge: Builtin::LagGauge ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::WarningFlash ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::KillsGauge ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::OffscreenRange ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::ObjectiveNotify ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::TargetShield ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::TargetHullShieldIcon ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::SupportGauge ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::Radar ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::Throttle ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::ExtraTargetInfo ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::ThreatGauge ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::CommMenu ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::TargetMonitor ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::Reticle ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::CenterOfReticle ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::AttackingTargetCount ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::LeadIndicator ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::CurrentTargetDirection ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::PowerManagement ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::AutoTargetIcon ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::AutoSpeedMatchIcon ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::PlayerShield ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::MissionTime ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::TargetOrientation ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::WeaponsDisplay ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::MonitoringView ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::DirectivesView ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::WingmenStatus ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::Countermeasures ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::LockedMissileDirection ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::ClosestAttackingHostile ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::AfterburnerEnergy ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::WeaponsEnergy ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::OffscreenIndicator ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::CommVideo ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::DamageDisplay ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::WeaponLinking ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + ++Gauge: Builtin::MessageOutput ++Visible: 1 ++Popup: 0 ++Color: 0 255 0 144 + From 56ad29a68f3362c5dba91d283d7f0f3468a641f4 Mon Sep 17 00:00:00 2001 From: the-e Date: Sun, 5 Jul 2026 09:38:34 +0200 Subject: [PATCH 2/3] Add unit tests for `Graphics.Anisotropy` option validation with and without OpenGL --- test/src/options/test_options_manager.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/src/options/test_options_manager.cpp b/test/src/options/test_options_manager.cpp index bd9405cd5a1..909ae66f6f1 100644 --- a/test/src/options/test_options_manager.cpp +++ b/test/src/options/test_options_manager.cpp @@ -187,6 +187,12 @@ TEST(OptionsManagerOverride, FlagsetOverrideRoundTrips) ASSERT_TRUE(value[FramebufferEffects::Shockwaves]); } +// Graphics.Anisotropy is only registered in builds with the OpenGL backend compiled in +// (its Option lives in gropengltexture.cpp, which the CMake build excludes entirely +// when FSO_BUILD_WITH_OPENGL is off) -- so, like Graphics.RenderAPI above, round-trip +// coverage needs a build-config split rather than a single unconditional test. +#ifdef WITH_OPENGL + TEST(OptionsManagerOverride, FloatOverrideRoundTrips) { auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.Anisotropy"); @@ -199,6 +205,15 @@ TEST(OptionsManagerOverride, FloatOverrideRoundTrips) ASSERT_FLOAT_EQ(typedOpt->getValue(), 16.0f); } +#else + +TEST(OptionsManagerOverride, AnisotropyIsAbsentWithoutOpenGLSupport) +{ + ASSERT_EQ(OptionsManager::instance()->getOptionByKey("Graphics.Anisotropy"), nullptr); +} + +#endif + TEST(OptionsManagerOverride, LanguageObjectOverrideRoundTrips) { auto* opt = OptionsManager::instance()->getOptionByKey("Game.Language"); From ebb5d3c4c65e908e8ab350d77aa328d5645ef447 Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 21 Jul 2026 19:56:30 +0200 Subject: [PATCH 3/3] 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. --- code/cmdline/cmdline.cpp | 16 +++++++++---- code/graphics/2d.cpp | 28 +++++++++++++---------- code/graphics/2d.h | 4 ++-- freespace2/freespace.cpp | 4 ---- test/src/options/test_options_manager.cpp | 18 ++++++++------- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index d7b9d1013f1..fd1fc05df8d 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -1834,15 +1834,15 @@ bool SetCmdlineParams() } if (hdr_nits_arg.found()) { // presence of the flag alone forces HDR output on, overriding the in-game HDR setting - options::OptionsManager::instance()->setOverride("Graphics.HDR", "true"); + options::OptionsManager::instance()->setOverride("Graphics.HDR", "true", "-hdr"); if (hdr_nits_arg.has_param()) { float paperwhite_nits = 0.0f, peak_nits = 0.0f; if (sscanf(hdr_nits_arg.str(), "%f,%f", &paperwhite_nits, &peak_nits) == 2 && paperwhite_nits > 0.0f && peak_nits > 0.0f) { // std::to_string always includes a decimal point, so this is a valid JSON real // (json_unpack's "f" format rejects a bare integer like "400") - options::OptionsManager::instance()->setOverride("Graphics.HDRPaperWhite", std::to_string(paperwhite_nits)); - options::OptionsManager::instance()->setOverride("Graphics.HDRPeakLuminance", std::to_string(peak_nits)); + options::OptionsManager::instance()->setOverride("Graphics.HDRPaperWhite", std::to_string(paperwhite_nits), "-hdr"); + options::OptionsManager::instance()->setOverride("Graphics.HDRPeakLuminance", std::to_string(peak_nits), "-hdr"); } else { Warning(LOCATION, "Failed to parse -hdr parameter \"%s\". Must be in format \",\".\n", hdr_nits_arg.str()); } @@ -2325,6 +2325,10 @@ bool SetCmdlineParams() // has no effect if the renderer/hardware doesn't support // CAPABILITY_RAYTRACED_SHADOWS -- the CSM path is used either way. Shadow_render_method = ShadowRenderMethod::Raytraced; + + SCP_string override; + sprintf(override, "%d", static_cast(ShadowRenderMethod::Raytraced)); + options::OptionsManager::instance()->setOverride("Graphics.ShadowRenderMethod", override, "-rt_shadows"); } if( no_deferred_lighting_arg.found() ) @@ -2432,11 +2436,13 @@ bool SetCmdlineParams() if (vulkan.found()) { Cmdline_graphics_api = GraphicsAPI::Vulkan; - options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-vulkan"); + options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", + std::to_string(static_cast(GraphicsAPI::Vulkan)), "-vulkan"); } else if (opengl.found()) { Cmdline_graphics_api = GraphicsAPI::OpenGL; - options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-opengl"); + options::OptionsManager::instance()->setOverride("Graphics.RenderAPI", + std::to_string(static_cast(GraphicsAPI::OpenGL)), "-opengl"); } //Deprecated flags - CommanderDJ diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index 1a39c297396..96063241e6d 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -228,29 +228,29 @@ const auto LightingOption __UNUSED = options::OptionBuilder("Graphics.Light // with a one-item dropdown -- gr_get_configured_render_api() just returns the fixed default in that case. #ifdef WITH_VULKAN -int Gr_configured_render_api = GR_OPENGL; +static GraphicsAPI Gr_configured_render_api = GraphicsAPI::OpenGL; static void parse_render_api_func() { SCP_string value; stuff_string(value, F_NAME); if (lcase_equal(value, "opengl")) { - Gr_configured_render_api = GR_OPENGL; + Gr_configured_render_api = GraphicsAPI::OpenGL; } else if (lcase_equal(value, "vulkan")) { - Gr_configured_render_api = GR_VULKAN; + Gr_configured_render_api = GraphicsAPI::Vulkan; } else { error_display(0, "%s is an invalid render API", value.c_str()); } } -static SCP_vector render_api_enumerator() { return {GR_OPENGL, GR_VULKAN}; } +static SCP_vector render_api_enumerator() { return {GraphicsAPI::OpenGL, GraphicsAPI::Vulkan}; } -static SCP_string render_api_display(const int& api) +static SCP_string render_api_display(const GraphicsAPI& api) { switch (api) { - case GR_VULKAN: + case GraphicsAPI::Vulkan: return "Vulkan"; - case GR_OPENGL: + case GraphicsAPI::OpenGL: default: return "OpenGL"; } @@ -260,7 +260,7 @@ static SCP_string render_api_display(const int& api) // rather than bound to a global via a change listener -- like Resolution/Anisotropy, this can't take effect // without a restart, so there is no "live" value to keep in sync. // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton -static auto RenderAPIOption __UNUSED = options::OptionBuilder("Graphics.RenderAPI", +static auto RenderAPIOption __UNUSED = options::OptionBuilder("Graphics.RenderAPI", SCP_string("Render API"), SCP_string("Selects the rendering backend used by the engine. Requires a restart to take effect.")) .category(std::make_pair("Graphics", 1825)) @@ -273,16 +273,17 @@ static auto RenderAPIOption __UNUSED = options::OptionBuilder("Graphics.Ren .parser(parse_render_api_func) .finish(); -int gr_get_configured_render_api() +GraphicsAPI gr_get_configured_render_api() { return RenderAPIOption->getValue(); } #else -int gr_get_configured_render_api() +GraphicsAPI gr_get_configured_render_api() { - return GR_DEFAULT; + // No backend choice in an OpenGL-only build; Default resolves to OpenGL in gr_init(). + return GraphicsAPI::Default; } #endif @@ -1897,7 +1898,10 @@ bool gr_init(std::unique_ptr&& graphicsOps, GraphicsAPI height = res.height; removeResolutionVROption(); } - //TODO set d_mode from Ingame Options if available here + + // Pick the rendering backend from the in-game Graphics.RenderAPI option (or the mod's default + // settings table). A -vulkan/-opengl command line flag still wins below via Cmdline_graphics_api. + d_mode = gr_get_configured_render_api(); } else if ( !Is_standalone ) { // We cannot continue without this, quit, but try to help the user out first ptr = os_config_read_string(nullptr, NOX("VideocardFs2open"), nullptr); diff --git a/code/graphics/2d.h b/code/graphics/2d.h index 423c05e3d58..d89f231f43c 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -1054,9 +1054,9 @@ extern const char *Resolution_prefixes[GR_NUM_RESOLUTIONS]; extern bool gr_init(std::unique_ptr&& graphicsOps, GraphicsAPI d_mode = GraphicsAPI::Default, int d_width = GR_DEFAULT, int d_height = GR_DEFAULT, int d_depth = GR_DEFAULT); -// The render API (GR_OPENGL/GR_VULKAN) selected via the "Graphics.RenderAPI" in-game option (or a mod's +// The render API (GraphicsAPI::OpenGL/GraphicsAPI::Vulkan) selected via the "Graphics.RenderAPI" in-game option (or a mod's // default settings table). Does not account for the -vulkan command line override; see Cmdline_vulkan for that. -extern int gr_get_configured_render_api(); +extern GraphicsAPI gr_get_configured_render_api(); extern void gr_screen_resize(int width, int height); extern int gr_get_resolution_class(int width, int height); diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index 2445ba3bcb2..209006bd2d3 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -1911,10 +1911,6 @@ void game_init() sdlGraphicsOperations.reset(new SDLGraphicsOperations()); } - if (Using_in_game_options) { - graphics_api = gr_get_configured_render_api(); - } - if (!gr_init(std::move(sdlGraphicsOperations))) { os::dialogs::Message(os::dialogs::MESSAGEBOX_ERROR, "Error initializing graphics!"); exit(1); diff --git a/test/src/options/test_options_manager.cpp b/test/src/options/test_options_manager.cpp index 909ae66f6f1..0978c87f20a 100644 --- a/test/src/options/test_options_manager.cpp +++ b/test/src/options/test_options_manager.cpp @@ -126,22 +126,22 @@ TEST(OptionsManagerOverride, EnumIntOverrideRoundTrips) // Graphics.RenderAPI only exists in builds compiled with Vulkan support (code/graphics/2d.cpp gates the whole // option definition on #ifdef WITH_VULKAN) -- in an OpenGL-only build there's nothing to choose between, so -// gr_get_configured_render_api() just returns GR_DEFAULT and no option is registered at all. +// gr_get_configured_render_api() just returns GraphicsAPI::Default and no option is registered at all. #ifdef WITH_VULKAN TEST(OptionsManagerOverride, RenderAPIOverrideRoundTrips) { auto* opt = OptionsManager::instance()->getOptionByKey("Graphics.RenderAPI"); ASSERT_NE(opt, nullptr); - auto* typedOpt = static_cast*>(opt); + auto* typedOpt = static_cast*>(opt); // Exactly the payload shape built in cmdline.cpp's -vulkan handling. SCP_string override; - sprintf(override, "%d", GR_VULKAN); + sprintf(override, "%d", static_cast(GraphicsAPI::Vulkan)); OptionsManager::instance()->setOverride("Graphics.RenderAPI", override, "-vulkan"); - ASSERT_EQ(typedOpt->getValue(), GR_VULKAN); - ASSERT_EQ(gr_get_configured_render_api(), GR_VULKAN); + ASSERT_EQ(typedOpt->getValue(), GraphicsAPI::Vulkan); + ASSERT_EQ(gr_get_configured_render_api(), GraphicsAPI::Vulkan); } TEST(OptionsManagerOverride, RenderAPIMenuInitPathDoesNotThrow) @@ -155,10 +155,12 @@ TEST(OptionsManagerOverride, RenderAPIMenuInitPathDoesNotThrow) ASSERT_EQ(opt->getType(), OptionType::Selection); ASSERT_EQ(opt->getValidValues().size(), 2u); - OptionsManager::instance()->setOverride("Graphics.RenderAPI", std::to_string(GR_OPENGL), "-test"); + OptionsManager::instance()->setOverride("Graphics.RenderAPI", + std::to_string(static_cast(GraphicsAPI::OpenGL)), "-test"); ASSERT_EQ(opt->getCurrentValueDescription().display, "OpenGL"); - OptionsManager::instance()->setOverride("Graphics.RenderAPI", std::to_string(GR_VULKAN), "-vulkan"); + OptionsManager::instance()->setOverride("Graphics.RenderAPI", + std::to_string(static_cast(GraphicsAPI::Vulkan)), "-vulkan"); ASSERT_EQ(opt->getCurrentValueDescription().display, "Vulkan"); } @@ -167,7 +169,7 @@ TEST(OptionsManagerOverride, RenderAPIMenuInitPathDoesNotThrow) TEST(OptionsManagerOverride, RenderAPIIsAbsentWithoutVulkanSupport) { ASSERT_EQ(OptionsManager::instance()->getOptionByKey("Graphics.RenderAPI"), nullptr); - ASSERT_EQ(gr_get_configured_render_api(), GR_DEFAULT); + ASSERT_EQ(gr_get_configured_render_api(), GraphicsAPI::Default); } #endif