From a54522b5f61bc06c0ca6b6b7ecd11b2cbc29e7a1 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 15 Oct 2026 01:05:44 -0500 Subject: [PATCH 1/5] Prevent primary action from using dismissed targets --- mods/src/patches/parts/hotkeys.cc | 37 +++++++++++-------- .../NavigationInteractionUIViewController.h | 28 ++++++++++++++ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index fb3129d63..277d1343a 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -1042,8 +1042,11 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) if (!Key::IsInputFocused()) { // Lets try to remove the pre-scan because we hit escape and it's visible - if (Key::Pressed(KeyCode::Escape) && DidHideViewers()) { - return; + if (Key::Pressed(KeyCode::Escape)) { + force_space_action_next_frame = false; + if (DidHideViewers()) { + return; + } } // Dismiss the golden rewards screen when escape or space is pressed. @@ -1284,6 +1287,14 @@ bool DidExecuteRepair(FleetBarViewController* fleet_bar) FleetState::Repairing); } +template bool IsObjectViewerShowing(T* widget) +{ + auto* visibility = widget ? widget->_visibilityController : nullptr; + auto* parent = widget ? widget->Parent : nullptr; + return visibility && (visibility->_state == VisibilityState::Visible || visibility->_state == VisibilityState::Show) + && parent && parent->IsShowing; +} + void ExecuteSpaceAction(FleetBarViewController* fleet_bar) { if (!fleet_bar) { @@ -1318,15 +1329,10 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } else { auto all_pre_scan_widgets = ObjectFinder::GetAll(); for (auto pre_scan_widget : all_pre_scan_widgets) { - auto visibility_controller = pre_scan_widget ? pre_scan_widget->_visibilityController : nullptr; - if (visibility_controller - && (visibility_controller->_state == VisibilityState::Visible - || visibility_controller->_state == VisibilityState::Show)) { + if (IsObjectViewerShowing(pre_scan_widget)) { if (auto mine_object_viewer_widget = ObjectFinder::Get(); - mine_object_viewer_widget && mine_object_viewer_widget->_visibilityController - && (mine_object_viewer_widget->_visibilityController->_state == VisibilityState::Visible - || mine_object_viewer_widget->_visibilityController->_state == VisibilityState::Show)) { + IsObjectViewerShowing(mine_object_viewer_widget)) { if (has_secondary && pre_scan_widget->_scanEngageButtonsWidget) { return pre_scan_widget->_scanEngageButtonsWidget->OnScanButtonClicked(); } else if (has_primary) { @@ -1418,9 +1424,7 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } if (auto mine_object_viewer_widget = ObjectFinder::Get(); - mine_object_viewer_widget && mine_object_viewer_widget->_visibilityController - && (mine_object_viewer_widget->_visibilityController->_state == VisibilityState::Visible - || mine_object_viewer_widget->_visibilityController->_state == VisibilityState::Show)) { + IsObjectViewerShowing(mine_object_viewer_widget)) { if (has_secondary && mine_object_viewer_widget->_scanEngageButtonsWidget) { if (mine_object_viewer_widget->_scanEngageButtonsWidget->Context) { return mine_object_viewer_widget->_scanEngageButtonsWidget->OnScanButtonClicked(); @@ -1429,7 +1433,7 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) return mine_object_viewer_widget->MineClicked(); } } else if (auto star_node_object_viewer_widget = ObjectFinder::Get(); - star_node_object_viewer_widget && star_node_object_viewer_widget->Context) { + IsObjectViewerShowing(star_node_object_viewer_widget) && star_node_object_viewer_widget->Context) { if (has_secondary) { star_node_object_viewer_widget->OnViewButtonActivation(); return; @@ -1452,14 +1456,17 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } spdlog::info("have armada? {}, State {}", (armada_widget ? "Yes" : "No"), (int)armada_state); - if (armada_widget && (armada_state == VisibilityState::Visible || armada_state == VisibilityState::Show)) { + if (IsObjectViewerShowing(armada_widget)) { auto button = armada_widget->__get__joinContext(); if (button && button->Interactable) { armada_widget->ValidateThenJoinArmada(); return; } } else { - navigation_ui_controller->OnSetCourseButtonClick(); + // Dismissal keeps the native context's last position/path. Require an active movement selection. + if (navigation_ui_controller->IsSetCourseVisible()) { + navigation_ui_controller->OnSetCourseButtonClick(); + } return; } } diff --git a/mods/src/prime/NavigationInteractionUIViewController.h b/mods/src/prime/NavigationInteractionUIViewController.h index 405170ed7..2551295df 100644 --- a/mods/src/prime/NavigationInteractionUIViewController.h +++ b/mods/src/prime/NavigationInteractionUIViewController.h @@ -1,9 +1,37 @@ #pragma once #include +#include "VisibilityController.h" struct NavigationInteractionUIViewController { public: + bool IsSetCourseVisible() + { + static auto widget_class = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Navigation", "SetCourseWidget"); + if (!get_class_helper().get_cls() || !widget_class.get_cls()) { + return false; + } + static auto widget_field = get_class_helper().GetField("_setCourseWidget"); + static auto visibility_field = widget_class.GetField("_visibilityController"); + static auto active_property = widget_class.GetProperty("isActiveAndEnabled"); + if (!widget_field.get_info() || !visibility_field.get_info()) { + return false; + } + + auto* widget = *reinterpret_cast(reinterpret_cast(this) + widget_field.offset()); + if (!widget) { + return false; + } + auto* visibility = + *reinterpret_cast(reinterpret_cast(widget) + visibility_field.offset()); + if (!visibility + || (visibility->_state != VisibilityState::Visible && visibility->_state != VisibilityState::Show)) { + return false; + } + const auto* active = active_property.Get(widget); + return active && *active; + } + void OnSetCourseButtonClick() { static auto OnSetCourseButtonClick = From d69d864e53147d49a3ccdf206013f556d03afcda Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 15 Oct 2026 01:06:52 -0500 Subject: [PATCH 2/5] Use native star viewer base for dismissal visibility checks --- mods/src/prime/StarNodeObjectViewerWidget.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mods/src/prime/StarNodeObjectViewerWidget.h b/mods/src/prime/StarNodeObjectViewerWidget.h index 889385437..43f2ea507 100644 --- a/mods/src/prime/StarNodeObjectViewerWidget.h +++ b/mods/src/prime/StarNodeObjectViewerWidget.h @@ -2,9 +2,9 @@ #include -#include "Widget.h" +#include "ObjectViewerBaseWidget.h" -struct StarNodeObjectViewerWidget : public Widget { +struct StarNodeObjectViewerWidget : public ObjectViewerBaseWidget { public: void OnViewButtonActivation() { @@ -27,5 +27,5 @@ struct StarNodeObjectViewerWidget : public Widget; - friend struct Widget; + friend class ObjectViewerBaseWidget; }; From 08544178fd45bc06eb7200981c05e551508798a3 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 15 Oct 2026 01:08:56 -0500 Subject: [PATCH 3/5] Unbox parent visibility Boolean before gating target actions --- mods/src/prime/ParentObjectViewerViewController.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mods/src/prime/ParentObjectViewerViewController.h b/mods/src/prime/ParentObjectViewerViewController.h index 11ae6a130..030985b0a 100644 --- a/mods/src/prime/ParentObjectViewerViewController.h +++ b/mods/src/prime/ParentObjectViewerViewController.h @@ -54,6 +54,7 @@ class ParentObjectViewerViewController : public ViewController(this); + const auto* showing = field.Get(this); + return showing && *showing; } -}; \ No newline at end of file +}; From 8a891710f25c39940e79489913eb9b8242d9f984 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 15 Oct 2026 19:27:16 -0500 Subject: [PATCH 4/5] Make primary-action target visibility protection opt-in --- example_community_patch_settings_en.toml | 4 ++ mods/src/config.cc | 2 + mods/src/config.h | 1 + mods/src/defaultconfig.h | 13 ++--- mods/src/patches/parts/hotkeys.cc | 61 +++++++++++------------- 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 05588e2af..cca0d5496 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -100,6 +100,10 @@ hotkeys_enabled = true # If you prefer to disable the extended hotkeys set this to false hotkeys_extended = true +# Require a visible target popup or Set Course control for the primary-action shortcut. +# false preserves sending commands to the last target after its popup is dismissed. +primary_requires_visible_target = false + # If you have the Kir'Shara artifact, should the queue be enabled by default? queue_enabled = true diff --git a/mods/src/config.cc b/mods/src/config.cc index 8cb005c91..1465ab119 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1016,6 +1016,8 @@ void Config::Load() get_config_or_default(config, parsed, "control", "hotkeys_enabled", DCC::hotkeys_enabled, write_config); this->hotkeys_extended = get_config_or_default(config, parsed, "control", "hotkeys_extended", DCC::hotkeys_extended, write_config); + this->primary_requires_visible_target = get_config_or_default( + config, parsed, "control", "primary_requires_visible_target", DCC::primary_requires_visible_target, write_config); this->use_scopely_hotkeys = get_config_or_default(config, parsed, "control", "use_scopely_hotkeys", DCC::use_scopely_hotkeys, write_config); this->select_timer = diff --git a/mods/src/config.h b/mods/src/config.h index 039d891e9..84ad4e34d 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -191,6 +191,7 @@ class Config final bool queue_enabled; bool hotkeys_enabled; bool hotkeys_extended; + bool primary_requires_visible_target; bool use_scopely_hotkeys; bool use_presets_as_default; bool enable_experimental; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 0ce3a01f2..5dbdc9da6 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -26,12 +26,13 @@ namespace SystemConfig namespace Control { - constexpr bool enable_experimental = false; - constexpr bool hotkeys_enabled = true; - constexpr bool hotkeys_extended = true; - constexpr bool use_scopely_hotkeys = false; - constexpr bool queue_enabled = true; - constexpr auto select_timer = 500; + constexpr bool enable_experimental = false; + constexpr bool hotkeys_enabled = true; + constexpr bool hotkeys_extended = true; + constexpr bool primary_requires_visible_target = false; + constexpr bool use_scopely_hotkeys = false; + constexpr bool queue_enabled = true; + constexpr auto select_timer = 500; } // namespace Control namespace Graphics diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index 277d1343a..c1a24ff76 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -1043,7 +1043,9 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) if (!Key::IsInputFocused()) { // Lets try to remove the pre-scan because we hit escape and it's visible if (Key::Pressed(KeyCode::Escape)) { - force_space_action_next_frame = false; + if (config->primary_requires_visible_target) { + force_space_action_next_frame = false; + } if (DidHideViewers()) { return; } @@ -1147,14 +1149,23 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) return original(_this); } -// NOTE: If you change this loop functionality, also change DoHideViewersOfType template +template inline bool CanHideViewer(T* widget, bool require_parent = false) +{ + auto* visibility = widget ? widget->_visibilityController : nullptr; + if (!visibility || (visibility->_state != VisibilityState::Visible && visibility->_state != VisibilityState::Show)) { + return false; + } + if (!require_parent) { + return true; + } + auto* parent = widget->Parent; + return parent && parent->IsShowing; +} + template inline bool CanHideViewersOfType() { for (auto widget : ObjectFinder::GetAll()) { - const auto visible = widget && widget->_visibilityController != NULL - && (widget->_visibilityController->_state == VisibilityState::Visible - || widget->_visibilityController->_state == VisibilityState::Show); - if (visible) { + if (CanHideViewer(widget)) { return true; } } @@ -1171,22 +1182,12 @@ bool CanHideViewers() || CanHideViewersOfType()); } -// NOTE: If you change this loop functionality, also change CanideViewersOfType template template inline bool DidHideViewersOfType() { const auto objects = ObjectFinder::GetAll(); auto didHide = false; for (auto widget : objects) { - if (!widget) { - continue; - } - auto visbility_controller = widget->_visibilityController; - if (!visbility_controller) { - continue; - } - const auto visible = (visbility_controller->_state == VisibilityState::Visible - || visbility_controller->_state == VisibilityState::Show); - if (visible) { + if (CanHideViewer(widget)) { widget->HideAllViewers(); didHide = true; } @@ -1287,14 +1288,6 @@ bool DidExecuteRepair(FleetBarViewController* fleet_bar) FleetState::Repairing); } -template bool IsObjectViewerShowing(T* widget) -{ - auto* visibility = widget ? widget->_visibilityController : nullptr; - auto* parent = widget ? widget->Parent : nullptr; - return visibility && (visibility->_state == VisibilityState::Visible || visibility->_state == VisibilityState::Show) - && parent && parent->IsShowing; -} - void ExecuteSpaceAction(FleetBarViewController* fleet_bar) { if (!fleet_bar) { @@ -1320,6 +1313,7 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) auto has_queue_clear = MapKey::IsDown(GameFunction::ActionQueueClear); auto has_recall = MapKey::IsDown(GameFunction::ActionRecall) && (!Config::Get().disable_preview_recall || !CanHideViewers()); + const bool require_visible_target = has_primary && Config::Get().primary_requires_visible_target; if (has_queue_clear) { action_queue->ClearQueue(fleet); @@ -1329,10 +1323,10 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } else { auto all_pre_scan_widgets = ObjectFinder::GetAll(); for (auto pre_scan_widget : all_pre_scan_widgets) { - if (IsObjectViewerShowing(pre_scan_widget)) { + if (CanHideViewer(pre_scan_widget, require_visible_target)) { if (auto mine_object_viewer_widget = ObjectFinder::Get(); - IsObjectViewerShowing(mine_object_viewer_widget)) { + CanHideViewer(mine_object_viewer_widget, require_visible_target)) { if (has_secondary && pre_scan_widget->_scanEngageButtonsWidget) { return pre_scan_widget->_scanEngageButtonsWidget->OnScanButtonClicked(); } else if (has_primary) { @@ -1424,7 +1418,7 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } if (auto mine_object_viewer_widget = ObjectFinder::Get(); - IsObjectViewerShowing(mine_object_viewer_widget)) { + CanHideViewer(mine_object_viewer_widget, require_visible_target)) { if (has_secondary && mine_object_viewer_widget->_scanEngageButtonsWidget) { if (mine_object_viewer_widget->_scanEngageButtonsWidget->Context) { return mine_object_viewer_widget->_scanEngageButtonsWidget->OnScanButtonClicked(); @@ -1433,7 +1427,8 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) return mine_object_viewer_widget->MineClicked(); } } else if (auto star_node_object_viewer_widget = ObjectFinder::Get(); - IsObjectViewerShowing(star_node_object_viewer_widget) && star_node_object_viewer_widget->Context) { + star_node_object_viewer_widget && star_node_object_viewer_widget->Context + && (!require_visible_target || CanHideViewer(star_node_object_viewer_widget, true))) { if (has_secondary) { star_node_object_viewer_widget->OnViewButtonActivation(); return; @@ -1456,15 +1451,17 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } spdlog::info("have armada? {}, State {}", (armada_widget ? "Yes" : "No"), (int)armada_state); - if (IsObjectViewerShowing(armada_widget)) { + if (require_visible_target + ? CanHideViewer(armada_widget, true) + : armada_widget && (armada_state == VisibilityState::Visible || armada_state == VisibilityState::Show)) { auto button = armada_widget->__get__joinContext(); if (button && button->Interactable) { armada_widget->ValidateThenJoinArmada(); return; } } else { - // Dismissal keeps the native context's last position/path. Require an active movement selection. - if (navigation_ui_controller->IsSetCourseVisible()) { + // Dismissal keeps the native context's last position/path. Preserve that legacy shortcut unless opted out. + if (!require_visible_target || navigation_ui_controller->IsSetCourseVisible()) { navigation_ui_controller->OnSetCourseButtonClick(); } return; From 2d11e3e2ed756920ec7b2007a9ece372373eaf1e Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 15 Oct 2026 19:29:50 -0500 Subject: [PATCH 5/5] Preserve native Armada join visibility in both option modes --- mods/src/patches/parts/hotkeys.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index c1a24ff76..c69ca6d22 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -1451,9 +1451,7 @@ void ExecuteSpaceAction(FleetBarViewController* fleet_bar) } spdlog::info("have armada? {}, State {}", (armada_widget ? "Yes" : "No"), (int)armada_state); - if (require_visible_target - ? CanHideViewer(armada_widget, true) - : armada_widget && (armada_state == VisibilityState::Visible || armada_state == VisibilityState::Show)) { + if (armada_widget && (armada_state == VisibilityState::Visible || armada_state == VisibilityState::Show)) { auto button = armada_widget->__get__joinContext(); if (button && button->Interactable) { armada_widget->ValidateThenJoinArmada();