diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg
index e6d4d44..9c6792c 100644
--- a/GameData/KSPCommunityFixes/Settings.cfg
+++ b/GameData/KSPCommunityFixes/Settings.cfg
@@ -403,6 +403,8 @@ KSP_COMMUNITY_FIXES
// Allow a min value of 0.02 instead of 0.03 for the "Max Physics Delta-Time Per Frame" main menu setting.
LowerMinPhysicsDTPerFrame = true
+ OptimizedModuleRaycasts = true
+
// ##########################
// Modding
// ##########################
diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj
index 0b27596..7588997 100644
--- a/KSPCommunityFixes/KSPCommunityFixes.csproj
+++ b/KSPCommunityFixes/KSPCommunityFixes.csproj
@@ -169,6 +169,7 @@
+
diff --git a/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs
new file mode 100644
index 0000000..6584065
--- /dev/null
+++ b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs
@@ -0,0 +1,152 @@
+using HarmonyLib;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using UnityEngine;
+
+namespace KSPCommunityFixes.Performance
+{
+ internal class OptimizedModuleRaycasts : BasePatch
+ {
+ private static readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate();
+ private static bool partModulesSyncedOnceInFixedUpdate = false;
+
+ protected override void ApplyPatches(List patches)
+ {
+ patches.Add(
+ new PatchInfo(PatchMethodType.Transpiler,
+ AccessTools.Method(typeof(ModuleEngines), nameof(ModuleEngines.EngineExhaustDamage)),
+ this));
+
+ patches.Add(
+ new PatchInfo(PatchMethodType.Prefix,
+ AccessTools.Method(typeof(ModuleDeployableSolarPanel), nameof(ModuleDeployableSolarPanel.CalculateTrackingLOS)),
+ this));
+
+ KSPCommunityFixes.Instance.StartCoroutine(ResetSyncOnFixedEnd());
+ }
+
+ static IEnumerator ResetSyncOnFixedEnd()
+ {
+ while (true)
+ {
+ partModulesSyncedOnceInFixedUpdate = false;
+ lastVesselId = 0;
+ lastTrackingTransformId = 0;
+ yield return waitForFixedUpdate;
+ }
+ }
+
+ static IEnumerable ModuleEngines_EngineExhaustDamage_Transpiler(IEnumerable instructions)
+ {
+ MethodInfo m_Physics_RayCast = AccessTools.Method(typeof(Physics), nameof(Physics.Raycast), new[] { typeof(Vector3), typeof(Vector3), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int) });
+ MethodInfo m_RaycastNoSync = AccessTools.Method(typeof(OptimizedModuleRaycasts), nameof(RaycastNoSync));
+
+ foreach (CodeInstruction instruction in instructions)
+ {
+ if (instruction.Calls(m_Physics_RayCast))
+ {
+ instruction.operand = m_RaycastNoSync;
+ }
+
+ yield return instruction;
+ }
+ }
+
+ private static int lastVesselId;
+ private static int lastTrackingTransformId;
+ private static bool lastHasLoS;
+ private static string lastBlocker;
+
+ private static bool ModuleDeployableSolarPanel_CalculateTrackingLOS_Prefix(ModuleDeployableSolarPanel __instance, Vector3 trackingDirection, ref string blocker, out bool __result)
+ {
+ if (__instance.part.ShieldedFromAirstream && __instance.applyShielding)
+ {
+ blocker = "aero shielding";
+ __result = false;
+ return false;
+ }
+
+ int trackingTransformId = __instance.trackingTransformLocal.GetInstanceID();
+ int vesselId = __instance.vessel.GetInstanceID();
+ if (lastTrackingTransformId == trackingTransformId && lastVesselId == vesselId)
+ {
+ if (!lastHasLoS)
+ {
+ __result = false;
+ blocker = lastBlocker;
+ return false;
+ }
+ }
+ else
+ {
+ lastTrackingTransformId = trackingTransformId;
+ lastVesselId = vesselId;
+
+ Vector3 scaledVesselPos = ScaledSpace.LocalToScaledSpace(__instance.vessel.transform.position);
+ Vector3 scaledDirection = (ScaledSpace.LocalToScaledSpace(__instance.trackingTransformLocal.position) - scaledVesselPos).normalized;
+
+ if (Physics.Raycast(scaledVesselPos, scaledDirection, out RaycastHit scaledHit, float.MaxValue, __instance.planetLayerMask) && scaledHit.transform.NotDestroyedRefNotEquals(__instance.trackingTransformScaled))
+ {
+ __instance.hit = scaledHit; // just to ensure this is populated
+ lastBlocker = scaledHit.transform.gameObject.name; // allocates a string
+ blocker = lastBlocker;
+ lastHasLoS = false;
+ __result = false;
+ return false;
+ }
+
+ lastHasLoS = true;
+ lastBlocker = null;
+ }
+
+ Vector3 localPanelPos = __instance.secondaryTransform.position + trackingDirection * __instance.raycastOffset;
+ __result = !RaycastNoSync(localPanelPos, trackingDirection, out RaycastHit localhit, float.MaxValue, __instance.defaultLayerMask);
+ __instance.hit = localhit; // just to ensure this is populated
+
+ if (!__result
+ && UIPartActionController.Instance.IsNotNullOrDestroyed()
+ && UIPartActionController.Instance.ItemListContains(__instance.part, false)
+ && localhit.transform.gameObject.IsNotNullOrDestroyed())
+ {
+ GameObject hitObject = localhit.transform.gameObject;
+ if (!ReferenceEquals(hitObject.GetComponent(), null))
+ {
+ blocker = ModuleDeployableSolarPanel.cacheAutoLOC_438839;
+ }
+ else
+ {
+ Part partUpwardsCached = FlightGlobals.GetPartUpwardsCached(hitObject);
+ if (partUpwardsCached.IsNotNullOrDestroyed())
+ {
+ blocker = partUpwardsCached.partInfo.title;
+ }
+ else
+ {
+ string tag = hitObject.tag; // allocates a string
+ if (tag.Contains("KSC"))
+ blocker = ResearchAndDevelopment.GetMiniBiomedisplayNameByUnityTag(tag, true);
+ else
+ blocker = hitObject.name;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static bool RaycastNoSync(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask)
+ {
+ if (!partModulesSyncedOnceInFixedUpdate)
+ {
+ Physics.SyncTransforms();
+ partModulesSyncedOnceInFixedUpdate = true;
+ }
+
+ Physics.autoSyncTransforms = false;
+ bool result = Physics.defaultPhysicsScene.Raycast(origin, direction, out hitInfo, maxDistance, layerMask);
+ Physics.autoSyncTransforms = true;
+ return result;
+ }
+ }
+}