diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg
index a0816ee..cbad5af 100644
--- a/GameData/KSPCommunityFixes/Settings.cfg
+++ b/GameData/KSPCommunityFixes/Settings.cfg
@@ -474,6 +474,9 @@ KSP_COMMUNITY_FIXES
PartParsingPerf = true
+ // Significantly reduces the time it takes to open the craft browser and to search by name. Most noticeable with lots of craft.
+ CraftBrowserOptimisations = true
+
// ##########################
// Modding
// ##########################
diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj
index 7a95593..9c35f9c 100644
--- a/KSPCommunityFixes/KSPCommunityFixes.csproj
+++ b/KSPCommunityFixes/KSPCommunityFixes.csproj
@@ -159,6 +159,7 @@
+
diff --git a/KSPCommunityFixes/Performance/CraftBrowserOptimisations.cs b/KSPCommunityFixes/Performance/CraftBrowserOptimisations.cs
new file mode 100644
index 0000000..84cff45
--- /dev/null
+++ b/KSPCommunityFixes/Performance/CraftBrowserOptimisations.cs
@@ -0,0 +1,212 @@
+// Disable logging when the thumbnail is not found.
+// Useful for testing with Unity Explorer installed, where debug calls take measurably longer.
+#define DISABLE_THUMBNAIL_LOGGING
+
+using HarmonyLib;
+using KSP.Localization;
+using KSP.UI.Screens;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace KSPCommunityFixes.Performance
+{
+ class CraftBrowserOptimisations : BasePatch
+ {
+ protected override Version VersionMin => new Version(1, 12, 0);
+
+ // These toggles are here for comparison purposes.
+ public static bool patchCheckCraftFileType = true;
+ public static bool preventImmediateRebuilds = true;
+ public static bool preventDelayedRebuild = true;
+ public static bool preventSearchYield = true;
+ public static bool useTimeBudget = true;
+
+ private static int lastBuiltFrame;
+
+ public static float searchKeystrokeDelay = 0.1f;
+
+ protected override void ApplyPatches()
+ {
+ AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.setbottomButtons));
+
+ AddPatch(PatchType.Prefix, typeof(ShipConstruction), nameof(ShipConstruction.CheckCraftFileType));
+
+ AddPatch(PatchType.Prefix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.BuildPlayerCraftList));
+
+ AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.Start));
+ AddPatch(PatchType.Postfix, typeof(CraftBrowserDialog), nameof(CraftBrowserDialog.ReDisplay));
+
+#if DISABLE_THUMBNAIL_LOGGING
+ AddPatch(PatchType.Transpiler, typeof(ShipConstruction), nameof(ShipConstruction.GetThumbnail), new Type[] { typeof(string), typeof(bool), typeof(bool), typeof(FileInfo) });
+#endif
+
+ AddPatch(PatchType.Prefix, typeof(CraftSearch), nameof(CraftSearch.SearchRoutine));
+ }
+
+ // Fix a miscellanous bug where setbottomButtons ignores showMergeOption. This is useful for Halbann/LazySpawner and BD's Vessel Mover.
+ static void CraftBrowserDialog_setbottomButtons_Postfix(CraftBrowserDialog __instance) =>
+ __instance.btnMerge.gameObject.SetActive(__instance.btnMerge.gameObject.activeSelf && __instance.showMergeOption);
+
+ // Speed up CheckCraftFileType by avoiding loading the whole craft file, instead only checking the file path.
+ // Loadmeta could be used here if it turns out that the path does not always contain the facility name.
+ static bool ShipConstruction_CheckCraftFileType_Prefix(string filePath, ref EditorFacility __result)
+ {
+ if (!patchCheckCraftFileType)
+ return true;
+
+ if (string.IsNullOrEmpty(filePath))
+ {
+ __result = EditorFacility.None;
+ return false;
+ }
+
+ // Search the file path for either "SPH" or "VAB".
+ bool sph = false;
+ string facilityString = filePath.Split(Path.DirectorySeparatorChar).FirstOrDefault(f => (sph = f == "SPH") || f == "VAB");
+
+ if (string.IsNullOrEmpty(facilityString))
+ {
+ // Fallback in case the split fails for any reason.
+ __result = facilityString.Contains("SPH") ? EditorFacility.SPH :
+ (facilityString.Contains("VAB") ? EditorFacility.VAB : EditorFacility.None);
+ }
+ else
+ {
+ __result = sph ? EditorFacility.SPH : EditorFacility.VAB;
+ }
+
+ return false;
+ }
+
+ // Prevent CraftBrowserDialog from rebuilding the craft list twice in a frame, as happens by default.
+ // Only the UI is rebuilt, but it still causes it to take 30% longer to open the dialog.
+ // I think a frame check is a simpler alternative to transpile patches.
+ static bool CraftBrowserDialog_BuildPlayerCraftList_Prefix(CraftBrowserDialog __instance)
+ {
+ if (!preventImmediateRebuilds)
+ return true;
+
+ if (Time.frameCount == lastBuiltFrame)
+ return false;
+
+ lastBuiltFrame = Time.frameCount;
+
+ return true;
+ }
+
+ // Prevent the craft list from rebuilding yet again on the next frame due to some poor logic in DirectoryController.
+ private static void PreventDelayedRebuild(CraftBrowserDialog dialog)
+ {
+ if (!preventDelayedRebuild)
+ return;
+
+ dialog.directoryController.isEnabledThisFrame = false;
+ }
+
+ static void CraftBrowserDialog_Start_Postfix(CraftBrowserDialog __instance)
+ {
+ PreventDelayedRebuild(__instance);
+ CraftSearch.Instance.searchKeystrokeDelay = searchKeystrokeDelay;
+ }
+
+ static void CraftBrowserDialog_ReDisplay_Postfix(CraftBrowserDialog __instance) =>
+ PreventDelayedRebuild(__instance);
+
+ // Disable the call to Debug.Log when the thumbnail is not found.
+ // On a new save with a large number of imported craft, the game will generate a lot of useless log entries.
+ static IEnumerable ShipConstruction_GetThumbnail_Transpiler(IEnumerable instructions)
+ {
+ MethodInfo debug = AccessTools.Method(typeof(Debug), nameof(Debug.Log), new Type[] { typeof(object) });
+
+ foreach (CodeInstruction instruction in instructions)
+ {
+ if (instruction.Calls(debug))
+ {
+ instruction.opcode = OpCodes.Pop;
+ instruction.operand = null;
+ break;
+ }
+ }
+
+ return instructions;
+ }
+
+ // Replace the search routine to prevent the search routine from yielding
+ // a frame on each craft, and to make some small optimisations.
+ static bool CraftSearch_SearchRoutine_Prefix(CraftSearch __instance, ref IEnumerator __result)
+ {
+ if (!preventSearchYield)
+ return true;
+
+ __result = SearchRoutine(__instance);
+ return false;
+ }
+
+ private static IEnumerator SearchRoutine(CraftSearch __instance)
+ {
+ // Delay the start of the search routine by the searchKeystrokeDelay.
+ while (__instance.searchTimer + __instance.searchKeystrokeDelay > Time.realtimeSinceStartup)
+ yield return null;
+
+ bool filtered = false;
+ string searchTerm = __instance.searchField.text;
+ List entries = CraftSearch.craftBrowserDialog.craftList;
+ float timer = Time.realtimeSinceStartup;
+ float budget = 1f / 60f / 2f; // 8 ms.
+ bool searchEmpty = string.IsNullOrWhiteSpace(searchTerm);
+
+#if DEBUG
+ Stopwatch stopwatch = Stopwatch.StartNew();
+#endif
+
+ foreach (CraftEntry entry in entries)
+ {
+ bool result = searchEmpty || FasterCraftMatchesSearch(entry, searchTerm);
+
+ if (entry.gameObject.activeSelf != result)
+ entry.gameObject.SetActive(result);
+
+ if (!filtered && !result)
+ filtered = true;
+
+ if (useTimeBudget && Time.realtimeSinceStartup - timer > budget)
+ {
+ timer = Time.realtimeSinceStartup;
+ yield return null;
+ }
+ }
+
+#if DEBUG
+ stopwatch.Stop();
+ Debug.Log($"[CraftBrowserOptimisations]: Filtered {entries.Count} craft in {stopwatch.Elapsed.Milliseconds:N3} ms.");
+#endif
+
+ __instance.hasFiltered?.Invoke(filtered);
+
+ if (string.IsNullOrWhiteSpace(searchTerm) && __instance.IsDifferentSearch)
+ __instance.StopSearch();
+
+ __instance.previousSearch = searchTerm;
+ }
+
+ private static bool FasterCraftMatchesSearch(CraftEntry craft, string searchTerm)
+ {
+ if (craft.craftName.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1)
+ return true;
+
+ if (craft.craftProfileInfo == null || string.IsNullOrEmpty(craft.craftProfileInfo.description))
+ return false;
+
+ string text = Localizer.Format(craft.craftProfileInfo.description);
+ return text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1;
+ }
+ }
+}
diff --git a/README.md b/README.md
index 955d099..c48a6a2 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,7 @@ User options are available from the "ESC" in-game settings menu :
 [KSP 1.12.3 - 1.12.5]<br/>Various small performance patches (volume normalizer, eva module checks)
- [**FloatingOriginPerf**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/257) [KSP 1.12.3 - 1.12.5]<br/>General micro-optimization of floating origin shifts. Main benefit is in large particle count situations (ie, launches with many engines) but this helps a bit in other cases as well.
- [**FasterPartFindTransform**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/255) [KSP 1.12.3 - 1.12.5]<br/>Faster, and minimal GC alloc relacements for the Part FindModelTransform* and FindHeirarchyTransform* methods.
+- [**CraftBrowserOptimisations**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/284) [KSP 1.12.0 - 1.12.5]<br/>Significantly reduces the time it takes to open the craft browser and to search by name. Most noticeable with lots of craft.
- [**OptimisedVectorLines**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/281) [KSP 1.12.0 - 1.12.5]<br/>Improve performance in the Map View when a large number of vessels and bodies are visible via faster drawing of orbit lines and CommNet lines.
#### API and modding tools
)