From 41790a8e2df3025bf00a52aaeb58d90d4adf0489 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 13 Nov 2026 09:01:16 -0600 Subject: [PATCH 1/6] Wire pan speed and consistent momentum falloff --- example_community_patch_settings_en.toml | 9 ++-- mods/src/config.cc | 18 +++++-- mods/src/config.h | 2 +- mods/src/defaultconfig.h | 2 +- mods/src/patches/parts/fix_pan.cc | 68 ++++++++++++++++++++---- mods/src/prime/NavigationPan.h | 16 +++++- 6 files changed, 93 insertions(+), 22 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index dfca5dd06..7df0f996c 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -186,10 +186,13 @@ zoom_label_player_threshold = 0.5 # This controls the zoom speed when using Q/E in System View, defaults to 350 keyboard_zoom_speed = 350.0 -# Controls the system pan momentum speed. Defaults to 0.4 -system_pan_momentum = 0.4 +# Controls mouse/touch and keyboard panning speed in System and Galaxy views. +# 1.0 = normal, 0.5 = half speed, 1.5 = 50% faster. Valid range: greater than 0, up to 10. +# Replaces system_pan_momentum, which was unused and did not control pan speed. +system_pan_speed = 1.0 -# Controls how quickly the momentum falls off when letting go of the mouse. Lower = quicker stop. Defaults to 0.8 +# Momentum retained each frame after releasing the mouse, touch or movement keys. +# Lower = quicker stop; 0 = immediate stop; 1 = no decay. Valid range: 0 to 1. Defaults to 0.8. system_pan_momentum_falloff = 0.8 # Subgroup: Loader diff --git a/mods/src/config.cc b/mods/src/config.cc index 51f535641..ecf073521 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1102,15 +1102,25 @@ void Config::Load() this->keyboard_zoom_speed = get_config_or_default(config, parsed, "graphics", "keyboard_zoom_speed", DCG::keyboard_zoom_speed, write_config); - if (this->enable_experimental) { - this->system_pan_momentum = get_config_or_default(config, parsed, "graphics", "system_pan_momentum", - DCG::system_pan_momentum, write_config); + this->system_pan_speed = + get_config_or_default(config, parsed, "graphics", "system_pan_speed", DCG::system_pan_speed, write_config); + if (!std::isfinite(this->system_pan_speed) || this->system_pan_speed <= 0.0f || this->system_pan_speed > 10.0f) { + spdlog::warn("Invalid system_pan_speed {}; using {}", this->system_pan_speed, DCG::system_pan_speed); + this->system_pan_speed = DCG::system_pan_speed; + parsed["graphics"].as_table()->insert_or_assign("system_pan_speed", this->system_pan_speed); } spdlog::debug(""); this->system_pan_momentum_falloff = get_config_or_default(config, parsed, "graphics", "system_pan_momentum_falloff", - DCG::system_pan_momentum_falloff, write_log); + DCG::system_pan_momentum_falloff, write_config); + if (!std::isfinite(this->system_pan_momentum_falloff) || this->system_pan_momentum_falloff < 0.0f + || this->system_pan_momentum_falloff > 1.0f) { + spdlog::warn("Invalid system_pan_momentum_falloff {}; using {}", this->system_pan_momentum_falloff, + DCG::system_pan_momentum_falloff); + this->system_pan_momentum_falloff = DCG::system_pan_momentum_falloff; + parsed["graphics"].as_table()->insert_or_assign("system_pan_momentum_falloff", this->system_pan_momentum_falloff); + } this->disable_target_camera_lock = get_config_or_default(config, parsed, "graphics", "disable_target_camera_lock", DCG::disable_target_camera_lock, write_log); this->borderless_fullscreen = diff --git a/mods/src/config.h b/mods/src/config.h index 91ede6a42..400765ff5 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -192,7 +192,7 @@ class Config final bool adjust_scale_res; bool use_out_of_dock_power; - float system_pan_momentum; + float system_pan_speed; float system_pan_momentum_falloff; bool disable_target_camera_lock; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 1d98dab81..64b43fb0f 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -50,7 +50,7 @@ namespace Graphics constexpr auto keyboard_zoom_speed = 350; constexpr auto system_pan_momentum_falloff = 0.8; constexpr bool disable_target_camera_lock = false; - constexpr auto system_pan_momentum = 0.4; + constexpr auto system_pan_speed = 1.0f; constexpr auto system_zoom_preset_1 = 50; constexpr auto system_zoom_preset_2 = 500; constexpr auto system_zoom_preset_3 = 1250; diff --git a/mods/src/patches/parts/fix_pan.cc b/mods/src/patches/parts/fix_pan.cc index 569e0c8e6..fe738155d 100644 --- a/mods/src/patches/parts/fix_pan.cc +++ b/mods/src/patches/parts/fix_pan.cc @@ -14,6 +14,44 @@ #include #include +namespace +{ +// Native LateUpdate uses _deltaScaler for both keyboard movement and coasting. +// Apply our falloff to the cached delta once, and preserve native keyboard speed +// while preventing a second native fade. Restore both fields after the call. +struct NativePanScalers { + NavigationPan* pan; + float delta; + float keyboard; + + NativePanScalers(NavigationPan* pan, float speed) + : pan(pan) + , delta(pan->_deltaScaler) + , keyboard(pan->_keyboardDeltaScaler) + { + pan->_deltaScaler = 1.0f; + pan->_keyboardDeltaScaler = keyboard * delta * speed; + } + + ~NativePanScalers() + { + pan->_keyboardDeltaScaler = keyboard; + pan->_deltaScaler = delta; + } +}; +} // namespace + +void NavigationPan_MoveCamera_Hook(auto original, NavigationPan* _this, vec2 delta, bool is_momentum) +{ + // Only fresh recognizer input is scaled. MoveCamera caches the effective + // movement, so scaling coasting again would compound speed every frame. + if (!is_momentum && _this->_panningStarted) { + delta.x *= Config::Get().system_pan_speed; + delta.y *= Config::Get().system_pan_speed; + } + original(_this, delta, is_momentum); +} + TKTouch *TKTouch_populateWithPosition_Hook(auto original, TKTouch *_this, uintptr_t pos, TouchPhase phase) { auto r = original(_this, pos, phase); @@ -24,7 +62,7 @@ TKTouch *TKTouch_populateWithPosition_Hook(auto original, TKTouch *_this, uintpt } // Retain the native tutorial and modal/text-input gates while replacing the -// keyboard source through the existing LateUpdate hook, without new detours. +// keyboard source through the existing LateUpdate hook. bool KeyboardPanAllowed() { static auto hub = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Core", "Hub"); @@ -58,10 +96,14 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) if (!config.disable_move_keys && config.hotkeys_enabled && !Key::IsDirectionalInputClaimed()) { if (config.use_scopely_hotkeys || !config.installHotkeyHooks) { - original(_this); native_handled = true; - } else if (!_this->BlockPan() && !_this->_trackingPOI && !dragging - && KeyboardPanAllowed()) { + if (!dragging) { + d->x *= config.system_pan_momentum_falloff; + d->y *= config.system_pan_momentum_falloff; + NativePanScalers scalers(_this, config.system_pan_speed); + original(_this); + } + } else if (!_this->BlockPan() && !_this->_trackingPOI && !dragging && KeyboardPanAllowed()) { // Opposite bindings cancel; normalize diagonals like the native 2D composite. float x = float(MapKey::IsPressed(GameFunction::MoveRight)) - float(MapKey::IsPressed(GameFunction::MoveLeft)); float y = float(MapKey::IsPressed(GameFunction::MoveUp)) - float(MapKey::IsPressed(GameFunction::MoveDown)); @@ -69,7 +111,8 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) if (length > 0.0f) { d->x = -x / length * _this->_keyboardDeltaScaler; d->y = -y / length * _this->_keyboardDeltaScaler; - _this->MoveCamera(vec2{d->x * _this->_deltaScaler, d->y * _this->_deltaScaler}, true); + const float speed = _this->_deltaScaler * config.system_pan_speed; + _this->MoveCamera(vec2{d->x * speed, d->y * speed}, true); by_keys = true; } } @@ -78,12 +121,10 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) if (_this->BlockPan() || (_this->_trackingPOI && !disable_target_camera_lock)) { d->x = 0.0f; d->y = 0.0f; - } else if (!dragging && !by_keys) { - d->x = d->x * Config::Get().system_pan_momentum_falloff; - d->y = d->y * Config::Get().system_pan_momentum_falloff; - if (!native_handled) { - _this->MoveCamera(vec2{d->x, d->y}, true); - } + } else if (!dragging && !by_keys && !native_handled) { + d->x *= config.system_pan_momentum_falloff; + d->y *= config.system_pan_momentum_falloff; + _this->MoveCamera(vec2{d->x, d->y}, true); } _this->_farMagRadiusRatioSystemExtended = _this->_farMagRadiusRatioSystemNormal; return true; @@ -135,6 +176,11 @@ void InstallPanHooks() !navHelper.isValidHelper()) { ErrorMsg::MissingHelper("Navigation", "NavigationPan"); } else { + if (const auto ptr = navHelper.GetMethod("MoveCamera"); ptr == nullptr) { + ErrorMsg::MissingMethod("NavigationPan", "MoveCamera"); + } else { + SPUD_STATIC_DETOUR(ptr, NavigationPan_MoveCamera_Hook); + } if (const auto ptr = navHelper.GetMethod("LateUpdate"); ptr == nullptr) { ErrorMsg::MissingMethod("NavigationPan", "LateUpdate"); } else { diff --git a/mods/src/prime/NavigationPan.h b/mods/src/prime/NavigationPan.h index eeff4082a..6dbed565f 100644 --- a/mods/src/prime/NavigationPan.h +++ b/mods/src/prime/NavigationPan.h @@ -16,8 +16,8 @@ struct NavigationPan { // expect a boxed value and would read/write the adjacent pixel drag delta. __declspec(property(get = __get__lastDelta)) vec2* _lastDelta; __declspec(property(get = __get__panningStarted)) bool _panningStarted; - __declspec(property(get = __get__deltaScaler)) float _deltaScaler; - __declspec(property(get = __get__keyboardDeltaScaler)) float _keyboardDeltaScaler; + __declspec(property(get = __get__deltaScaler, put = __set__deltaScaler)) float _deltaScaler; + __declspec(property(get = __get__keyboardDeltaScaler, put = __set__keyboardDeltaScaler)) float _keyboardDeltaScaler; __declspec(property(get = __get__trackingPOI, put = __set__trackingPOI)) uintptr_t* _trackingPOI; __declspec(property(get = __get__farMagRadiusRatioSystemNormal, @@ -75,12 +75,24 @@ struct NavigationPan { return *(float*)((ptrdiff_t)this + field.offset()); } + void __set__keyboardDeltaScaler(float value) + { + static auto field = get_class_helper().GetField("_keyboardDeltaScaler"); + *(float*)((ptrdiff_t)this + field.offset()) = value; + } + float __get__deltaScaler() { static auto field = get_class_helper().GetField("_deltaScaler"); return *(float*)((ptrdiff_t)this + field.offset()); } + void __set__deltaScaler(float value) + { + static auto field = get_class_helper().GetField("_deltaScaler"); + *(float*)((ptrdiff_t)this + field.offset()) = value; + } + float __get__farMagRadiusRatioSystemNormal() { static auto field = get_class_helper().GetField("_farMagRadiusRatioSystemNormal").offset(); From 0e83a9a643216a8ca1a7a7036750106bddf4eae4 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 13 Nov 2026 09:09:02 -0600 Subject: [PATCH 2/6] Remove duplicate keys from the English example config --- example_community_patch_settings_en.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 7df0f996c..d28d1eb4b 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -792,14 +792,6 @@ default_chat_width = 0 # Change to true to remove Galaxy Chat from the game disable_galaxy_chat = false -# Initial side-chat width in canvas units. 0 keeps the game default. -# The game clamps positive values to the available screen space and minimum width. -default_chat_width = 0.0 - -# Open alliance side chat once per login, after station and chat initialization. -# Waits for section completion and three continuous seconds of readiness. -auto_open_chat = false - # Change to true to remove Veil Chat from the game disable_veil_chat = false From 8a5294cc332c381fa9741d12f1b7c1df580e06c2 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 13 Nov 2026 18:20:50 -0600 Subject: [PATCH 3/6] Keep pan momentum falloff below one --- example_community_patch_settings_en.toml | 2 +- mods/src/config.cc | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index d28d1eb4b..e07fca6ba 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -192,7 +192,7 @@ keyboard_zoom_speed = 350.0 system_pan_speed = 1.0 # Momentum retained each frame after releasing the mouse, touch or movement keys. -# Lower = quicker stop; 0 = immediate stop; 1 = no decay. Valid range: 0 to 1. Defaults to 0.8. +# Lower = quicker stop; 0 = immediate stop; 0.99 = long glide. Clamped to 0 to 0.99. Defaults to 0.8. system_pan_momentum_falloff = 0.8 # Subgroup: Loader diff --git a/mods/src/config.cc b/mods/src/config.cc index ecf073521..87cc8af19 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1114,11 +1114,13 @@ void Config::Load() this->system_pan_momentum_falloff = get_config_or_default(config, parsed, "graphics", "system_pan_momentum_falloff", DCG::system_pan_momentum_falloff, write_config); - if (!std::isfinite(this->system_pan_momentum_falloff) || this->system_pan_momentum_falloff < 0.0f - || this->system_pan_momentum_falloff > 1.0f) { + const auto bounded_falloff = std::isfinite(this->system_pan_momentum_falloff) + ? std::clamp(this->system_pan_momentum_falloff, 0.0f, 0.99f) + : static_cast(DCG::system_pan_momentum_falloff); + if (this->system_pan_momentum_falloff != bounded_falloff) { spdlog::warn("Invalid system_pan_momentum_falloff {}; using {}", this->system_pan_momentum_falloff, - DCG::system_pan_momentum_falloff); - this->system_pan_momentum_falloff = DCG::system_pan_momentum_falloff; + bounded_falloff); + this->system_pan_momentum_falloff = bounded_falloff; parsed["graphics"].as_table()->insert_or_assign("system_pan_momentum_falloff", this->system_pan_momentum_falloff); } this->disable_target_camera_lock = get_config_or_default(config, parsed, "graphics", "disable_target_camera_lock", From e9a76306eba8e14b67e40b9837df6a785aff1a6d Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 13 Nov 2026 21:33:27 -0600 Subject: [PATCH 4/6] Separate System and Galaxy pan speed controls --- .../example_community_patch_settings_en.toml | 5 +++- src/mod/config.cc | 2 ++ src/mod/config.h | 1 + src/mod/features/camera.cc | 26 ++++++++++++++---- src/mod/game/navigation.h | 5 ++++ tests/mod_logic.cc | 27 ++++++++++++++----- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/docs/configuration/example_community_patch_settings_en.toml b/docs/configuration/example_community_patch_settings_en.toml index b4935d287..8db6933eb 100644 --- a/docs/configuration/example_community_patch_settings_en.toml +++ b/docs/configuration/example_community_patch_settings_en.toml @@ -186,11 +186,14 @@ zoom_label_player_threshold = 0.5 # This controls the zoom speed when using Q/E in System View, defaults to 350 keyboard_zoom_speed = 350.0 -# Controls mouse/touch and keyboard panning speed in System and Galaxy views. +# Controls mouse/touch and keyboard panning speed in System view only. # 1.0 = normal, 0.5 = half speed, 1.5 = 50% faster. Valid range: greater than 0, up to 10. # Replaces system_pan_momentum, which was unused. Does not require experimental mode. system_pan_speed = 1.0 +# Independent mouse/touch and keyboard panning speed in Galaxy view, with the same range. +galaxy_pan_speed = 1.0 + # Controls the system pan momentum speed. Defaults to 0.4. Read with enable_experimental, but not applied yet system_pan_momentum = 0.4 diff --git a/src/mod/config.cc b/src/mod/config.cc index bf9cb4909..37e6b3630 100644 --- a/src/mod/config.cc +++ b/src/mod/config.cc @@ -612,6 +612,8 @@ void Config::Read(const toml::table& file, toml::table& effective, bool log_valu r.Read("graphics", "keyboard_zoom_speed", g.keyboard_zoom_speed); r.ReadValid("graphics", "system_pan_speed", g.system_pan_speed, [](float v) { return std::isfinite(v) && v > 0.0f && v <= 10.0f; }); + r.ReadValid("graphics", "galaxy_pan_speed", g.galaxy_pan_speed, + [](float v) { return std::isfinite(v) && v > 0.0f && v <= 10.0f; }); if (control.enable_experimental) r.Read("graphics", "system_pan_momentum", g.system_pan_momentum); r.ReadValid("graphics", "system_pan_momentum_falloff", g.system_pan_momentum_falloff, diff --git a/src/mod/config.h b/src/mod/config.h index 0937b8310..7c8053d72 100644 --- a/src/mod/config.h +++ b/src/mod/config.h @@ -216,6 +216,7 @@ class Config final bool allow_cursor = true; float keyboard_zoom_speed = 350.0f; float system_pan_speed = 1.0f; + float galaxy_pan_speed = 1.0f; // Retained compatibility key; use system_pan_speed for panning speed. float system_pan_momentum = 0.4f; // How much of the pan speed is kept each frame after a drag or key is released. diff --git a/src/mod/features/camera.cc b/src/mod/features/camera.cc index 8eced84e9..3349c7f15 100644 --- a/src/mod/features/camera.cc +++ b/src/mod/features/camera.cc @@ -278,10 +278,24 @@ struct NativePanScalers { } }; +float CameraPanSpeed(NodeDepth depth, float system_speed, float galaxy_speed) +{ + switch (depth) { + case NodeDepth::SolarSystem: + return system_speed; + case NodeDepth::Galaxy: + return galaxy_speed; + default: + return 1.0f; + } +} + void NavigationPan_MoveCamera_Hook(auto original, NavigationPan* _this, unity::Vector2 delta, bool momentum) { - if (!momentum && _this->panningStarted()) - delta = delta * Config::Get().graphics.system_pan_speed; + if (!momentum && _this->panningStarted()) { + const auto& graphics = Config::Get().graphics; + delta = delta * CameraPanSpeed(_this->nodeDepth(), graphics.system_pan_speed, graphics.galaxy_pan_speed); + } original(_this, delta, momentum); } @@ -300,7 +314,9 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) // until the arrow is released. A modified move binding such as CTRL-LEFT still pans. const bool keyboard = config.control.hotkeys_enabled && !config.ui.disable_move_keys; - const bool native = keyboard && (config.control.use_scopely_hotkeys || !config.patches.hotkeyhooks); + const bool native = keyboard && (config.control.use_scopely_hotkeys || !config.patches.hotkeyhooks); + const float pan_speed = + CameraPanSpeed(_this->nodeDepth(), config.graphics.system_pan_speed, config.graphics.galaxy_pan_speed); auto d = _this->lastDelta(); const bool blocked = _this->BlockPan() || (_this->trackingPOI() && !disable_target_camera_lock); @@ -310,7 +326,7 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) if (native) { if (!pointer) { _this->SetLastDelta(blocked ? unity::Vector2{} : d * config.graphics.system_pan_momentum_falloff); - NativePanScalers scalers(_this, config.graphics.system_pan_speed); + NativePanScalers scalers(_this, pan_speed); original(_this); } _this->SetFarMagRadiusRatioSystemExtended(_this->farMagRadiusRatioSystemNormal()); @@ -328,7 +344,7 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan* _this) d = {-x / length * speed, -y / length * speed}; by_keys = true; const float scale = _this->deltaScaler(); - _this->MoveCamera(d * (scale * config.graphics.system_pan_speed), true); + _this->MoveCamera(d * (scale * pan_speed), true); d = _this->lastDelta(); } } diff --git a/src/mod/game/navigation.h b/src/mod/game/navigation.h index 52ffa0596..d7b7337ff 100644 --- a/src/mod/game/navigation.h +++ b/src/mod/game/navigation.h @@ -27,9 +27,14 @@ struct NavigationLOD : il2cpp::Object { { Call<"UpdateLOD">(level); } }; +enum class NodeDepth : int32_t; + struct NavigationPan : il2cpp::Object { using Class = il2cpp::Class<"Assembly-CSharp", "Digit.Prime.Navigation", "NavigationPan">; + NodeDepth nodeDepth() + { return Field<"_nodeDepth", NodeDepth>(); } + unity::Vector2 lastDelta() { return Field<"_lastDelta", unity::Vector2>(); } void SetLastDelta(unity::Vector2 value) diff --git a/tests/mod_logic.cc b/tests/mod_logic.cc index a2db779c4..b07a8da29 100644 --- a/tests/mod_logic.cc +++ b/tests/mod_logic.cc @@ -446,6 +446,16 @@ void RenamedSettings() void PanControls() { + for (const auto* text : {"", "[graphics]\nsystem_pan_speed=0.5", "[graphics]\ngalaxy_pan_speed=2.5", + "[graphics]\nsystem_pan_speed=0.5\ngalaxy_pan_speed=2.5"}) { + toml::table effective; + const auto input = toml::parse(text); + const auto parsed = Config::Parse(input, effective, false); + Check(parsed->graphics.system_pan_speed == (input["graphics"]["system_pan_speed"] ? 0.5f : 1.0f), + "System pan speed keeps its own default and saved value"); + Check(parsed->graphics.galaxy_pan_speed == (input["graphics"]["galaxy_pan_speed"] ? 2.5f : 1.0f), + "Galaxy pan speed keeps its own default and saved value"); + } struct Case { const char* text; float expected; @@ -461,13 +471,16 @@ void PanControls() Check(effective["graphics"]["system_pan_momentum_falloff"].value() == double(item.expected), "runtime vars record the bounded pan glide actually used"); } - for (const auto& item : std::array{Case{"0.5", 0.5f}, Case{"1", 1}, Case{"10", 10}, Case{"0", 1}, Case{"-1", 1}, - Case{"10.1", 1}, Case{"nan", 1}, Case{"inf", 1}, Case{"\"bad\"", 1}}) { - toml::table effective; - const auto parsed = - Config::Parse(toml::parse(std::string("[graphics]\nsystem_pan_speed = ") + item.text), effective, false); - Check(parsed->graphics.system_pan_speed == item.expected, "pan speed accepts only finite values in (0, 10]"); - } + for (const auto* key : {"system_pan_speed", "galaxy_pan_speed"}) + for (const auto& item : std::array{Case{"0.5", 0.5f}, Case{"1", 1}, Case{"10", 10}, Case{"0", 1}, Case{"-1", 1}, + Case{"10.1", 1}, Case{"nan", 1}, Case{"inf", 1}, Case{"\"bad\"", 1}}) { + toml::table effective; + const auto parsed = + Config::Parse(toml::parse(std::string("[graphics]\n") + key + " = " + item.text), effective, false); + const float value = std::string_view(key) == "system_pan_speed" ? parsed->graphics.system_pan_speed + : parsed->graphics.galaxy_pan_speed; + Check(value == item.expected, "pan speed accepts only finite values in (0, 10]"); + } } void ParseDoesNotActivateSettings() From 92027b79fa963252b2c0c613727b259eae226d9d Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Fri, 13 Nov 2026 22:50:14 -0600 Subject: [PATCH 5/6] Require useful positive pan speeds --- .../configuration/example_community_patch_settings_en.toml | 3 ++- src/mod/config.cc | 4 ++-- tests/mod_logic.cc | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/configuration/example_community_patch_settings_en.toml b/docs/configuration/example_community_patch_settings_en.toml index 8db6933eb..25cc04f5b 100644 --- a/docs/configuration/example_community_patch_settings_en.toml +++ b/docs/configuration/example_community_patch_settings_en.toml @@ -187,7 +187,8 @@ zoom_label_player_threshold = 0.5 keyboard_zoom_speed = 350.0 # Controls mouse/touch and keyboard panning speed in System view only. -# 1.0 = normal, 0.5 = half speed, 1.5 = 50% faster. Valid range: greater than 0, up to 10. +# 1.0 = normal, 0.5 = half speed, 1.5 = 50% faster. Valid range: 0.1 to 10. +# Invalid values, including zero and values below 0.1, use normal speed (1.0). # Replaces system_pan_momentum, which was unused. Does not require experimental mode. system_pan_speed = 1.0 diff --git a/src/mod/config.cc b/src/mod/config.cc index 37e6b3630..2b55db6d8 100644 --- a/src/mod/config.cc +++ b/src/mod/config.cc @@ -611,9 +611,9 @@ void Config::Read(const toml::table& file, toml::table& effective, bool log_valu r.Read("graphics", "allow_cursor", g.allow_cursor); r.Read("graphics", "keyboard_zoom_speed", g.keyboard_zoom_speed); r.ReadValid("graphics", "system_pan_speed", g.system_pan_speed, - [](float v) { return std::isfinite(v) && v > 0.0f && v <= 10.0f; }); + [](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; }); r.ReadValid("graphics", "galaxy_pan_speed", g.galaxy_pan_speed, - [](float v) { return std::isfinite(v) && v > 0.0f && v <= 10.0f; }); + [](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; }); if (control.enable_experimental) r.Read("graphics", "system_pan_momentum", g.system_pan_momentum); r.ReadValid("graphics", "system_pan_momentum_falloff", g.system_pan_momentum_falloff, diff --git a/tests/mod_logic.cc b/tests/mod_logic.cc index b07a8da29..25b62fa8e 100644 --- a/tests/mod_logic.cc +++ b/tests/mod_logic.cc @@ -472,14 +472,15 @@ void PanControls() "runtime vars record the bounded pan glide actually used"); } for (const auto* key : {"system_pan_speed", "galaxy_pan_speed"}) - for (const auto& item : std::array{Case{"0.5", 0.5f}, Case{"1", 1}, Case{"10", 10}, Case{"0", 1}, Case{"-1", 1}, - Case{"10.1", 1}, Case{"nan", 1}, Case{"inf", 1}, Case{"\"bad\"", 1}}) { + for (const auto& item : std::array{Case{"0.1", 0.1f}, Case{"0.5", 0.5f}, Case{"1", 1}, Case{"10", 10}, Case{"0", 1}, + Case{"0.05", 1}, Case{"1e-40", 1}, Case{"-1", 1}, Case{"10.1", 1}, + Case{"nan", 1}, Case{"inf", 1}, Case{"\"bad\"", 1}}) { toml::table effective; const auto parsed = Config::Parse(toml::parse(std::string("[graphics]\n") + key + " = " + item.text), effective, false); const float value = std::string_view(key) == "system_pan_speed" ? parsed->graphics.system_pan_speed : parsed->graphics.galaxy_pan_speed; - Check(value == item.expected, "pan speed accepts only finite values in (0, 10]"); + Check(value == item.expected, "pan speed accepts only finite values in [0.1, 10]"); } } From a170d12b874d6dc64e46206b18be2029f8b835b8 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sat, 14 Nov 2026 19:23:57 -0600 Subject: [PATCH 6/6] Read legacy pan momentum without experimental mode --- .../example_community_patch_settings_en.toml | 2 +- src/mod/config.cc | 3 +-- tests/mod_logic.cc | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/configuration/example_community_patch_settings_en.toml b/docs/configuration/example_community_patch_settings_en.toml index 25cc04f5b..c14fa1962 100644 --- a/docs/configuration/example_community_patch_settings_en.toml +++ b/docs/configuration/example_community_patch_settings_en.toml @@ -195,7 +195,7 @@ system_pan_speed = 1.0 # Independent mouse/touch and keyboard panning speed in Galaxy view, with the same range. galaxy_pan_speed = 1.0 -# Controls the system pan momentum speed. Defaults to 0.4. Read with enable_experimental, but not applied yet +# Legacy compatibility setting; read without experimental mode, but does not affect panning. Defaults to 0.4. system_pan_momentum = 0.4 # Momentum retained each update after releasing the mouse, touch or movement keys. diff --git a/src/mod/config.cc b/src/mod/config.cc index 2b55db6d8..d005140c0 100644 --- a/src/mod/config.cc +++ b/src/mod/config.cc @@ -614,8 +614,7 @@ void Config::Read(const toml::table& file, toml::table& effective, bool log_valu [](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; }); r.ReadValid("graphics", "galaxy_pan_speed", g.galaxy_pan_speed, [](float v) { return std::isfinite(v) && v >= 0.1f && v <= 10.0f; }); - if (control.enable_experimental) - r.Read("graphics", "system_pan_momentum", g.system_pan_momentum); + r.Read("graphics", "system_pan_momentum", g.system_pan_momentum); r.ReadValid("graphics", "system_pan_momentum_falloff", g.system_pan_momentum_falloff, [](float v) { return std::isfinite(v); }); const auto bounded_falloff = std::clamp(g.system_pan_momentum_falloff, 0.0f, 0.99f); diff --git a/tests/mod_logic.cc b/tests/mod_logic.cc index 25b62fa8e..94ead4acc 100644 --- a/tests/mod_logic.cc +++ b/tests/mod_logic.cc @@ -446,6 +446,20 @@ void RenamedSettings() void PanControls() { + { + toml::table effective; + const auto parsed = Config::Parse(toml::parse(R"( + [control] + enable_experimental = false + [graphics] + system_pan_momentum = 0.25 + )"), + effective, false); + Check(!parsed->control.enable_experimental && parsed->graphics.system_pan_momentum == 0.25f, + "legacy pan momentum remains readable without experimental mode"); + Check(effective["graphics"]["system_pan_momentum"].value() == 0.25, + "runtime vars retain the legacy pan momentum value"); + } for (const auto* text : {"", "[graphics]\nsystem_pan_speed=0.5", "[graphics]\ngalaxy_pan_speed=2.5", "[graphics]\nsystem_pan_speed=0.5\ngalaxy_pan_speed=2.5"}) { toml::table effective;