From 1711c9f12c604c1d18ff803bf53d159a6e1e76f1 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Sat, 15 Aug 2026 22:44:58 +0100 Subject: [PATCH] Stop stock toolbar buttons being duplicated on scene switch An app now gets one trip to the toolbar at a time, and only while it is not already on it. A genuine second instance still destroys itself as it always did. --- CHANGELOG.md | 1 + GameData/KSPCommunityFixes/Settings.cfg | 6 ++ .../BugFixes/DuplicateAppLauncherButtons.cs | 70 +++++++++++++++++++ README.md | 1 + 4 files changed, 78 insertions(+) create mode 100644 KSPCommunityFixes/BugFixes/DuplicateAppLauncherButtons.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3aea9..f7d3159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ##### Unreleased **New/Improved patches** +- New KSP bugfix : **DuplicateAppLauncherButtons** Fix the stock toolbar accumulating duplicate buttons on switching scene, and the flood of `NullReferenceException` that follows. A `UIApp` adds itself to the toolbar again every time the app launcher restarts, losing track of the button it already has, and then destroys itself because its duplicate check tests whether an instance exists rather than whether it is a different one. - Improved the **FastLoader** patch to reuse the initial GameDatabase directory tree during the second config pass while refreshing files and directories created or modified by `Startup.Instantly` addons. Avoids reparsing unchanged configs and saves several seconds in heavily modded installs. **Bug Fixes** diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index 1fb9b39..092bf69 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -283,6 +283,12 @@ KSP_COMMUNITY_FIXES // dialog being wildly inflated. FlightLoggerInflatedDistance = true + // Fix stock toolbar buttons being duplicated on switching scene, and the flood of NullReferenceException + // that follows. An app adds itself to the toolbar again whenever the launcher restarts, losing track of + // the button it already has, and then destroys itself because its own duplicate check tests whether an + // instance exists rather than whether it is a different one. + DuplicateAppLauncherButtons = true + // ########################## // Obsolete bugfixes // ########################## diff --git a/KSPCommunityFixes/BugFixes/DuplicateAppLauncherButtons.cs b/KSPCommunityFixes/BugFixes/DuplicateAppLauncherButtons.cs new file mode 100644 index 0000000..8437268 --- /dev/null +++ b/KSPCommunityFixes/BugFixes/DuplicateAppLauncherButtons.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using KSP.UI.Screens; + +namespace KSPCommunityFixes.BugFixes +{ + internal class DuplicateAppLauncherButtons : BasePatch + { + // The apps that are part way through adding themselves, by instance id. Keyed on the id + // rather than the app because a destroyed UnityEngine.Object compares equal to null. + static readonly HashSet addsInProgress = new HashSet(); + + protected override Version VersionMin => new Version(1, 8, 0); + + protected override void ApplyPatches() + { + AddPatch(PatchType.Prefix, typeof(UIApp), "AddToAppLauncher"); + AddPatch(PatchType.Postfix, typeof(UIApp), "set_appLauncherButton", + nameof(UIApp_SetAppLauncherButton_Postfix)); + AddPatch(PatchType.Postfix, typeof(UIApp), "OnDestroy"); + } + + static bool UIApp_AddToAppLauncher_Prefix(UIApp __instance, ref IEnumerator __result) + { + if (IsOnAppLauncher(__instance.appLauncherButton) || + !addsInProgress.Add(__instance.GetInstanceID())) + { + // Already on the toolbar, or on its way there. Going again would overwrite the only + // reference to the button it ends up with, leaving the other one on the toolbar for + // the rest of the session, and would run OnAppInitialized a second time. + __result = Nothing(); + return false; + } + + return true; + } + + static void UIApp_SetAppLauncherButton_Postfix(UIApp __instance) + { + addsInProgress.Remove(__instance.GetInstanceID()); + } + + static void UIApp_OnDestroy_Postfix(UIApp __instance) + { + // The app can be destroyed part way through adding itself, including by + // OnAppInitialized when it turns out to be a second instance. + addsInProgress.Remove(__instance.GetInstanceID()); + } + + static bool IsOnAppLauncher(ApplicationLauncherButton button) + { + if (button.IsNullOrDestroyed()) + return false; + + // The button outlives the launcher being torn down and built again, so holding one is + // not on its own enough to say the app is still on the toolbar. + ApplicationLauncher launcher = ApplicationLauncher.Instance; + if (launcher.IsNullOrDestroyed()) + return false; + + return launcher.appList.Contains(button) || launcher.appListHidden.Contains(button); + } + + static IEnumerator Nothing() + { + yield break; + } + } +} diff --git a/README.md b/README.md index a2c5f6e..ca986de 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ User options are available from the "ESC" in-game settings menu :
Fix the stock toolbar accumulating duplicate buttons (resource display, action groups, alarm clock, delta-v...) on switching scene, and the flood of `NullReferenceException` from `KSP.UI` that follows. A `UIApp` adds itself to the toolbar again every time the app launcher restarts, losing track of the button it already has, and then destroys itself because its duplicate check tests whether an instance exists rather than whether it is a different one. - **PartTooltipUpgradesApplyToSubstituteParts** [KSP 1.12.0 - 1.12.5]
Disabled by default, you can enable it with a MM patch. Fix part upgrades being applied directly to the prefab part when creating the `PartListTooltip`, instead using a substitute part instance. Requires **UpgradeBugs** to be enabled. #### Quality of Life tweaks