diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg
index e6d4d44..c82c451 100644
--- a/GameData/KSPCommunityFixes/Settings.cfg
+++ b/GameData/KSPCommunityFixes/Settings.cfg
@@ -403,6 +403,10 @@ 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
+ // Improve engine exhaust damage and solar panel line of sight raycasts performance by avoiding extra physics
+ // state synchronization and caching solar panels scaled space raycasts results.
+ 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/Library/Extensions.cs b/KSPCommunityFixes/Library/Extensions.cs
index d3e4422..0553322 100644
--- a/KSPCommunityFixes/Library/Extensions.cs
+++ b/KSPCommunityFixes/Library/Extensions.cs
@@ -28,5 +28,10 @@ public static string AssemblyQualifiedName(this object obj)
Type type = obj.GetType();
return $"{type.Assembly.GetName().Name}:{type.Name}";
}
+
+ public static bool IsPAWOpen(this Part part)
+ {
+ return part.PartActionWindow.IsNotNullOrDestroyed() && part.PartActionWindow.isActiveAndEnabled;
+ }
}
}
diff --git a/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs
new file mode 100644
index 0000000..4c402c5
--- /dev/null
+++ b/KSPCommunityFixes/Performance/OptimizedModuleRaycasts.cs
@@ -0,0 +1,152 @@
+using System;
+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 Version VersionMin => new Version(1, 12, 3);
+
+ 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 && __instance.part.IsPAWOpen() && 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;
+ }
+ }
+}
diff --git a/README.md b/README.md
index 0a77aee..36740e5 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ User options are available from the "ESC" in-game settings menu :
Improve engine exhaust damage and solar panel line of sight raycasts performance by avoiding extra physics state synchronization and caching solar panels scaled space raycasts results.
#### API and modding tools
- **MultipleModuleInPartAPI** [KSP 1.8.0 - 1.12.5]
This API allow other plugins to implement PartModules that can exist in multiple occurrence in a single part and won't suffer "module indexing mismatch" persistent data losses following part configuration changes. [See documentation on the wiki](https://github.com/KSPModdingLibs/KSPCommunityFixes/wiki/MultipleModuleInPartAPI).
@@ -189,6 +189,7 @@ If doing so in the `Debug` configuration and if your KSP install is modified to
### Changelog
##### 1.35.0
+- New KSP performance patch : [**OptimizedModuleRaycasts**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/216) : Improve engine exhaust damage and solar panel line of sight raycasts performance by avoiding extra physics state synchronization and caching solar panels scaled space raycasts results.
- **FastLoader** : Improved DDS loading performance by avoiding an extra copy of the DDS data
##### 1.34.1