From 58280bbd1a4de4c3e8f48ec779891fafd0e225fb Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:28:26 +0100 Subject: [PATCH 1/3] Attitude control overhaul, 4 new patches : - [BugFix] GetPotentialTorqueFixes : Rewrite of the stock ITorqueProvider.GetPotentialTorque() implementations for the reaction wheels, rcs, gimbal and control surface stock partmodules. Fix various issues with the stock implementations, ranging from "minor" to "completely broken". Also add in-flight available torque readouts in the PAW for gimbals and control surfaces. - [Perf] NoLiftInSpace : Disable control surfaces updates and actuation when the part isn't submerged or in an atmosphere - [QoL] RCSLimiter : Add two extra tweakables in the RCS module "Actuation Toggles", giving the ability to define a separate angle threshold for linear and rotation actuation. This allow to optimize efficiency of multi-nozzle RCS parts that are impossible to fine-tune with only the actuation toggles. This also add a potential torque/force readout to the actuation toggles PAW items, both in editor and in flight. - [QoL] BetterSAS : Slightly improves the stock SAS precision and implement an optional alternate attitude controller called "PreciseController" (derived from the MechJeb PID controller), more stable and precise for in-space operations but not well suited for atmospheric operations. - Added an EditorPhysics support class that provide in-editor data about CoM and reference part, as well as the state of the stock delta-v app situation selector. Used by the RCSLimiter patch to provide in-editor torque readouts. New modding patch : BaseFieldListUseFieldHost, allow BaseField and related features (PAW controls, persistence...) to work when a custom BaseField is added to a BaseFieldList (ie, a Part or PartModule) with a host instance other than the BaseFieldList owner. Allow to dynamically add fields defined in other classes to a Part or PartModule. --- GameData/KSPCommunityFixes/Settings.cfg | 24 + .../BugFixes/GetPotentialTorqueFixes.cs | 1446 +++++++++++++++++ KSPCommunityFixes/Internal/EditorPhysics.cs | 214 +++ KSPCommunityFixes/KSPCommunityFixes.csproj | 6 + .../Modding/BaseFieldListUseFieldHost.cs | 210 +++ .../Performance/NoLiftInSpace.cs | 104 ++ KSPCommunityFixes/QoL/BetterSAS.cs | 823 ++++++++++ KSPCommunityFixes/QoL/RCSLimiter.cs | 690 ++++++++ 8 files changed, 3517 insertions(+) create mode 100644 KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs create mode 100644 KSPCommunityFixes/Internal/EditorPhysics.cs create mode 100644 KSPCommunityFixes/Modding/BaseFieldListUseFieldHost.cs create mode 100644 KSPCommunityFixes/Performance/NoLiftInSpace.cs create mode 100644 KSPCommunityFixes/QoL/BetterSAS.cs create mode 100644 KSPCommunityFixes/QoL/RCSLimiter.cs diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index bc9e26c..eb05ab3 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -157,6 +157,11 @@ KSP_COMMUNITY_FIXES // Fix spread angle still being applied after decoupling symmetry-placed parachutes. ChutePhantomSymmetry = true + // Rewrite of the stock ITorqueProvider.GetPotentialTorque() implementations for the reaction wheels, + // rcs, gimbal and control surface stock partmodules. Fix various issues with the stock implementations, + // ranging from "minor" to "completely broken". + GetPotentialTorqueFixes = true + // ########################## // Obsolete bugfixes // ########################## @@ -210,6 +215,17 @@ KSP_COMMUNITY_FIXES // Add part actions for locking/unlocking part resources flow state. ResourceLockActions = true + // Slightly improves the stock SAS precision and implement an optional alternate attitude + // controller called "PreciseController", more stable and precise for in-space operations but + // not suited for atmospheric operations. + BetterSAS = true + + // Add two extra tweakables in the RCS module "Actuation Toggles", giving the ability to define + // a separate angle threshold for linear and rotation actuation. This allow to optimize efficiency + // of multi-nozzle RCS parts that are impossible to fine-tune with only the actuation toggles. + // This also add a potential torque/force readout to the actuation toggles PAW items. + RCSLimiter = true + // ########################## // Performance tweaks // ########################## @@ -302,6 +318,9 @@ KSP_COMMUNITY_FIXES // Prevent performance drops when there are in-progress comet sample or rover construction contracts ContractProgressEnumCache = true + // Disable control surfaces updates and actuation when the part isn't submerged or in an atmosphere + NoLiftInSpace = true + // ########################## // Modding // ########################## @@ -335,6 +354,11 @@ KSP_COMMUNITY_FIXES // upgrade scripts. ModUpgradePipeline = false + // Allow BaseField and related features (PAW controls, persistence...) to work when a custom BaseField + // is added to a BaseFieldList (ie, a Part or PartModule) with a host instance other than the + // BaseFieldList owner. Allow to dynamically add fields defined in other classes to a Part or PartModule. + BaseFieldListUseFieldHost = true + // ########################## // Localization tools // ########################## diff --git a/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs b/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs new file mode 100644 index 0000000..b215858 --- /dev/null +++ b/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs @@ -0,0 +1,1446 @@ +using System; +using System.Collections; +using HarmonyLib; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using KSP.Localization; +using KSPCommunityFixes.QoL; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.UIElements; +using Random = UnityEngine.Random; + +/* +This patch is a rewrite of the stock implementations for `ITorqueProvider.GetPotentialTorque(out Vector3 pos, out Vector3 neg)`. +All 4 of the stock implementations have various issues and are generally giving unreliable (not to say plain wrong) results. +Those issues are commented in each patch, but to summarize : +- ModuleRectionWheels is mostly ok, its only issue is to ignore the state of "authority limiter" tweakable +- ModuleRCS is giving entirely random results, and the stcok implementation just doesn't make any sense. Note that compared to + other custom implementations (MechJeb, TCA, kOS), the KSPCF implementation account for the RCS module control scheme thrust + clamping and the actual thrust power (instead of the theoretical maximum). +- ModuleGimbal results are somewhat coherent, but their magnitude for pitch/yaw is wrong. They are underestimated for CoM-aligned + engines and vastly overestimated for engines placed off-CoM-center. +- ModuleControlSurface results are generally unreliable. Beside the fact that they can be randomly negative, the magnitude is + usually wrong and inconsistent. Depending on part placement relative to the CoM, they can return zero available torque or being + vastly overestimated. They also don't account for drag induced torque, and are almost entirely borked when a control surface is + in the deployed state. + +Note that the KSPCF GetPotentialTorque() implementations for ModuleControlSurface and especially for ModuleGimbal are more +computationally intensive that the stock ones. Profiling a stock Dynawing with RCS enabled during ascent show a ~30% degradation +when summing the vessel total available torque (~250 calls median : 0.31ms vs 0.24ms, frame time : 1.81% vs 1.46% ). Overall +this feels acceptable, but this is still is a non-negligible impact that will likely be noticeable in some cases (ie, +atmospheric flight with a large vessel having many gimballing engines and control surfaces). +The implementations are pretty naive and could probably be vastly optimized by someone with a better understanding than me of +the underlying maths and physics. + +The KSPCF implementations follow these conventions : +- in pos/neg : x is pitch, y is roll, z is yaw +- `pos` is the actuation induced torque for a positive FlightCtrlState (pitch = 1, roll, = 1 yaw = 1) control request +- `neg` is the actuation induced torque for a negative FlightCtrlState (pitch = -1, roll, = -1 yaw = -1) control request +- Contrary to the stock implementations, values are strictly the **actuation induced** torque (ie, the torque difference + between the neutral state and the actuated state). Especially in the case of ModuleGimbal, the stock implementation + returns the actuation torque plus the eventual "structural" torque due to an eventual CoM/CoT misalignement. +- Positive values mean actuation will induce a torque in the desired direction. Negatives values mean that actuation will + induce a torque in the opposite direction. For example, a negative `pos.x` value mean that for a positive roll actuation + (ctrlState.roll = 1), the torque provider will produce a torque inducing a negative roll, essentially reducing the total + available torque in that direction. This can notably happen with the stock aero control surfaces, due to their control + scheme being only based on their relative position/orientation to the vessel CoM and ignoring other factors like AoA. +- Like the stock implementations, they will give reliable results only if called from FixedUpdate(), including the control + state callbacks like `Vessel.OnFlyByWire` or `Vessel.On*AutopilotUpdate`. Calling them from the Update() loop will result + in an out-of-sync CoM position being used, producing garbage results. + +So in the context of the KSPCF patch, a correct implementation of a `GetVesselPotentialTorque()` method is : + ```cs + foreach (ITorqueProvider torqueProvider) + { + torqueProvider.GetPotentialTorque(out Vector3 pos, out Vector3 neg); + vesselPosTorque += pos; + vesselNegTorque += neg; + } + if (vesselPosTorque.x < 0f) vesselPosTorque.x = 0f; + if (vesselPosTorque.y < 0f) vesselPosTorque.y = 0f; + if (vesselPosTorque.z < 0f) vesselPosTorque.z = 0f; + if (vesselNegTorque.x < 0f) vesselNegTorque.x = 0f; + if (vesselNegTorque.y < 0f) vesselNegTorque.y = 0f; + if (vesselNegTorque.z < 0f) vesselNegTorque.z = 0f; + ``` + +Quick review of how the stock implementations are handled in the modding ecosystem : +- *It seems* Mechjeb doesn't care about a value being from "pos" or "neg", it assume a negative value from either of the vector3 + is a negative torque component (ie, if "pos.x" or "neg.x" is negative, it add that as negative available torque around x). + Ref : https://github.com/MuMech/MechJeb2/blob/f5c1193813da7d2e2e347f963dd4ee4b7fb11a90/MechJeb2/VesselState.cs#L1073-L1076 + Ref2 : https://github.com/MuMech/MechJeb2/blob/f5c1193813da7d2e2e347f963dd4ee4b7fb11a90/MechJeb2/Vector6.cs#L82-L93 + As it is, since MechJeb doesn't care for pos/neg and only consider the max, the patches will result in wrong values, but arguably + since it reimplement RCS they will only be "different kind of wrong" for control surfaces and gimbals, and probably "less wrong" + overall. +- kOS assume that the absolute value should be used. + (side note : kOS reimplements ModuleReactionWheel.GetPotentialTorque() to get around the authority limiter bug) + Ref : https://github.com/KSP-KOS/KOS/blob/7b7874153bc6c428404b3a1a913487b2fd0a9d99/src/kOS/Control/SteeringManager.cs#L658-L664 + The patches should apply mostly alright for kOS, at the exception of occasional negative values for gimbals and control surfaces + being treated as positive, resulting in a higher available torque than what it should. +- TCA doesn't seem aware of the possibility of negative values, it assume they are positive. + Ref : https://github.com/allista/ThrottleControlledAvionics/blob/b79a7372ab69616801f9953256b43ee872b90cf2/VesselProps/TorqueProps.cs#L167-L169 + The patches should more or less work for TCA, at the exception of negative gimbal/control surfaces values being treated incorrectly + and the reaction wheels authority limiter being applied twice. +- Atmospheric Autopilot replace the stock module implementation by its own and doesn't use the interface at all + Ref : https://github.com/Boris-Barboris/AtmosphereAutopilot/blob/master/AtmosphereAutopilot/SyncModuleControlSurface.cs +- FAR implements a replacement for ModuleControlSurface and consequently has a custom GetPotentialTorque() implementation. + It seems that it will *always* return positive "pos" values and negative "neg" values : + Ref : https://github.com/dkavolis/Ferram-Aerospace-Research/blob/95e127ae140b4be9699da8783d24dd8db726d753/FerramAerospaceResearch/LEGACYferram4/FARControllableSurface.cs#L294-L300 +*/ + +namespace KSPCommunityFixes.BugFixes +{ + class GetPotentialTorqueFixes : BasePatch + { + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleReactionWheel), nameof(ModuleReactionWheel.GetPotentialTorque)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.GetPotentialTorque)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleControlSurface), nameof(ModuleControlSurface.GetPotentialTorque)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleControlSurface), nameof(ModuleControlSurface.OnStart)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleGimbal), nameof(ModuleGimbal.GetPotentialTorque)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleGimbal), nameof(ModuleGimbal.OnStart)), + this)); + + autoLOC_6001330_Pitch = Localizer.Format("#autoLOC_6001330"); + autoLOC_6001331_Yaw = Localizer.Format("#autoLOC_6001331"); + autoLOC_6001332_Roll = Localizer.Format("#autoLOC_6001332"); + + KSPCommunityFixes.Instance.StartCoroutine(CustomUpdate()); + } + + private IEnumerator CustomUpdate() + { + Repeat: + ModuleGimbalExtension.UpdateInstances(); + ModuleCtrlSrfExtension.UpdateInstances(); + yield return null; + goto Repeat; + } + + private static string autoLOC_6001330_Pitch; + private static string autoLOC_6001331_Yaw; + private static string autoLOC_6001332_Roll; + + static ProfilerMarker rwProfiler = new ProfilerMarker("ModuleReactionWheel.GetPotentialTorque"); + static ProfilerMarker rcsProfiler = new ProfilerMarker("ModuleRCS.GetPotentialTorque"); + static ProfilerMarker ctrlSrfProfiler = new ProfilerMarker("ModuleControlSurface.GetPotentialTorque"); + static ProfilerMarker gimbalProfiler = new ProfilerMarker("ModuleGimbal.GetPotentialTorque"); + static ProfilerMarker gimbalCacheProfiler = new ProfilerMarker("ModuleGimbal.GetPotentialTorque.CacheCheck"); + + #region ModuleReactionWheel + + // Fix reaction wheels reporting incorrect available torque when the "Wheel Authority" tweakable is set below 100%. + static bool ModuleReactionWheel_GetPotentialTorque_Prefix(ModuleReactionWheel __instance, out Vector3 pos, out Vector3 neg) + { + rwProfiler.Begin(); + if (__instance.moduleIsEnabled && __instance.wheelState == ModuleReactionWheel.WheelState.Active && __instance.actuatorModeCycle != 2) + { + float authorityLimiter = __instance.authorityLimiter * 0.01f; + neg.x = pos.x = __instance.PitchTorque * authorityLimiter; + neg.y = pos.y = __instance.RollTorque * authorityLimiter; + neg.z = pos.z = __instance.YawTorque * authorityLimiter; + rwProfiler.End(); + return false; + } + + pos = neg = Vector3.zero; + rwProfiler.End(); + return false; + } + + #endregion + + #region ModuleRCS + + // The stock implementation is 100% broken, this is a complete replacement + static bool ModuleRCS_GetPotentialTorque_Prefix(ModuleRCS __instance, out Vector3 pos, out Vector3 neg) + { + rcsProfiler.Begin(); + pos = Vector3.zero; + neg = Vector3.zero; + + if (!__instance.moduleIsEnabled + || !__instance.rcsEnabled + || !__instance.rcs_active + || __instance.IsAdjusterBreakingRCS() + || __instance.isJustForShow + || __instance.flameout + || (__instance.part.ShieldedFromAirstream && !__instance.shieldedCanThrust) + || (!__instance.enablePitch && !__instance.enableRoll && !__instance.enableYaw)) + { + rcsProfiler.End(); + return false; + } + + float power = GetMaxRCSPower(__instance); + if (power < 0.0001f) + { + rcsProfiler.End(); + return false; + } + + Vector3 currentCoM = __instance.vessel.CurrentCoM; + + Quaternion controlRotation = __instance.vessel.ReferenceTransform.rotation; + Vector3 pitchCtrl = controlRotation * Vector3.right; + Vector3 rollCtrl = controlRotation * Vector3.up; + Vector3 yawCtrl = controlRotation * Vector3.forward; + + float minRotActuation; + bool checkActuation = RCSLimiter.moduleRCSExtensions != null; + if (checkActuation && RCSLimiter.moduleRCSExtensions.TryGetValue(__instance, out RCSLimiter.ModuleRCSExtension limits)) + { + minRotActuation = Mathf.Max(Mathf.Cos(limits.minRotationAlignement * Mathf.Deg2Rad), 0f); + } + else + { + minRotActuation = 0f; + checkActuation = __instance.fullThrust; + } + + for (int i = __instance.thrusterTransforms.Count - 1; i >= 0; i--) + { + Transform thruster = __instance.thrusterTransforms[i]; + + if (!thruster.gameObject.activeInHierarchy) + continue; + + Vector3 thrusterPosFromCoM = thruster.position - currentCoM; + Vector3 thrusterDirFromCoM = thrusterPosFromCoM.normalized; + Vector3 thrustDirection = __instance.useZaxis ? thruster.forward : thruster.up; + + float thrusterPower = power; + if (FlightInputHandler.fetch.precisionMode) + { + if (__instance.useLever) + { + float leverDistance = __instance.GetLeverDistance(thruster, thrustDirection, currentCoM); + if (leverDistance > 1f) + { + thrusterPower /= leverDistance; + } + } + else + { + thrusterPower *= __instance.precisionFactor; + } + } + + Vector3 thrusterThrust = thrustDirection * thrusterPower; + Vector3 thrusterTorque = Vector3.Cross(thrusterPosFromCoM, thrusterThrust); + // transform in vessel control space + thrusterTorque = __instance.vessel.ReferenceTransform.InverseTransformDirection(thrusterTorque); + + if (__instance.enablePitch && Math.Abs(thrusterTorque.x) > 0.0001f) + { + Vector3 pitchRot = Vector3.Cross(pitchCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, pitchCtrl)); + float actuation = Vector3.Dot(thrustDirection, pitchRot.normalized); + + if (checkActuation) + { + float actuationMagnitude = Math.Abs(actuation); + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (__instance.fullThrust && actuationMagnitude > __instance.fullThrustMin) + actuation = Math.Sign(actuation); + } + + if (actuation != 0f) + { + if (actuation > 0f) + pos.x += thrusterTorque.x * actuation; + else + neg.x += thrusterTorque.x * actuation; + } + } + + if (__instance.enableRoll && Math.Abs(thrusterTorque.y) > 0.0001f) + { + Vector3 rollRot = Vector3.Cross(rollCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, rollCtrl)); + float actuation = Vector3.Dot(thrustDirection, rollRot.normalized); + + if (checkActuation) + { + float actuationMagnitude = Math.Abs(actuation); + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (__instance.fullThrust && actuationMagnitude > __instance.fullThrustMin) + actuation = Math.Sign(actuation); + } + + if (actuation != 0f) + { + if (actuation > 0f) + pos.y += thrusterTorque.y * actuation; + else + neg.y += thrusterTorque.y * actuation; + } + } + + if (__instance.enableYaw && Math.Abs(thrusterTorque.z) > 0.0001f) + { + Vector3 yawRot = Vector3.Cross(yawCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, yawCtrl)); + float actuation = Vector3.Dot(thrustDirection, yawRot.normalized); + + if (checkActuation) + { + float actuationMagnitude = Math.Abs(actuation); + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (__instance.fullThrust && actuationMagnitude > __instance.fullThrustMin) + actuation = Math.Sign(actuation); + } + + if (actuation != 0f) + { + if (actuation > 0f) + pos.z += thrusterTorque.z * actuation; + else + neg.z += thrusterTorque.z * actuation; + } + } + } + +#if DEBUG + TorqueUIModule ui = __instance.part.FindModuleImplementing(); + if (ui != null) + { + ui.pos = pos; + ui.neg = neg; + } +#endif + rcsProfiler.End(); + return false; + } + + private static float GetMaxRCSPower(ModuleRCS mrcs) + { + if (!mrcs.requiresFuel) + return 1f; + + double flowMult = mrcs.flowMult; + if (mrcs.useThrustCurve) + flowMult *= mrcs.thrustCurveDisplay; + + float result = (float)(flowMult * mrcs.maxFuelFlow * mrcs.exhaustVel * mrcs.thrustPercentage * 0.01); + return result; + } + + #endregion + + #region ModuleControlSurface + + // The stock ModuleControlSurface.GetPotentialTorque() implementation has several issues and its results are overall very wrong : + // - It doesn't take drag forces into account (only lift). + // - It attempt to provide actuation torque (ie, the torque difference between pos/neg actuation and the neutral state) by substracting + // the neutral lift force vector to the actuated pos/neg force vectors. This is wrong and produce garbage results, it's the resulting + // torque from the neutral vector that should be substracted to the resulting torque from the pos/neg force vectors. + // - It entirely fails to correct the raw torque results for the pitch/roll/yaw actuation inversion and clamping logic, resulting in + // wrongly negative values and random inversions of the neg / pos value. + // - For reasons that escape my understanding entirely, it multiply the torque results by the vessel CoM to Part vector, resulting in + // more non-sense sign inversions and a near squaring of the result magnitude. + // - It generally doesn't handle correctly control surfaces in the deployed state. + + // This reimplementation fixes all the above issues. + // It still has a few shortcomings. Notably, it partially reuse results from the previous FixedUpdate() and mix them with current values. + // This mean the results are slightly wrong, but this saves quite a bit of extra processing in that already quite performance heavy method, + // and the error magnitude shouldn't matter for potential applications of GetPotentialTorque(). + // It shall also be noted that the result magnitude is an approximation when the actuation is clamped by the module (ie, when the control + // surface neutral state isn't aligned with the airflow), with the error being greater when the allowed actuation is lower. + + // Note that the results can still return negative components. The meaning of a negative value is that the actuation of that component will + // induce a torque in the opposite direction. For example, a negative pos.x value mean that for a positive roll actuation (ctrlState.roll > 0), + // the control surface will produce a torque incuding a negative roll, essentially reducing the total available torque in that direction. + + + // stuff we don't have in the editor : nVel, Qlift, part.machNumber, Qdrag, baseLiftForce + + private class ModuleCtrlSrfExtension + { + private static Dictionary instances = new Dictionary(); + + public static ModuleCtrlSrfExtension Get(ModuleControlSurface module) + { + if (instances.TryGetValue(module, out ModuleCtrlSrfExtension gimbalExt)) + return gimbalExt; + + return new ModuleCtrlSrfExtension(module); + } + + private ModuleControlSurface module; + + public Vector3 pos; + public Vector3 neg; + + public Vector3 worldCoM; + public Vector3 localCoM; + public Vector3 nVel; + public Vector3 neutralForce; + public double QLift; + public double QDrag; + public float currentDeployAngle; + public float machNumber; + + public float lastTime; + public Vector3 lastBaseLiftForce; + private bool lastPitch; + private bool lastRoll; + private bool lastYaw; + + private bool pawTorqueEnabled; + private BaseField ignorePitchField; + private BaseField ignoreRollField; + private BaseField ignoreYawField; + + public ModuleCtrlSrfExtension(ModuleControlSurface module) + { + this.module = module; + + ignorePitchField = module.Fields[nameof(ModuleControlSurface.ignorePitch)]; + ignoreRollField = module.Fields[nameof(ModuleControlSurface.ignoreRoll)]; + ignoreYawField = module.Fields[nameof(ModuleControlSurface.ignoreYaw)]; + + module.part.OnJustAboutToBeDestroyed += OnDestroy; + instances.Add(module, this); + } + + public void OnDestroy() + { + module.part.OnJustAboutToBeDestroyed -= OnDestroy; + instances.Remove(module); + module = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UpdateCachedState(Vector3 worldCoM, Vector3 localCoM) + { + this.worldCoM = worldCoM; + this.localCoM = localCoM; + lastTime = Time.fixedTime; + nVel = module.nVel; + QLift = module.Qlift; + QDrag = module.Qdrag; + machNumber = (float)module.part.machNumber; + + if (module.deploy) + { + currentDeployAngle = module.currentDeployAngle; + Vector3 rhsNeutral = Quaternion.AngleAxis(currentDeployAngle, module.baseTransform.rotation * Vector3.right) * module.baseTransform.forward; + float dotNeutral = Vector3.Dot(nVel, rhsNeutral); + float absDotNeutral = Mathf.Abs(dotNeutral); + neutralForce = module.GetLiftVector(rhsNeutral, dotNeutral, absDotNeutral, QLift, machNumber) * module.ctrlSurfaceArea; + neutralForce += GetDragForce(module, this, absDotNeutral); + } + else + { + currentDeployAngle = 0f; + neutralForce = module.baseLiftForce * module.ctrlSurfaceArea; + neutralForce += GetDragForce(module, this, module.absDot); + } + + lastBaseLiftForce = module.baseLiftForce; + lastPitch = module.ignorePitch; + lastRoll = module.ignoreRoll; + lastYaw = module.ignoreYaw; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsCacheValid(Vector3 worldCoM, Vector3 localCoM) + { + if ((QLift > 0.0 && Math.Abs((module.Qlift / QLift) - 1.0) > Random.Range(0.04f, 0.06f)) + || (this.localCoM - localCoM).sqrMagnitude > 0.1f * 0.1f + || (lastBaseLiftForce - module.baseLiftForce).sqrMagnitude > 0.1f * 0.1f + || lastTime + Random.Range(0.75f, 1.25f) < Time.fixedTime + || currentDeployAngle != module.currentDeployAngle + || lastPitch != module.ignorePitch || lastRoll != module.ignoreRoll || lastYaw != module.ignoreYaw) + { + UpdateCachedState(worldCoM, localCoM); + return true; + } + + return false; + } + + public void UpdateEditorState(Transform referenceTransform, EditorPhysics editorPhysics) + { + if (editorPhysics.atmDensity == 0.0) + return; + + worldCoM = editorPhysics.CoM; + + nVel = referenceTransform.up; // velocity normalized + + currentDeployAngle = module.deploy ? module.currentDeployAngle : 0f; + + double dynamicPressureKpa = editorPhysics.atmDensity; + double speed = 100.0; // just assume 100m/s for now + + dynamicPressureKpa *= 0.0005 * speed * speed; + QLift = dynamicPressureKpa * 1000.0; + QDrag = dynamicPressureKpa * 1000.0; + + double speedOfSound = editorPhysics.body.GetSpeedOfSound(editorPhysics.atmStaticPressureKpa, editorPhysics.atmDensity); + if (speedOfSound > 0.0) + machNumber = (float)(speed / speedOfSound); + else + machNumber = 0f; + + Vector3 rhsNeutral = Quaternion.AngleAxis(currentDeployAngle, module.baseTransform.rotation * Vector3.right) * module.baseTransform.forward; + float dotNeutral = Vector3.Dot(nVel, rhsNeutral); + float absDotNeutral = Mathf.Abs(dotNeutral); + neutralForce = GetLiftForce(module, this, rhsNeutral, dotNeutral, absDotNeutral) * module.ctrlSurfaceArea; + neutralForce += GetDragForce(module, this, absDotNeutral); + } + + public static void UpdateInstances() + { + foreach (ModuleCtrlSrfExtension moduleExtension in instances.Values) + { + try + { + if (moduleExtension.module.isActiveAndEnabled && !moduleExtension.module.displaceVelocity) + moduleExtension.UpdatePAW(); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + private void UpdatePAW() + { + if (!ActuationToggleDisplayed(module)) + { + if (pawTorqueEnabled) + DisablePAWTorque(); + + return; + } + + ModuleControlSurface_GetPotentialTorque_Prefix(module, out Vector3 pos, out Vector3 neg); + + if (pos == Vector3.zero && neg == Vector3.zero) + { + if (pawTorqueEnabled) + DisablePAWTorque(); + + return; + } + + pawTorqueEnabled = true; + + if (!module.ignorePitch) + SetToggleGuiName(ignorePitchField, $"{autoLOC_6001330_Pitch}: {Math.Round(pos.x, 3):G3} / {-Math.Round(neg.x, 3):G3} kNm"); + else + SetToggleGuiName(ignorePitchField, autoLOC_6001330_Pitch); + + if (!module.ignoreRoll) + SetToggleGuiName(ignoreRollField, $"{autoLOC_6001332_Roll}: {Math.Round(pos.y, 3):G3} / {-Math.Round(neg.y, 3):G3} kNm"); + else + SetToggleGuiName(ignoreRollField, autoLOC_6001332_Roll); + + if (!module.ignoreYaw) + SetToggleGuiName(ignoreYawField, $"{autoLOC_6001331_Yaw}: {Math.Round(pos.z, 3):G3} / {-Math.Round(neg.z, 3):G3} kNm"); + else + SetToggleGuiName(ignoreYawField, autoLOC_6001331_Yaw); + } + + private void DisablePAWTorque() + { + pawTorqueEnabled = false; + SetToggleGuiName(ignorePitchField, autoLOC_6001330_Pitch); + SetToggleGuiName(ignoreRollField, autoLOC_6001332_Roll); + SetToggleGuiName(ignoreYawField, autoLOC_6001331_Yaw); + } + + static bool ActuationToggleDisplayed(ModuleControlSurface module) + { + if (module.part.PartActionWindow.IsNullOrDestroyed() || !module.part.PartActionWindow.isActiveAndEnabled) + return false; + + return true; + } + + private static void SetToggleGuiName(BaseField baseField, string guiName) + { + baseField.guiName = guiName; + + UIPartActionToggle toggle; + + if (baseField.uiControlEditor.partActionItem.IsNotNullOrDestroyed()) + toggle = (UIPartActionToggle)baseField.uiControlEditor.partActionItem; + else if (baseField.uiControlFlight.partActionItem.IsNotNullOrDestroyed()) + toggle = (UIPartActionToggle)baseField.uiControlFlight.partActionItem; + else + return; + + toggle.fieldName.text = guiName; + ((RectTransform)toggle.fieldName.transform).sizeDelta = new Vector2(150f, toggle.fieldName.rectTransform.sizeDelta.y); + } + } + + static void ModuleControlSurface_OnStart_Postfix(ModuleControlSurface __instance) + { + ModuleCtrlSrfExtension.Get(__instance); + } + + static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface __instance, out Vector3 pos, out Vector3 neg) + { + ctrlSrfProfiler.Begin(); + pos = Vector3.zero; + neg = Vector3.zero; + + bool isEditor = HighLogic.LoadedScene == GameScenes.EDITOR; + + if (isEditor) + { + if (__instance.ignorePitch && __instance.ignoreYaw && __instance.ignoreRoll) + { + ctrlSrfProfiler.End(); + return false; + } + } + else + { + if (__instance.Qlift < 1.0 || (__instance.ignorePitch && __instance.ignoreYaw && __instance.ignoreRoll)) + { + ctrlSrfProfiler.End(); + return false; + } + } + + if (__instance.displaceVelocity) + { + if (isEditor) + return false; + + // This case is for handling "propeller blade" control surfaces. Those have a completely different behavior and + // actuation scheme (and why this wasn't implemented as a separate module is beyond my understanding). + // This is the stock GetPotentialTorque() implementation for them, I've no idea how correct it is and just don't + // have the motivation to investigate. + + Vector3 potentialForcePos = __instance.GetPotentialLift(true); + Vector3 potentialForceNeg = __instance.GetPotentialLift(false); + float magnitude = __instance.vesselBladeLiftReference.magnitude; + pos = Vector3.Dot(potentialForcePos, __instance.vesselBladeLiftReference) * __instance.potentialBladeControlTorque / magnitude; + neg = Vector3.Dot(potentialForceNeg, __instance.vesselBladeLiftReference) * __instance.potentialBladeControlTorque / magnitude; + } + else + { + // The stock method doesn't handle correctly the deployed state : + // - It always apply `currentDeployAngle` in the AngleAxis() call, but that field is updated only if `mcs.deploy == true`, and + // it isn't reverted to 0 if deploy changes from true to false, resulting in the deployed angle still being applied after un-deploying. + // - It always substract `baseLiftForce` which is always the non-deployed lift vector, resulting in the positive deflection being twice + // what it should be and the negative deflection being always zero. + + ModuleCtrlSrfExtension moduleExt; + Transform vesselReferenceTransform; + + if (isEditor) + { + if (!EditorPhysics.TryGetAndUpdate(out EditorPhysics editorPhysics) || editorPhysics.atmDensity == 0.0) + { + gimbalProfiler.End(); + return false; + } + + vesselReferenceTransform = editorPhysics.referenceTransform; + moduleExt = ModuleCtrlSrfExtension.Get(__instance); + moduleExt.UpdateEditorState(vesselReferenceTransform, editorPhysics); + } + else + { + vesselReferenceTransform = __instance.vessel.ReferenceTransform; + + gimbalCacheProfiler.Begin(); + + moduleExt = ModuleCtrlSrfExtension.Get(__instance); + if (!moduleExt.IsCacheValid(__instance.vessel.CurrentCoM, vesselReferenceTransform.InverseTransformPoint(__instance.vessel.CurrentCoM))) + { + pos = moduleExt.pos; + neg = moduleExt.neg; + gimbalCacheProfiler.End(); + gimbalProfiler.End(); + return false; + } + gimbalCacheProfiler.End(); + } + + Vector3 potentialForcePos = GetPotentialLiftAndDrag(__instance, moduleExt, moduleExt.currentDeployAngle, true); + Vector3 potentialForceNeg = GetPotentialLiftAndDrag(__instance, moduleExt, moduleExt.currentDeployAngle, false); + + Vector3 partPosition = __instance.part.Rigidbody.worldCenterOfMass - moduleExt.worldCoM; + + Vector3 posTorque = vesselReferenceTransform.InverseTransformDirection(Vector3.Cross(partPosition, potentialForcePos)); + Vector3 negTorque = vesselReferenceTransform.InverseTransformDirection(Vector3.Cross(partPosition, potentialForceNeg)); + + Vector3 neutralTorque = vesselReferenceTransform.InverseTransformDirection(Vector3.Cross(partPosition, moduleExt.neutralForce)); + + posTorque -= neutralTorque; + negTorque -= neutralTorque; + + // At this point, we have raw torque results for two given actuations. However, GetPotentialTorque() is supposed to + // represent the torque produced by pitch/roll/yaw requests. We need to determine which actuation is applied for a pos=(1,1,1) + // and neg=(-1,-1,-1) ctrlState, then swap the raw torque components accordingly. Said otherwise, to take an example, + // we need to answer : does a positive pitch request results in a positive or negative actuation ? + // Additionally, ModuleControlSurface will clamp actuation magnitude depending on the surface orientation, and apply an + // additional (weird) clamping for the deployed state. + + // The following code is essentially derived from ModuleControlSurface.FixedCtrlSurfaceUpdate(), which is the method responsible + // for updating the control surface angle according to the vessel.ctrlState pitch/roll/yaw request. + + float deployAction; + if (__instance.deploy) + { + deployAction = __instance.usesMirrorDeploy + ? ((__instance.deployInvert ? (-1f) : 1f) * (__instance.partDeployInvert ? (-1f) : 1f) * (__instance.mirrorDeploy ? (-1f) : 1f)) + : ((__instance.deployInvert ? (-1f) : 1f) * Mathf.Sign((Quaternion.Inverse(vesselReferenceTransform.rotation) * (__instance.baseTransform.position - moduleExt.worldCoM)).x)); + + deployAction *= -1f; + } + else + { + deployAction = 0f; + } + + Vector3 comRelPos = __instance.baseTransform.InverseTransformPoint(moduleExt.worldCoM); + +#if DEBUG + Vector3 posAction = Vector3.zero; + Vector3 negAction = Vector3.zero; +#endif + + if (!__instance.ignorePitch) + { + Vector3 pitchVector = vesselReferenceTransform.rotation * new Vector3(1f, 0f, 0f); + float pitchActionPos = Vector3.Dot(pitchVector, __instance.baseTransform.rotation * Vector3.right); + if (comRelPos.y < 0f) + pitchActionPos = -pitchActionPos; + + float pitchActionNeg = -pitchActionPos; + + if (__instance.deploy) + { + pitchActionPos = Mathf.Clamp(pitchActionPos + deployAction, -1.5f, 1.5f) - deployAction; + pitchActionNeg = Mathf.Clamp(pitchActionNeg + deployAction, -1.5f, 1.5f) - deployAction; + } + + // I hope I got this right. TBH, this was mostly a trial and error job. + // - the control surface actuation direction depends on sign of the action + // - then we clamp and inverse the raw torque by the action + // note that this direct scaling is a rough approximation, as the torque output vs actuation function isn't linear, + // but the whole thing is computationally intensive (and complex) enough already... + if (pitchActionPos > 0f) + { + pos.x = negTorque.x * pitchActionPos; + neg.x = posTorque.x * pitchActionNeg; + } + else + { + pos.x = posTorque.x * pitchActionNeg; + neg.x = negTorque.x * pitchActionPos; + } +#if DEBUG + posAction.x = pitchActionPos; + negAction.x = pitchActionNeg; +#endif + } + + if (!__instance.ignoreYaw) + { + Vector3 yawVector = vesselReferenceTransform.rotation * new Vector3(0f, 0f, 1f); + float yawActionPos = Vector3.Dot(yawVector, __instance.baseTransform.rotation * Vector3.right); + if (comRelPos.y < 0f) + yawActionPos = -yawActionPos; + + float yawActionNeg = -yawActionPos; + + if (__instance.deploy) + { + yawActionPos = Mathf.Clamp(yawActionPos + deployAction, -1.5f, 1.5f) - deployAction; + yawActionNeg = Mathf.Clamp(yawActionNeg + deployAction, -1.5f, 1.5f) - deployAction; + } + + if (yawActionPos > 0f) + { + pos.z = negTorque.z * yawActionPos; + neg.z = posTorque.z * yawActionNeg; + } + else + { + pos.z = posTorque.z * yawActionNeg; + neg.z = negTorque.z * yawActionPos; + } +#if DEBUG + posAction.z = yawActionPos; + negAction.z = yawActionNeg; +#endif + } + + if (!__instance.ignoreRoll) + { + // optimization note : we could get rollAction by doing `rollAction = mcs.roll / ctrlStateRoll` where + // ctrlStateRoll is the `vessel.ctrlState.roll` value from the last fixedUpdate(). + // But implementing that would be a mess, and the value would be slightly wrong due to being a frame outdated + // (altough this won't matter much, the overall GetPotentialTorque() implementation already rely on a bunch of + // one-frame outdated values for performance optimization reasons) + + Vector3 rhs = new Vector3(comRelPos.x, 0f, comRelPos.z); + + float rollActionPos = Vector3.Dot(Vector3.right, rhs) + * (1f - (Mathf.Abs(Vector3.Dot(rhs.normalized, Quaternion.Inverse(__instance.baseTransform.rotation) * vesselReferenceTransform.up)) * 0.5f + 0.5f)) + * Mathf.Sign(Vector3.Dot(__instance.baseTransform.up, vesselReferenceTransform.up)) + * Mathf.Sign(__instance.ctrlSurfaceRange) + * -1f; + + rollActionPos = Mathf.Clamp(rollActionPos, -1f, 1f); + + float rollActionNeg = -rollActionPos; + + if (__instance.deploy) + { + rollActionPos = Mathf.Clamp(rollActionPos + deployAction, -1.5f, 1.5f) - deployAction; + rollActionNeg = Mathf.Clamp(rollActionNeg + deployAction, -1.5f, 1.5f) - deployAction; + } + + if (rollActionPos > 0f) + { + pos.y = negTorque.y * rollActionPos; + neg.y = posTorque.y * rollActionNeg; + } + else + { + pos.y = posTorque.y * rollActionNeg; + neg.y = negTorque.y * rollActionPos; + } +#if DEBUG + posAction.y = rollActionPos; + negAction.y = rollActionNeg; +#endif + } + +#if DEBUG + TorqueUIModule ui = __instance.part.FindModuleImplementing(); + if (ui != null) + { + ui.pos = pos; + ui.neg = neg; + + ui.Fields["spos"].guiActive = true; + ui.spos = posTorque; + + ui.Fields["sneg"].guiActive = true; + ui.sneg = negTorque; + + ui.Fields["posAction"].guiActive = true; + ui.posAction = posAction; + + ui.Fields["negAction"].guiActive = true; + ui.negAction = negAction; + } + + moduleExt.pos = pos; + moduleExt.neg = neg; +#endif + } + + ctrlSrfProfiler.End(); + return false; + } + + private static Vector3 GetPotentialLiftAndDrag(ModuleControlSurface mcs, ModuleCtrlSrfExtension moduleExt, float deployAngle, bool positiveDeflection) + { + float deflectionDir = positiveDeflection ? 1f : -1f; + float angle = deployAngle + (deflectionDir * mcs.ctrlSurfaceRange * mcs.authorityLimiter * 0.01f); + Vector3 rhs = Quaternion.AngleAxis(angle, mcs.baseTransform.rotation * Vector3.right) * mcs.baseTransform.forward; + float dot = Vector3.Dot(moduleExt.nVel, rhs); + float absDot = Mathf.Abs(dot); + Vector3 result = GetLiftForce(mcs, moduleExt, rhs, dot, absDot) * mcs.ctrlSurfaceArea; + result += GetDragForce(mcs, moduleExt, absDot); + return result; + } + + private static Vector3 GetLiftForce(ModuleControlSurface mcs, ModuleCtrlSrfExtension moduleExt, Vector3 liftVector, float liftDot, float absDot) + { + if (mcs.nodeEnabled && mcs.attachNode.attachedPart != null) + { + return Vector3.zero; + } + float liftScalar = Mathf.Sign(liftDot) * mcs.liftCurve.Evaluate(absDot) * mcs.liftMachCurve.Evaluate(moduleExt.machNumber); + liftScalar *= mcs.deflectionLiftCoeff; + if (liftScalar != 0f && !float.IsNaN(liftScalar)) + { + liftScalar = (float)(moduleExt.QLift * PhysicsGlobals.LiftMultiplier * liftScalar); + if (mcs.perpendicularOnly) + { + Vector3 vector = -liftVector * liftScalar; + vector = Vector3.ProjectOnPlane(vector, -moduleExt.nVel); + return vector; + } + return -liftVector * liftScalar; + } + return Vector3.zero; + } + + private static Vector3 GetDragForce(ModuleControlSurface mcs, ModuleCtrlSrfExtension moduleExt, float absDot) + { + if (!mcs.useInternalDragModel || (mcs.nodeEnabled && mcs.attachNode.attachedPart.IsNotNullOrDestroyed())) + return Vector3.zero; + + float dragScalar = mcs.dragCurve.Evaluate(absDot) * mcs.dragMachCurve.Evaluate(moduleExt.machNumber); + dragScalar *= mcs.deflectionLiftCoeff; + if (dragScalar != 0f && !float.IsNaN(dragScalar)) + { + dragScalar = (float)moduleExt.QDrag * dragScalar * PhysicsGlobals.LiftDragMultiplier; + return -moduleExt.nVel * dragScalar * mcs.ctrlSurfaceArea; + } + + return Vector3.zero; + } + + #endregion + + #region ModuleGimbal + + private class ModuleGimbalExtension + { + private static Dictionary instances = new Dictionary(); + + public static ModuleGimbalExtension Get(ModuleGimbal module) + { + if (instances.TryGetValue(module, out ModuleGimbalExtension gimbalExt)) + return gimbalExt; + + return new ModuleGimbalExtension(module); + } + + private ModuleGimbal module; + + public Vector3 pos; + public Vector3 neg; + + private Vector3 lastLocalCoM; + private float lastThrustForce; + private float lastTime; + private float lastGimbalLimiter; + private bool lastPitch; + private bool lastRoll; + private bool lastYaw; + + private bool pawTorqueEnabled; + private BaseField enablePitchField; + private BaseField enableRollField; + private BaseField enableYawField; + + public ModuleGimbalExtension(ModuleGimbal module) + { + this.module = module; + + enablePitchField = module.Fields[nameof(ModuleGimbal.enablePitch)]; + enableRollField = module.Fields[nameof(ModuleGimbal.enableRoll)]; + enableYawField = module.Fields[nameof(ModuleGimbal.enableYaw)]; + + module.part.OnJustAboutToBeDestroyed += OnDestroy; + instances.Add(module, this); + } + + public void OnDestroy() + { + module.part.OnJustAboutToBeDestroyed -= OnDestroy; + instances.Remove(module); + module = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UpdateLastState(Vector3 localCoM, float thrustForce) + { + lastLocalCoM = localCoM; + lastThrustForce = thrustForce; + lastTime = Time.fixedTime; + lastGimbalLimiter = module.gimbalLimiter; + lastPitch = module.enablePitch; + lastRoll = module.enableRoll; + lastYaw = module.enableYaw; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool UpdateRequired(Vector3 localCoM, float thrustForce) + { + if (Math.Abs(lastThrustForce - thrustForce) > 1.0 + || (lastLocalCoM - localCoM).sqrMagnitude > 0.1f * 0.1f + || lastTime + Random.Range(0.75f, 1.25f) < Time.fixedTime + || lastGimbalLimiter != module.gimbalLimiter + || lastPitch != module.enablePitch || lastRoll != module.enableRoll || lastYaw != module.enableYaw) + { + UpdateLastState(localCoM, thrustForce); + return true; + } + + return false; + } + + public static void UpdateInstances() + { + foreach (ModuleGimbalExtension moduleExtension in instances.Values) + { + try + { + if (moduleExtension.module.isActiveAndEnabled) + moduleExtension.UpdatePAW(); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + + private void UpdatePAW() + { + if (!ActuationToggleDisplayed(module)) + { + if (pawTorqueEnabled) + DisablePAWTorque(); + + return; + } + + ModuleGimbal_GetPotentialTorque_Prefix(module, out Vector3 pos, out Vector3 neg); + + if (pos == Vector3.zero && neg == Vector3.zero) + { + if (pawTorqueEnabled) + DisablePAWTorque(); + + return; + } + + pawTorqueEnabled = true; + + if (module.enablePitch) + SetToggleGuiName(enablePitchField, $"{autoLOC_6001330_Pitch}: {Math.Round(pos.x, 3):G3} / {-Math.Round(neg.x, 3):G3} kNm"); + else + SetToggleGuiName(enablePitchField, autoLOC_6001330_Pitch); + + if (module.enableRoll) + SetToggleGuiName(enableRollField, $"{autoLOC_6001332_Roll}: {Math.Round(pos.y, 3):G3} / {-Math.Round(neg.y, 3):G3} kNm"); + else + SetToggleGuiName(enableRollField, autoLOC_6001332_Roll); + + if (module.enableYaw) + SetToggleGuiName(enableYawField, $"{autoLOC_6001331_Yaw}: {Math.Round(pos.z, 3):G3} / {-Math.Round(neg.z, 3):G3} kNm"); + else + SetToggleGuiName(enableYawField, autoLOC_6001331_Yaw); + } + + private void DisablePAWTorque() + { + pawTorqueEnabled = false; + SetToggleGuiName(enablePitchField, autoLOC_6001330_Pitch); + SetToggleGuiName(enableRollField, autoLOC_6001332_Roll); + SetToggleGuiName(enableYawField, autoLOC_6001331_Yaw); + } + + static bool ActuationToggleDisplayed(ModuleGimbal module) + { + if (!module.showToggles || !module.currentShowToggles) + return false; + + if (module.part.PartActionWindow.IsNullOrDestroyed() || !module.part.PartActionWindow.isActiveAndEnabled) + return false; + + return true; + } + + private static void SetToggleGuiName(BaseField baseField, string guiName) + { + baseField.guiName = guiName; + + UIPartActionToggle toggle; + + if (baseField.uiControlEditor.partActionItem.IsNotNullOrDestroyed()) + toggle = (UIPartActionToggle)baseField.uiControlEditor.partActionItem; + else if (baseField.uiControlFlight.partActionItem.IsNotNullOrDestroyed()) + toggle = (UIPartActionToggle)baseField.uiControlFlight.partActionItem; + else + return; + + toggle.fieldName.text = guiName; + ((RectTransform)toggle.fieldName.transform).sizeDelta = new Vector2(150f, toggle.fieldName.rectTransform.sizeDelta.y); + } + } + + static void ModuleGimbal_OnStart_Postfix(ModuleGimbal __instance) + { + ModuleGimbalExtension.Get(__instance); + } + + static bool ModuleGimbal_GetPotentialTorque_Prefix(ModuleGimbal __instance, out Vector3 pos, out Vector3 neg) + { + gimbalProfiler.Begin(); + + pos = Vector3.zero; + neg = Vector3.zero; + + bool isEditor = HighLogic.LoadedScene == GameScenes.EDITOR; + + if (isEditor) + { + if (__instance.gimbalLock + || !__instance.moduleIsEnabled + || (!__instance.enablePitch && !__instance.enableRoll && !__instance.enableYaw)) + { + gimbalProfiler.End(); + return false; + } + } + else + { + if (__instance.gimbalLock + || !__instance.gimbalActive + || !__instance.moduleIsEnabled + || (!__instance.enablePitch && !__instance.enableRoll && !__instance.enableYaw)) + { + gimbalProfiler.End(); + return false; + } + } + + // ensure we don't create a cache entry when the part is destroyed + if (__instance.part.State == PartStates.DEAD) + { + gimbalProfiler.End(); + return false; + } + + if (__instance.engineMultsList == null) + __instance.CreateEngineList(); + + ModuleGimbalExtension gimbalCache; + Vector3 worldCoM; + Vector3 localCoM; + Transform vesselReferenceTransform; + int transformIndex; + EditorPhysics editorPhysics; + + if (isEditor) + { + if (!EditorPhysics.TryGetAndUpdate(out editorPhysics)) + { + gimbalProfiler.End(); + return false; + } + + worldCoM = editorPhysics.CoM; + vesselReferenceTransform = editorPhysics.referenceTransform; + localCoM = vesselReferenceTransform.InverseTransformPoint(worldCoM); + gimbalCache = ModuleGimbalExtension.Get(__instance); + } + else + { + editorPhysics = null; + + float totalThrust = 0f; + transformIndex = __instance.gimbalTransforms.Count; + while (transformIndex-- > 0) + { + int engineIndex = __instance.engineMultsList[transformIndex].Count; + while (engineIndex-- > 0) + totalThrust += __instance.engineMultsList[transformIndex][engineIndex].Key.finalThrust; + } + + if (totalThrust == 0f) + { + gimbalProfiler.End(); + return false; + } + + worldCoM = __instance.vessel.CurrentCoM; + vesselReferenceTransform = __instance.vessel.ReferenceTransform; + localCoM = vesselReferenceTransform.InverseTransformPoint(worldCoM); + + gimbalCacheProfiler.Begin(); + + gimbalCache = ModuleGimbalExtension.Get(__instance); + if (!gimbalCache.UpdateRequired(localCoM, totalThrust)) + { + pos = gimbalCache.pos; + neg = gimbalCache.neg; + gimbalCacheProfiler.End(); + gimbalProfiler.End(); + return false; + } + gimbalCacheProfiler.End(); + } + + transformIndex = __instance.gimbalTransforms.Count; + while (transformIndex-- > 0) + { + List> engines = __instance.engineMultsList[transformIndex]; + + Transform gimbalTransform = __instance.gimbalTransforms[transformIndex]; + + // this is the neutral gimbalTransform.localRotation + Quaternion neutralLocalRot = __instance.initRots[transformIndex]; + Quaternion neutralWorldRot = gimbalTransform.parent.rotation * neutralLocalRot; + // get the rotation between the current gimbal rotation and the neutral rotation + Quaternion gimbalWorldRotToNeutral = neutralWorldRot * Quaternion.Inverse(gimbalTransform.rotation); + + Vector3 neutralTorque = Vector3.zero; + Vector3 pitchPosTorque = Vector3.zero; + Vector3 pitchNegTorque = Vector3.zero; + Vector3 rollPosTorque = Vector3.zero; + Vector3 rollNegTorque = Vector3.zero; + Vector3 yawPosTorque = Vector3.zero; + Vector3 yawNegTorque = Vector3.zero; + + Vector3 controlPoint = vesselReferenceTransform.InverseTransformPoint(gimbalTransform.position); + + bool inversedControl = localCoM.y < controlPoint.y; + + Quaternion pitchPosActuation; + Quaternion pitchNegActuation; + if (__instance.enablePitch) + { + pitchPosActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.right, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + pitchNegActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.left, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + } + else + { + pitchPosActuation = Quaternion.identity; + pitchNegActuation = Quaternion.identity; + } + + Quaternion rollPosActuation; + Quaternion rollNegActuation; + if (__instance.enableRoll) + { + rollPosActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.up, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + rollNegActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.down, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + } + else + { + rollPosActuation = Quaternion.identity; + rollNegActuation = Quaternion.identity; + } + + Quaternion yawPosActuation; + Quaternion yawNegActuation; + if (__instance.enableYaw) + { + yawPosActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.forward, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + yawNegActuation = GetGimbalWorldRotation(__instance, vesselReferenceTransform, gimbalTransform, Vector3.back, controlPoint, inversedControl, neutralLocalRot, neutralWorldRot); + } + else + { + yawPosActuation = Quaternion.identity; + yawNegActuation = Quaternion.identity; + } + + int engineIndex = engines.Count; + while (engineIndex-- > 0) + { + KeyValuePair engineThrustMultiplier = engines[engineIndex]; + + ModuleEngines engine = engineThrustMultiplier.Key; + float thrustMultiplier = engineThrustMultiplier.Value; + + float thrustMagnitude; + if (isEditor) + { + if (editorPhysics.atmStaticPressure == 0f) + thrustMagnitude = engine.MaxThrustOutputVac(true); + else + thrustMagnitude = engine.MaxThrustOutputAtm(true, true, (float)editorPhysics.atmStaticPressure, editorPhysics.atmTemperature, editorPhysics.atmDensity); + } + else + { + thrustMagnitude = engine.finalThrust; + } + + thrustMagnitude *= thrustMultiplier; + + if (thrustMagnitude <= 0f) + continue; + + int thrustTransformIndex = engine.thrustTransforms.Count; + while (thrustTransformIndex-- > 0) + { + Transform thrustTransform = engine.thrustTransforms[thrustTransformIndex]; + + // To get the "neutral" transform position, we need to walk back the transform hierarchy to correct for the current gimbal + // rotation induced thrustTransform position offset. It's not critical to do it (see below as for why), but it would be weird + // to have the end results varying slightly depending on the current actuation. + // But note that when getting the actuated forces, we don't use the modified thrustTransform position. In most cases, the + // actuation induced position shift of the thrustTransform won't matter much, since the gimbal pivot - thrustTransform distance + // is usally tiny compared to the CoM-thrustTransform distance. + Vector3 thrustTransformPosition = gimbalTransform.position + (gimbalWorldRotToNeutral * (thrustTransform.position - gimbalTransform.position)); + Vector3 trustPosFromCoM = thrustTransformPosition - worldCoM; + + // get the neutral thrust force by removing the thrustTransform current actuation induced rotation + Vector3 neutralThrustForce = gimbalWorldRotToNeutral * (thrustTransform.forward * thrustMagnitude); + + // get the "natural" torque induced by the engine thrust, in world space + neutralTorque += Vector3.Cross(trustPosFromCoM, neutralThrustForce); + + if (__instance.enablePitch) + { + pitchPosTorque += Vector3.Cross(trustPosFromCoM, pitchPosActuation * neutralThrustForce); + pitchNegTorque += Vector3.Cross(trustPosFromCoM, pitchNegActuation * neutralThrustForce); + } + + if (__instance.enableRoll) + { + rollPosTorque += Vector3.Cross(trustPosFromCoM, rollPosActuation * neutralThrustForce); + rollNegTorque += Vector3.Cross(trustPosFromCoM, rollNegActuation * neutralThrustForce); + } + + if (__instance.enableYaw) + { + yawPosTorque += Vector3.Cross(trustPosFromCoM, yawPosActuation * neutralThrustForce); + yawNegTorque += Vector3.Cross(trustPosFromCoM, yawNegActuation * neutralThrustForce); + } + } + } + + neutralTorque = vesselReferenceTransform.InverseTransformDirection(neutralTorque); + + if (__instance.enablePitch) + { + pitchPosTorque = vesselReferenceTransform.InverseTransformDirection(pitchPosTorque); + pitchNegTorque = vesselReferenceTransform.InverseTransformDirection(pitchNegTorque); + pos.x += pitchPosTorque.x - neutralTorque.x; + neg.x -= pitchNegTorque.x - neutralTorque.x; + } + + if (__instance.enableRoll) + { + rollPosTorque = vesselReferenceTransform.InverseTransformDirection(rollPosTorque); + rollNegTorque = vesselReferenceTransform.InverseTransformDirection(rollNegTorque); + pos.y += rollPosTorque.y - neutralTorque.y; + neg.y -= rollNegTorque.y - neutralTorque.y; + } + + if (__instance.enableYaw) + { + yawPosTorque = vesselReferenceTransform.InverseTransformDirection(yawPosTorque); + yawNegTorque = vesselReferenceTransform.InverseTransformDirection(yawNegTorque); + pos.z += yawPosTorque.z - neutralTorque.z; + neg.z -= yawNegTorque.z - neutralTorque.z; + } + } + + gimbalCache.pos = pos; + gimbalCache.neg = neg; + + gimbalProfiler.End(); +#if DEBUG + TorqueUIModule ui = __instance.part.FindModuleImplementing(); + if (ui != null) + { + ui.pos = pos; + ui.neg = neg; + } +#endif + return false; + } + + static Quaternion GetGimbalWorldRotation(ModuleGimbal mg, Transform referenceTransform, Transform gimbalTransform, Vector3 ctrlState, Vector3 controlPoint, bool inversedControl, Quaternion neutralLocalRot, Quaternion neutralWorldRot) + { + if (inversedControl) + { + ctrlState.x *= -1f; + ctrlState.z *= -1f; + } + + if (ctrlState.y != 0f && mg.enableRoll) + { + if (controlPoint.x > mg.minRollOffset) + ctrlState.x += ctrlState.y; + else if (controlPoint.x < -mg.minRollOffset) + ctrlState.x -= ctrlState.y; + + if (controlPoint.z > mg.minRollOffset) + ctrlState.z += ctrlState.y; + else if (controlPoint.z < -mg.minRollOffset) + ctrlState.z -= ctrlState.y; + } + + // Stock does gimbalTransform.InverseTransformDirection(), resulting in the available torque varying with the current gimbal actuation... + // To work around that, we call InverseTransformDirection() on the parent, then apply the neutral rotation. + Vector3 localActuation = + Quaternion.Inverse(neutralLocalRot) + * gimbalTransform.parent.InverseTransformDirection(referenceTransform.TransformDirection(ctrlState)); + + // get actuation angles + localActuation.x = Mathf.Clamp(localActuation.x, -1f, 1f) * ((localActuation.x > 0f) ? mg.gimbalRangeXP : mg.gimbalRangeXN) * mg.gimbalLimiter * 0.01f; + localActuation.y = Mathf.Clamp(localActuation.y, -1f, 1f) * ((localActuation.y > 0f) ? mg.gimbalRangeYP : mg.gimbalRangeYN) * mg.gimbalLimiter * 0.01f; + + // get local rotation + Quaternion gimbalRotation = + neutralLocalRot + * Quaternion.AngleAxis(localActuation.x, mg.xMult * Vector3.right) + * Quaternion.AngleAxis(localActuation.y, mg.yMult * (mg.flipYZ ? Vector3.forward : Vector3.up)); + + // transform in world space + gimbalRotation = (gimbalTransform.parent.rotation * gimbalRotation) * Quaternion.Inverse(neutralWorldRot); + + return gimbalRotation; + } + + #endregion + } + +#if DEBUG + public class TorqueUIModule : PartModule + { + [KSPField(guiActive = true, guiFormat = "F1")] + public Vector3 pos; + [KSPField(guiActive = true, guiFormat = "F1")] + public Vector3 neg; + + // control surface debug stuff + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 spos; + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 sneg; + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 posAction; + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 negAction; + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 actionV; + + // gimbal debug stuff + [KSPField(guiActive = false, guiFormat = "F1")] + public Vector3 gimbalNeutralTorque; + } +#endif +} diff --git a/KSPCommunityFixes/Internal/EditorPhysics.cs b/KSPCommunityFixes/Internal/EditorPhysics.cs new file mode 100644 index 0000000..29b6e48 --- /dev/null +++ b/KSPCommunityFixes/Internal/EditorPhysics.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace KSPCommunityFixes +{ + public class EditorPhysics : BasePatch + { + protected override bool IgnoreConfig => true; + + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + instance = this; + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(DeltaVAppSituation), nameof(DeltaVAppSituation.UpdatePressureDisplay)), + this)); + + GameEvents.onEditorShipModified.Add(OnEditorShipModified); + GameEvents.onDeltaVAppAtmosphereChanged.Add(OnDeltaVAppAtmosphereChanged); + GameEvents.OnControlPointChanged.Add(OnControlPointChanged); + GameEvents.onGameSceneLoadRequested.Add(OnGameSceneLoadRequested); + } + + private static EditorPhysics instance; + public static bool TryGetAndUpdate(out EditorPhysics updatedInstance) + { + instance.Update(); + if (instance.isValid) + { + updatedInstance = instance; + return true; + } + + updatedInstance = null; + return false; + } + + private bool isValid; + + public Vector3 CoM => EditorMarker_CoM.CraftCoM; + public Transform referenceTransform; + public Part referencePart; + public int referencePartShipIndex; + + public CelestialBody body; + public double atmStaticPressureKpa; + public double atmStaticPressure; + public double atmDensity; + public double atmTemperature; + + public int lastShipModificationFrame = int.MaxValue; + private int lastShipStatsUpdateFrame; + + private void Update() + { + if (lastShipStatsUpdateFrame < lastShipModificationFrame) + { + lastShipStatsUpdateFrame = lastShipModificationFrame; + EditorUpdateShipStats(); + } + } + + private void EditorUpdateShipStats() + { + if (EditorLogic.fetch.ship == null || EditorLogic.fetch.ship.parts.Count == 0) + { + isValid = false; + return; + } + + if (DeltaVGlobals.fetch != null && DeltaVGlobals.ready && DeltaVGlobals.DeltaVAppValues.body != null) + { + body = DeltaVGlobals.DeltaVAppValues.body; + + if (!DeltaVGlobals.DeltaVAppValues.body.atmosphere || DeltaVGlobals.DeltaVAppValues.situation == DeltaVSituationOptions.Vaccum) + { + + atmStaticPressureKpa = 0.0; + atmStaticPressure = 0.0; + atmTemperature = 0.0; + atmDensity = 0.0; + } + else + { + if (DeltaVGlobals.DeltaVAppValues.situation == DeltaVSituationOptions.Altitude) + { + atmTemperature = body.GetFullTemperature(DeltaVGlobals.DeltaVAppValues.altitude, 0.0); + atmStaticPressureKpa = body.GetPressure(DeltaVGlobals.DeltaVAppValues.altitude); + } + else + { + atmTemperature = body.GetFullTemperature(0.0, 0.0); + atmStaticPressureKpa = body.GetPressure(0.0); + } + + atmStaticPressure = atmStaticPressureKpa * 0.0098692326671601278; + atmDensity = DeltaVGlobals.DeltaVAppValues.atmDensity; + } + } + else + { + atmStaticPressureKpa = 0.0; + atmStaticPressure = 0.0; + atmTemperature = 0.0; + atmDensity = 0.0; + } + + if (referenceTransform.IsNullOrDestroyed() || EditorLogic.fetch.ship.Parts.IndexOf(referencePart) != referencePartShipIndex) + { + if (!GetFirstReferenceTransform(EditorLogic.RootPart, ref referenceTransform, ref referencePart)) + { + referencePart = EditorLogic.RootPart; + referenceTransform = EditorLogic.RootPart.referenceTransform; + } + + referencePartShipIndex = EditorLogic.fetch.ship.Parts.IndexOf(referencePart); + } + + if (referenceTransform.IsNullOrDestroyed()) + { + isValid = false; + return; + } + + bool GetFirstReferenceTransform(Part part, ref Transform referenceTransform, ref Part referencePart) + { + if (part.isControlSource != Vessel.ControlLevel.NONE) + { + ModuleCommand mc = part.FindModuleImplementing(); + if (mc != null && mc.controlPoints != null && mc.controlPoints.TryGetValue(mc.activeControlPointName, out ControlPoint ctrlPoint)) + referenceTransform = ctrlPoint.transform; + else + referenceTransform = part.referenceTransform; + + referencePart = part; + return true; + } + + int childIdx = part.children.Count; + while (childIdx-- > 0) + { + if (GetFirstReferenceTransform(part.children[childIdx], ref referenceTransform, ref referencePart)) + return true; + } + + return false; + } + + EditorMarker_CoM comMarker = EditorVesselOverlays.fetch.CoMmarker; + if (comMarker == null) + { + isValid = false; + return; + } + + if (!comMarker.isActiveAndEnabled) + { + comMarker.rootPart = EditorLogic.RootPart; + comMarker.UpdatePosition(); + } + + isValid = true; + } + + private void OnGameSceneLoadRequested(GameScenes data) + { + referenceTransform = null; + referencePart = null; + referencePartShipIndex = -1; + atmStaticPressure = 0f; + lastShipModificationFrame = 0; + lastShipStatsUpdateFrame = 0; + } + + private void OnControlPointChanged(Part part, ControlPoint controlPoint) + { + if (HighLogic.LoadedScene != GameScenes.EDITOR || EditorLogic.fetch.ship == null) + return; + + int partIndex = EditorLogic.fetch.ship.Parts.IndexOf(part); + if (partIndex < 0) + return; + + lastShipModificationFrame = Time.frameCount; + referenceTransform = controlPoint.transform; + referencePart = part; + referencePartShipIndex = partIndex; + } + + private void OnDeltaVAppAtmosphereChanged(DeltaVSituationOptions data) + { + lastShipModificationFrame = Time.frameCount; + } + + private void OnEditorShipModified(ShipConstruct ship) + { + if (ship != EditorLogic.fetch.ship || ship.parts.Count == 0) + return; + + lastShipModificationFrame = Time.frameCount; + } + + // OnDeltaVAppAtmosphereChanged isn't fired when the altitude is modified, so implement our own event + static void DeltaVAppSituation_UpdatePressureDisplay_Postfix() + { + instance.lastShipModificationFrame = Time.frameCount; + } + } +} \ No newline at end of file diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index d95b0c5..f741ec2 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -99,14 +99,17 @@ + + + @@ -143,10 +146,13 @@ + + + diff --git a/KSPCommunityFixes/Modding/BaseFieldListUseFieldHost.cs b/KSPCommunityFixes/Modding/BaseFieldListUseFieldHost.cs new file mode 100644 index 0000000..171d05e --- /dev/null +++ b/KSPCommunityFixes/Modding/BaseFieldListUseFieldHost.cs @@ -0,0 +1,210 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Reflection; + +/* +The purpose of this patch if to allow BaseField and associated features (PAW controls, persistence, etc) to work when +a custom BaseField is added to a BaseFieldList (ie, a Part or PartModule) with a host instance other than the BaseFieldList +owner. This allow to dynamically add fields defined in another class to a Part or PartModule and to benefit from all the +associated KSP sugar : +- PAW UI controls +- Value and symmetry events +- Automatic persistence on the Part/PartModule hosting the BaseFieldList + +The whole thing seems actually designed with such a scenario in mind, but for some reason some BaseField and BaseFieldList +methods are using the BaseFieldList.host instance instead of the BaseField.host instance (as for why BaseFieldList has a +"host" at all, I've no idea and this seems to be a design oversight). There is little to no consistency in which host +reference is used, they are even sometimes mixed in the same method. For example, BaseFieldList.Load() uses BaseFieldList.host +in its main body, then calls BaseFieldList.SetOriginalValue() which is relying on BaseField.host. + +Changing every place where a `host` reference is acquired to ensure the BaseField.host reference is used allow to use a custom +host instance, and shouldn't result in any behavior change. This being said, the stock code can theoretically allow a plugin +to instantiate a custom BaseField with a null host and have it kinda functional if that field is only used to SetValue() / +Getvalue() and as long as the field isn't persistent and doesn't have any associated UI_Control. This feels like an extremly +improbable scenario, so this is probably fine. +*/ + +namespace KSPCommunityFixes.Modding +{ + [PatchPriority(Order = 0)] + class BaseFieldListUseFieldHost : BasePatch + { + private static AccessTools.FieldRef> BaseField_OnValueModified_FieldRef; + + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + + BaseField_OnValueModified_FieldRef = AccessTools.FieldRefAccess>(typeof(BaseField), nameof(BaseField.OnValueModified)); + if (BaseField_OnValueModified_FieldRef == null) + throw new MissingFieldException($"BaseFieldListUseFieldHost patch could not find the BaseField.OnValueModified event backing field"); + + MethodInfo BaseField_GetValue_MethodInfo = null; + foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(BaseField))) + { + if (declaredMethod.Name == nameof(BaseField.GetValue) && !declaredMethod.IsGenericMethod) + { + BaseField_GetValue_MethodInfo = declaredMethod; + break; + } + } + if (BaseField_GetValue_MethodInfo == null) + throw new MissingMethodException($"BaseFieldListUseFieldHost patch could not find the BaseField.GetValue() method"); + + // Note : fortunately, we don't need to patch the generic BaseField.GetValue(object host) method because it calls + // the non-generic GetValue method + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + BaseField_GetValue_MethodInfo, + this, nameof(BaseField_GetValue_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(BaseField), nameof(BaseField.SetValue)), + this, nameof(BaseField_SetValue_Prefix))); + + + // BaseField.Read() is a public method called from : + // - BaseFieldList.Load() + // - BaseFieldList.ReadValue() (2 overloads) + // The method is really tiny so there is a potential inlining risk (doesn't happen in my tests, but this stuff can be platform + // dependent). It's only really critical to have BaseFieldList.Load() being patched, the ReadValue() methods are unused in the + // stock codebase, and it is doubtfull anybody would ever call them. + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(BaseField), nameof(BaseField.Read)), + this)); + + // We also patch BaseFieldList.Load() because : + // - Of the above mentioned inlining risk + // - Because it pass the (arguably wrong) host reference to the UI_Control.Load() method, and even though none + // of the various overloads make use of that argument we might want to be consistent. This being said, in the context + // of a UI_Control, it might be expected that the "host" should be the host of the control (ie, a Part or PartModule), + // and not the host of the backing BaseField. In the end, it is doubtful anybody as ever relied on this unused-in-stock + // unclear-what-this-is stuff. Not to mention I'm not even sure anybody has ever defined a custom UI_Control (B9PS maybe ?), + // since this isn't something that can be done out of the box without some major hacking around. + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(BaseFieldList), nameof(BaseFieldList.Load)), + this)); + } + + static bool BaseField_GetValue_Prefix(BaseField __instance, out object __result) + { + try + { + // uses the field host reference instead of the reference passed as a parameter + // in case the field host reference is null, let the call throw and call the original method + // In theory, this should never happen unless a plugin is doing very sketchy like instantiating + // a BaseField manually with a null host. + __result = __instance._fieldInfo.GetValue(__instance._host); + return false; + } + catch + { + __result = null; + return true; + } + } + + static bool BaseField_SetValue_Prefix(BaseField __instance, object newValue, out bool __result) + { + try + { + __instance._fieldInfo.SetValue(__instance._host, newValue); + + // Note : since BaseField.OnValueModified is a "field-like event", it is relying on a compiler-generated + // private backing field, and the public event "synctatic suger" member can only be invoked from the declaring + // class, so we can't directly call "__instance.OnValueModified()" here. Additionally, the compiler has the extremly + // bad taste to name the backing field "OnValueModified" too, resulting in an "ambiguous reference" if we try to use + // it through the publicized assembly. So we have to resort to creating a FieldRef open delegate for that backing field. + BaseField_OnValueModified_FieldRef(__instance)?.Invoke(newValue); + __result = true; + return false; + } + catch + { + __result = false; + return true; + } + } + + static bool BaseField_Read_Prefix(BaseField __instance, string value) + { + BaseField.ReadPvt(__instance._fieldInfo, value, __instance._host); + return false; + } + + static bool BaseFieldList_Load_Prefix(BaseFieldList __instance, ConfigNode node) + { + for (int i = 0; i < node.values.Count; i++) + { + ConfigNode.Value value = node.values[i]; + BaseField baseField = __instance[value.name]; + if (baseField == null || baseField.hasInterface || baseField.uiControlOnly) + { + continue; + } + + // The original code calls BaseField.Read() here. We bypass it to avoid + // any inlining risk and call directly the underlying static method. + BaseField.ReadPvt(baseField._fieldInfo, value.value, baseField._host); + + if (baseField.uiControlFlight.GetType() != typeof(UI_Label)) + { + ConfigNode node2 = node.GetNode(value.name + "_UIFlight"); + if (node2 != null) + { + baseField.uiControlFlight.Load(node2, baseField._host); + } + } + else if (baseField.uiControlEditor.GetType() != typeof(UI_Label)) + { + ConfigNode node3 = node.GetNode(value.name + "_UIEditor"); + if (node3 != null) + { + baseField.uiControlEditor.Load(node3, baseField._host); + } + } + } + for (int j = 0; j < node.nodes.Count; j++) + { + ConfigNode configNode = node.nodes[j]; + BaseField baseField2 = __instance[configNode.name]; + if (baseField2 == null || !baseField2.hasInterface || baseField2.uiControlOnly) + { + continue; + } + + object value2 = baseField2.GetValue(baseField2._host); + if (value2 == null) + { + continue; + } + (value2 as IConfigNode)?.Load(configNode); + if (baseField2.uiControlFlight.GetType() != typeof(UI_Label)) + { + ConfigNode node4 = node.GetNode(configNode.name + "_UIFlight"); + if (node4 != null) + { + baseField2.uiControlFlight.Load(node4, baseField2._host); + } + } + else if (baseField2.uiControlEditor.GetType() != typeof(UI_Label)) + { + ConfigNode node5 = node.GetNode(configNode.name + "_UIEditor"); + if (node5 != null) + { + baseField2.uiControlEditor.Load(node5, baseField2._host); + } + } + } + __instance.SetOriginalValue(); + return false; + } + + + } +} diff --git a/KSPCommunityFixes/Performance/NoLiftInSpace.cs b/KSPCommunityFixes/Performance/NoLiftInSpace.cs new file mode 100644 index 0000000..74cc7a8 --- /dev/null +++ b/KSPCommunityFixes/Performance/NoLiftInSpace.cs @@ -0,0 +1,104 @@ +using System; +using HarmonyLib; +using System.Collections.Generic; +using UnityEngine; + +namespace KSPCommunityFixes.Performance +{ + class NoLiftInSpace : BasePatch + { + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleLiftingSurface), nameof(ModuleLiftingSurface.FixedUpdate)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleControlSurface), nameof(ModuleControlSurface.FixedUpdate)), + this)); + } + + static bool ModuleLiftingSurface_FixedUpdate_Prefix(ModuleLiftingSurface __instance) + { + if (HighLogic.LoadedSceneIsFlight && __instance.part.dynamicPressurekPa == 0.0 && __instance.part.submergedDynamicPressurekPa == 0.0) + { + if (__instance.pointVelocity != Vector3.zero) + { + __instance.pointVelocity = Vector3.zero; + __instance.nVel = Vector3.zero; + __instance.liftVector = Vector3.zero; + __instance.liftForce = Vector3.zero; + __instance.dragForce = Vector3.zero; + __instance.Qdrag = 0.0; + __instance.Qlift = 0.0; + __instance.liftDot = 0f; + __instance.liftField.guiActive = false; + __instance.dragField.guiActive = false; + } + return false; + } + + return true; + } + + static bool ModuleControlSurface_FixedUpdate_Prefix(ModuleControlSurface __instance) + { + if (HighLogic.LoadedSceneIsFlight && __instance.part.dynamicPressurekPa == 0.0 && __instance.part.submergedDynamicPressurekPa == 0.0) + { + if (__instance.pointVelocity != Vector3.zero) + { + __instance.pointVelocity = Vector3.zero; + __instance.nVel = Vector3.zero; + __instance.liftVector = Vector3.zero; + __instance.liftForce = Vector3.zero; + __instance.dragForce = Vector3.zero; + __instance.Qdrag = 0.0; + __instance.Qlift = 0.0; + __instance.liftDot = 0f; + __instance.liftField.guiActive = false; + __instance.dragField.guiActive = false; + __instance.baseLiftForce = Vector3.zero; + } + + if (__instance.deploy && __instance.currentDeployAngle != __instance.deployAngle) + { + float sign = __instance.usesMirrorDeploy + ? ((__instance.deployInvert ? (-1f) : 1f) * (__instance.partDeployInvert ? (-1f) : 1f) * (__instance.mirrorDeploy ? (-1f) : 1f)) + : ((__instance.deployInvert ? (-1f) : 1f) * Mathf.Sign((Quaternion.Inverse(__instance.vessel.ReferenceTransform.rotation) * (__instance.baseTransform.position - __instance.vessel.CurrentCoM)).x)); + __instance.currentDeployAngle = -1f * sign * __instance.deployAngle; + } + + if (__instance.ctrlSurface != null && __instance.deploy ? __instance.deflection != __instance.currentDeployAngle : __instance.deflection != 0f) + { + float targetAngle = __instance.deploy ? __instance.currentDeployAngle : 0f; + __instance.deflection = Mathf.MoveTowards(__instance.deflection, targetAngle, __instance.actuatorSpeed * TimeWarp.fixedDeltaTime); + __instance.ctrlSurface.localRotation = Quaternion.AngleAxis(__instance.deflection, Vector3.right) * __instance.neutral; + + if (__instance.deflection == targetAngle) + { + __instance.inputVector = Vector3.zero; + __instance.action = 0f; + __instance.roll = 0f; + + if (__instance.displaceVelocity) + { + __instance.PitchCtrlState = "n/a"; + __instance.RollCtrlState = "n/a"; + __instance.YawCtrlState = "n/a"; + __instance.potentialBladeControlTorque = Vector3.zero; + __instance.rotatingControlInput = Vector3.zero; + } + } + } + + return false; + } + + return true; + } + } +} diff --git a/KSPCommunityFixes/QoL/BetterSAS.cs b/KSPCommunityFixes/QoL/BetterSAS.cs new file mode 100644 index 0000000..cc59525 --- /dev/null +++ b/KSPCommunityFixes/QoL/BetterSAS.cs @@ -0,0 +1,823 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Unity.Profiling; +using UnityEngine; + +namespace KSPCommunityFixes.QoL +{ + class BetterSAS : BasePatch + { + protected override Version VersionMin => new Version(1, 12, 3); + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Constructor(typeof(VesselAutopilot), new Type[]{typeof(Vessel)}), + this, nameof(VesselAutopilot_Ctor_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(VesselAutopilot.VesselSAS), nameof(VesselAutopilot.VesselSAS.ConnectFlyByWire)), + this, nameof(VesselAutopilot_VesselSAS_ConnectFlyByWire_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(VesselAutopilot.VesselSAS), nameof(VesselAutopilot.VesselSAS.DisconnectFlyByWire)), + this, nameof(VesselAutopilot_VesselSAS_DisconnectFlyByWire_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(VesselAutopilot.VesselSAS), nameof(VesselAutopilot.VesselSAS.ResetAllPIDS)), + this, nameof(VesselAutopilot_VesselSAS_ResetAllPIDS_Prefix))); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleCommand), nameof(ModuleCommand.OnLoad)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleCommand), nameof(ModuleCommand.OnSave)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleCommand), nameof(ModuleCommand.OnStart)), + this)); + + GameEvents.onVesselsUndocking.Add(OnUndockOrDecouple); + GameEvents.onPartDeCoupleNewVesselComplete.Add(OnUndockOrDecouple); + GameEvents.onPartCoupleComplete.Add(OnDockOrCouple); + } + + private void OnDockOrCouple(GameEvents.FromToAction data) + { + Part vesselPart = data.to; + Part dockingPart = data.from; + + if (!(vesselPart.vessel.autopilot.SAS is KSPCFVesselSAS vesselSAS)) + return; + + UpdateModuleCommandStateRecursive(dockingPart, vesselSAS.AttitudeControllerGuiName()); + + void UpdateModuleCommandStateRecursive(Part part, string eventGuiName) + { + ModuleCommand mc = part.FindModuleImplementing(); + if (!ReferenceEquals(mc, null)) + { + BaseEvent baseEvent = mc.Events[KSPCFVesselSAS.EVENT_HASHCODE]; + if (baseEvent != null) + baseEvent.guiName = eventGuiName; + } + + int childIdx = part.children.Count; + while (childIdx-- > 0) + UpdateModuleCommandStateRecursive(part.children[childIdx], eventGuiName); + } + } + + private void OnUndockOrDecouple(Vessel oldVessel, Vessel newVessel) + { + if (!(newVessel.autopilot.SAS is KSPCFVesselSAS newVesselSAS)) + return; + + if (!(oldVessel.autopilot.SAS is KSPCFVesselSAS oldVesselSAS)) + return; + + if (newVesselSAS.controller != oldVesselSAS.controller) + { + newVesselSAS.controller = oldVesselSAS.controller; + newVesselSAS.ResetAllPIDS(); + } + } + + static bool VesselAutopilot_Ctor_Prefix(VesselAutopilot __instance, Vessel vessel) + { + __instance.vessel = vessel; + __instance.sas = new KSPCFVesselSAS(vessel); + return false; + } + + static bool VesselAutopilot_VesselSAS_ConnectFlyByWire_Prefix(VesselAutopilot.VesselSAS __instance, bool reset) + { + if (!(__instance is KSPCFVesselSAS derivedInstance)) + return true; + + if (!derivedInstance.FBWconnected) + { + derivedInstance.vessel.OnAutopilotUpdate += new FlightInputCallback(derivedInstance.ControlUpdate); + + derivedInstance.FBWconnected = true; + if (!(derivedInstance.storedTransform != derivedInstance.vessel.ReferenceTransform) && !(derivedInstance.storedTransform == null)) + { + derivedInstance.LockRotation(derivedInstance.storedTransform.rotation); + } + else + { + derivedInstance.storedTransform = derivedInstance.vessel.ReferenceTransform; + derivedInstance.LockRotation(derivedInstance.storedTransform.rotation); + } + } + if (reset) + { + derivedInstance.ResetAllPIDS(); + } + + return false; + } + + static bool VesselAutopilot_VesselSAS_DisconnectFlyByWire_Prefix(VesselAutopilot.VesselSAS __instance) + { + if (!(__instance is KSPCFVesselSAS derivedInstance)) + return true; + + derivedInstance.targetOrientation = Vector3.zero; + derivedInstance.lockedRotation = Quaternion.identity; + derivedInstance.ResetAllPIDS(); + if (derivedInstance.FBWconnected) + { + derivedInstance.vessel.OnAutopilotUpdate -= new FlightInputCallback(derivedInstance.ControlUpdate); + derivedInstance.FBWconnected = false; + } + + return false; + } + + static bool VesselAutopilot_VesselSAS_ResetAllPIDS_Prefix(VesselAutopilot.VesselSAS __instance) + { + if (!(__instance is KSPCFVesselSAS derivedInstance)) + return true; + + derivedInstance.ResetAllPIDS(); + return false; + } + + private const string CONTROLLER_VALUENAME = "KSPCF_AttCtrl"; + + static void ModuleCommand_OnLoad_Postfix(ModuleCommand __instance, ConfigNode node) + { + if (__instance.vessel.DestroyedAsNull()?.autopilot?.sas == null || !(__instance.vessel.autopilot.sas is KSPCFVesselSAS customSAS)) + return; + + string controller = node.GetValue(CONTROLLER_VALUENAME); + if (!string.IsNullOrEmpty(controller) && Enum.TryParse(controller, out KSPCFVesselSAS.AttitudeController value)) + customSAS.controller = value; + } + + static void ModuleCommand_OnSave_Postfix(ModuleCommand __instance, ConfigNode node) + { + if (__instance.vessel.DestroyedAsNull()?.autopilot?.sas == null || !(__instance.vessel.autopilot.sas is KSPCFVesselSAS customSAS)) + return; + + node.AddValue(CONTROLLER_VALUENAME, customSAS.controller.ToString()); + } + + static KSPEvent attitudeControllerKSPEvent = new KSPEvent + { + advancedTweakable = true, + active = true, + guiActive = true, + guiActiveEditor = false + }; + + static void ModuleCommand_OnStart_Postfix(ModuleCommand __instance) + { + if (__instance.vessel.DestroyedAsNull()?.autopilot?.sas == null || !(__instance.vessel.autopilot.sas is KSPCFVesselSAS customSAS)) + return; + + BaseEventDelegate baseEventDelegate = () => KSPCFVesselSAS.OnAttitudeControllerSwitch(__instance); + BaseEvent baseEvent = __instance.events.Add(KSPCFVesselSAS.EVENT_NAME, baseEventDelegate, attitudeControllerKSPEvent); + baseEvent.guiName = customSAS.AttitudeControllerGuiName(); + } + } + + public class KSPCFVesselSAS : VesselAutopilot.VesselSAS + { + public enum AttitudeController + { + StockSAS = 1, + PreciseController = 2 + } + + public AttitudeController controller = AttitudeController.PreciseController; + + public KSPCFVesselSAS(Vessel v) : base(v) + { + } + + public new void ControlUpdate(FlightCtrlState s) + { + switch (controller) + { + case AttitudeController.StockSAS: + StockSASControlUpdate(s); + break; + case AttitudeController.PreciseController: + MechJebControlUpdate(s); + break; + } + } + + public new void ResetAllPIDS() + { + switch (controller) + { + case AttitudeController.StockSAS: + pidLockedPitch.Reset(); + pidLockedRoll.Reset(); + pidLockedYaw.Reset(); + break; + case AttitudeController.PreciseController: + MechJebResetResetPID(0); + MechJebResetResetPID(1); + MechJebResetResetPID(2); + break; + } + } + + private new void UpdateVesselTorque(FlightCtrlState s) + { + torqueVector = GetTotalVesselTorque(vessel); + } + + static ProfilerMarker vesselTorqueProfiler = new ProfilerMarker("KSPCFVesselSAS.GetTotalVesselTorque"); + + private new Vector3 GetTotalVesselTorque(Vessel v) + { + vesselTorqueProfiler.Begin(); + posTorque = Vector3.zero; + negTorque = Vector3.zero; + int partIdx = vessel.parts.Count; + while (partIdx-- > 0) + { + Part part = vessel.parts[partIdx]; + int moduleIdx = part.Modules.Count; + while (moduleIdx-- > 0) + { + if (part.Modules[moduleIdx] is ITorqueProvider torqueProvider && torqueProvider != null) + { + torqueProvider.GetPotentialTorque(out Vector3 pos, out Vector3 neg); + posTorque += pos; + negTorque += neg; + } + } + } + + if (posTorque.x < 0f) posTorque.x = 0f; + if (posTorque.y < 0f) posTorque.y = 0f; + if (posTorque.z < 0f) posTorque.z = 0f; + if (negTorque.x < 0f) negTorque.x = 0f; + if (negTorque.y < 0f) negTorque.y = 0f; + if (negTorque.z < 0f) negTorque.z = 0f; + + Vector3 averageTorque = new Vector3( + 0.5f * (posTorque.x + negTorque.x), + 0.5f * (posTorque.y + negTorque.y), + 0.5f * (posTorque.z + negTorque.z)); + + vesselTorqueProfiler.End(); + return averageTorque; + } + + public const string EVENT_NAME = "KSPCFAttitudeControllerSwitch"; + public static readonly int EVENT_HASHCODE = EVENT_NAME.GetHashCode(); + + public static void OnAttitudeControllerSwitch(ModuleCommand origin) + { + if (origin.vessel.DestroyedAsNull()?.autopilot?.sas == null || !(origin.vessel.autopilot.sas is KSPCFVesselSAS customSAS)) + return; + + customSAS.ResetAllPIDS(); + + switch (customSAS.controller) + { + case AttitudeController.StockSAS: + customSAS.controller = AttitudeController.PreciseController; + break; + case AttitudeController.PreciseController: + customSAS.controller = AttitudeController.StockSAS; + break; + } + + string guiName = customSAS.AttitudeControllerGuiName(); + foreach (ModuleCommand moduleCommand in origin.vessel.FindPartModulesImplementing()) + { + BaseEvent baseEvent = moduleCommand.Events[EVENT_HASHCODE]; + if (baseEvent != null) + baseEvent.guiName = guiName; + } + + customSAS.ResetAllPIDS(); + } + + public string AttitudeControllerGuiName() + { + switch (controller) + { + case AttitudeController.PreciseController: + return "Attitude controller: PreciseController"; + case AttitudeController.StockSAS: + return "Attitude controller: Stock SAS"; + } + + return string.Empty; + } + + #region Stock SAS + + private Vector3 angularAccelerationPosMax; + private Vector3 angularAccelerationNegMax; + + private void StockSASControlUpdate(FlightCtrlState s) + { + if (!(storedTransform == null)) + { + UpdateVesselTorque(s); // called here instead from a separate OnAutopilotUpdate callback + + currentRotation = storedTransform.rotation; + UpdateMaximumAcceleration(); + rotationDelta = Quaternion.Inverse(GetRotationDelta()).eulerAngles; + if (!lockedMode) + { + PitchYawAngle(vessel.ReferenceTransform, targetOrientation, out neededPitch, out neededYaw); + rotationDelta.x = 0f - neededPitch; + rotationDelta.z = neededYaw; + CheckCoasting(); + } + else if (!dampingMode) + { + CheckCoasting(); + } + angularDelta.x = AngularTrim(rotationDelta.x); + angularDelta.y = AngularTrim(rotationDelta.y); + angularDelta.z = AngularTrim(rotationDelta.z); + angularDeltaRad = angularDelta * 0.01745329238474369; + StabilityDecay(); + AutoTuneScalar(); + sasResponse.x = pidLockedPitch.Update(angularDeltaRad.x, TimeWarp.deltaTime) / pidLockedPitch.clamp; + sasResponse.y = pidLockedRoll.Update(angularDeltaRad.y, TimeWarp.deltaTime) / pidLockedRoll.clamp; + sasResponse.z = pidLockedYaw.Update(angularDeltaRad.z, TimeWarp.deltaTime) / pidLockedYaw.clamp; + CheckDamping(); + sasResponse.x = UtilMath.Clamp(sasResponse.x, -1.0, 1.0); + sasResponse.y = UtilMath.Clamp(sasResponse.y, -1.0, 1.0); + sasResponse.z = UtilMath.Clamp(sasResponse.z, -1.0, 1.0); + s.pitch = (float)sasResponse.x; + s.roll = (float)sasResponse.y; + s.yaw = (float)sasResponse.z; + lastRotation = currentRotation; + } + } + + private new void UpdateMaximumAcceleration() + { + angularAccelerationMax.x = Mathf.Max(torqueVector.x / vessel.MOI.x, 0.0001f); + angularAccelerationMax.y = Mathf.Max(torqueVector.y / vessel.MOI.y, 0.0001f); + angularAccelerationMax.z = Mathf.Max(torqueVector.z / vessel.MOI.z, 0.0001f); + + angularAccelerationPosMax.x = Mathf.Max(posTorque.x / vessel.MOI.x, 0.0001f); + angularAccelerationPosMax.y = Mathf.Max(posTorque.y / vessel.MOI.y, 0.0001f); + angularAccelerationPosMax.z = Mathf.Max(posTorque.z / vessel.MOI.z, 0.0001f); + + angularAccelerationNegMax.x = Mathf.Max(negTorque.x / vessel.MOI.x, 0.0001f); + angularAccelerationNegMax.y = Mathf.Max(negTorque.y / vessel.MOI.y, 0.0001f); + angularAccelerationNegMax.z = Mathf.Max(negTorque.z / vessel.MOI.z, 0.0001f); + } + + private new void CheckCoasting() + { + if (angularAccelerationMax.x > 0f) + { + // float num = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.x / angularAccelerationMax.x); + float num; + if (vessel.angularVelocity.x > 0f) + num = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.x / angularAccelerationPosMax.x); + else + num = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.x / angularAccelerationNegMax.x); + + + float num2 = Mathf.Abs(rotationDelta.x * ((float)Math.PI / 180f) / vessel.angularVelocity.x); + if (num * stopScalar > num2 && Math.Sign(rotationDelta.x) != Math.Sign(vessel.angularVelocity.x)) + { + pidLockedPitch.Reset(); + rotationDelta.x = 0f - rotationDelta.x; + } + else if (num * coastScalar > num2 && Math.Sign(rotationDelta.x) != Math.Sign(vessel.angularVelocity.x)) + { + pidLockedPitch.Reset(); + rotationDelta.x = 0f; + } + } + + if (angularAccelerationMax.z > 0f) + { + //float num3 = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.z / angularAccelerationMax.z); + float num3; + if (vessel.angularVelocity.z > 0f) + num3 = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.z / angularAccelerationPosMax.z); + else + num3 = Mathf.Abs(Time.deltaTime + vessel.angularVelocity.z / angularAccelerationNegMax.z); + + + float num4 = Mathf.Abs(rotationDelta.z * ((float)Math.PI / 180f) / vessel.angularVelocity.z); + if (num3 * stopScalar > num4 && Math.Sign(rotationDelta.z) != Math.Sign(vessel.angularVelocity.z)) + { + pidLockedYaw.Reset(); + rotationDelta.z = 0f - rotationDelta.z; + } + else if (num3 * coastScalar > num4 && Math.Sign(rotationDelta.z) != Math.Sign(vessel.angularVelocity.z)) + { + pidLockedYaw.Reset(); + rotationDelta.z = 0f; + } + } + } + + #endregion + + #region Mechjeb + + private class PIDLoop + { + public double Kp { get; set; } = 1.0; + public double Ki { get; set; } + public double Kd { get; set; } + public double Ts { get; set; } = 0.02; + public double N { get; set; } = 50; + public double B { get; set; } = 1; + public double C { get; set; } = 1; + public double SmoothIn { get; set; } = 1.0; + public double SmoothOut { get; set; } = 1.0; + public double MinOutput { get; set; } = double.MinValue; + public double MaxOutput { get; set; } = double.MaxValue; + + // internal state for PID filter + private double _d1, _d2; + + // internal state for last measured and last output for low pass filters + private double _m1 = double.NaN; + private double _o1 = double.NaN; + + public double Update(double reference, double measured) + { + // lowpass filter the input + measured = _m1.IsFiniteOrZero() ? _m1 + SmoothIn * (measured - _m1) : measured; + + double ep = B * reference - measured; + double ei = reference - measured; + double ed = C * reference - measured; + + // trapezoidal PID with derivative filtering as a digital biquad filter + double a0 = 2 * N * Ts + 4; + double a1 = -8 / a0; + double a2 = (-2 * N * Ts + 4) / a0; + double b0 = (4 * Kp * ep + 4 * Kd * ed * N + 2 * Ki * ei * Ts + 2 * Kp * ep * N * Ts + Ki * ei * N * Ts * Ts) / a0; + double b1 = (2 * Ki * ei * N * Ts * Ts - 8 * Kp * ep - 8 * Kd * ed * N) / a0; + double b2 = (4 * Kp * ep + 4 * Kd * ed * N - 2 * Ki * ei * Ts - 2 * Kp * ep * N * Ts + Ki * ei * N * Ts * Ts) / a0; + + // if we have NaN values saved into internal state that needs to be cleared here or it won't reset + if (!_d1.IsFiniteOrZero()) + _d1 = 0; + if (!_d2.IsFiniteOrZero()) + _d2 = 0; + + // transposed direct form 2 + double u0 = b0 + _d1; + u0 = AttitudeUtils.Clamp(u0, MinOutput, MaxOutput); + _d1 = b1 - a1 * u0 + _d2; + _d2 = b2 - a2 * u0; + + // low pass filter the output + _o1 = _o1.IsFiniteOrZero() ? _o1 + SmoothOut * (u0 - _o1) : u0; + + _m1 = measured; + + return _o1; + } + + public void Reset() + { + _d1 = _d2 = 0; + _m1 = double.NaN; + _o1 = double.NaN; + } + } + + private static readonly Vector3d _vector3dnan = new Vector3d(double.NaN, double.NaN, double.NaN); + + private const double EPS = 2.2204e-16; + + private readonly double VelKp = 9.18299345180006; + private readonly double VelKi = 16.2833478287224; + private readonly double VelKd = -0.0921320503942923; + private readonly double VelN = 99.6720838459594; + private readonly double VelB = 0.596313214751797; + private readonly double VelC = 0.596313214751797; + private readonly double VelSmoothIn = 0.3; + private readonly double VelSmoothOut = 1.0; + private readonly double PosSmoothIn = 1.0; + private readonly double PosFactor = 1.0; + private readonly double maxStoppingTime = 2.0; + private readonly double minFlipTime = 30; + private readonly double rollControlRange = 5.0; + private bool useControlRange = true; + private bool useFlipTime = true; + private bool useStoppingTime = true; + + /* error in pitch, roll, yaw */ + private Vector3d _error0 = Vector3d.zero; + private Vector3d _error1 = _vector3dnan; + + /* max angular acceleration */ + private Vector3d _maxAlpha = Vector3d.zero; + + /* max angular rotation */ + private Vector3d _maxOmega = Vector3d.zero; + private Vector3d _omega0 = _vector3dnan; + private Vector3d _targetOmega = Vector3d.zero; + private Vector3d _actuation = Vector3d.zero; + + /* error */ + private double _errorTotal; + + private readonly PIDLoop[] _pid = + { + new PIDLoop(), + new PIDLoop(), + new PIDLoop() + }; + + private Quaternion attitudeTarget; + + private void MechJebControlUpdate(FlightCtrlState st) + { + if (storedTransform == null) + return; + + UpdateAttitudeTarget(); + + UpdatePredictionPI(); + + for (int i = 0; i < 3; i++) + if (Math.Abs(_actuation[i]) < EPS || double.IsNaN(_actuation[i])) + _actuation[i] = 0; + + Vector3d act = _actuation; + + SetFlightCtrlState(act, st); + } + + private void UpdateAttitudeTarget() + { + currentRotation = storedTransform.rotation; + if (lockedMode) + { + attitudeTarget = lockedRotation; + } + else + { + Vector3 dir = targetOrientation; + Vector3 up = -vessel.GetTransform().forward; + Vector3.OrthoNormalize(ref dir, ref up); + attitudeTarget = Quaternion.LookRotation(dir, up); + } + } + + private void SetFlightCtrlState(Vector3d act, FlightCtrlState s) + { + bool userCommandingPitch = !Mathfx.Approx(s.pitch, s.pitchTrim, 0.1f); + bool userCommandingYaw = !Mathfx.Approx(s.yaw, s.yawTrim, 0.1f); + bool userCommandingRoll = !Mathfx.Approx(s.roll, s.rollTrim, 0.1f); + + if (userCommandingPitch) + MechJebResetResetPID(0); + + if (userCommandingRoll) + MechJebResetResetPID(1); + + if (userCommandingYaw) + MechJebResetResetPID(2); + + if (!userCommandingRoll) + if (!double.IsNaN(act.y)) + s.roll = Mathf.Clamp((float)act.y, -1f, 1f); + + if (!userCommandingPitch && !userCommandingYaw) + { + if (!double.IsNaN(act.x)) + s.pitch = Mathf.Clamp((float)act.x, -1f, 1f); + + if (!double.IsNaN(act.z)) + s.yaw = Mathf.Clamp((float)act.z, -1f, 1f); + } + } + + private void UpdateError() + { + Transform vesselTransform = vessel.ReferenceTransform; + + // 1. The Euler(-90) here is because the unity transform puts "up" as the pointy end, which is wrong. The rotation means that + // "forward" becomes the pointy end, and "up" and "right" correctly define e.g. AoA/pitch and AoS/yaw. This is just KSP being KSP. + // 2. We then use the inverse ship rotation to transform the requested attitude into the ship frame (we do everything in the ship frame + // first, and then negate the error to get the error in the target reference frame at the end). + Quaternion deltaRotation; + if (lockedMode) + deltaRotation = Quaternion.identity; + else + deltaRotation = Quaternion.Inverse(vesselTransform.transform.rotation * Quaternion.Euler(-90f, 0f, 0f)) * attitudeTarget; + + // get us some euler angles for the target transform + Vector3d ea = deltaRotation.eulerAngles; + double pitch = ea[0] * UtilMath.Deg2Rad; + double yaw = ea[1] * UtilMath.Deg2Rad; + double roll = ea[2] * UtilMath.Deg2Rad; + + // law of cosines for the "distance" of the miss in radians + _errorTotal = Math.Acos(AttitudeUtils.Clamp(Math.Cos(pitch) * Math.Cos(yaw), -1.0, 1.0)); + + // this is the initial direction of the great circle route of the requested transform + // (pitch is latitude, yaw is -longitude, and we are "navigating" from 0,0) + // doing this calculation is the ship frame is a bit easier to reason about. + var temp = new Vector3d(Math.Sin(pitch), Math.Cos(pitch) * Math.Sin(-yaw), 0.0); + temp = temp.normalized * _errorTotal; + + // we assemble phi in the pitch, roll, yaw basis that vessel.MOI uses (right handed basis) + var phi = new Vector3d( + AttitudeUtils.ClampRadiansPi(temp[0]), // pitch distance around the geodesic + AttitudeUtils.ClampRadiansPi(roll), + AttitudeUtils.ClampRadiansPi(temp[1]) // yaw distance around the geodesic + ); + + // apply the axis control from the parent controller + //phi.Scale(ac.AxisControl); + + // the error in the ship's position is the negative of the reference position in the ship frame + _error0 = -phi; + } + + private void UpdatePredictionPI() + { + GetTotalVesselTorque(vessel); + + _omega0 = vessel.angularVelocityD; + + UpdateError(); + + // lowpass filter on the error input + _error0 = _error1.IsFiniteOrZero() ? _error1 + PosSmoothIn * (_error0 - _error1) : _error0; + + double deltaT = TimeWarp.fixedDeltaTime; + + // needed to stop wiggling at higher phys warp + double warpFactor = Math.Pow(deltaT / 0.02, 0.90); // the power law here comes ultimately from the simulink PID tuning app + + // see https://archive.is/NqoUm and the "Alt Hold Controller", the acceleration PID is not implemented so we only + // have the first two PIDs in the cascade. + for (int i = 0; i < 3; i++) + { + double error = _error0[i]; + + double MOI = vessel.MOI[i]; + + // I don't think this is actually correct. The resulting actuation direction (and consequentely which torque value should + // be used) doesn't always match the error direction. As it is, I think this kinda work because a correct torque evaluation + // matter a lot more when decelerating than accelerating, but I this might be also be a source of unwanted oscillations + // when the error is small, as well as overshoots when the torque authority is very large (due to effective actuation when + // accelerating being higher than predicted). + // This being said, all that likely doesn't matter as much as the controller ignoring reaction delay for gimbals and + // control surfaces. I've no idea how to account for that, if that is at all possible in this controller design. + double availableTorque = error > 0.0 ? negTorque[i] : posTorque[i]; + + if (availableTorque != 0.0 && MOI != 0.0) + _maxAlpha[i] = availableTorque / MOI; + else + _maxAlpha[i] = 1.0; + + double maxAlphaCbrt = Math.Pow(_maxAlpha[i], 1.0 / 3.0); + double effLD = maxAlphaCbrt * PosFactor; + double posKp = Math.Sqrt(_maxAlpha[i] / (2.0 * effLD)); + + if (Math.Abs(error) <= 2.0 * effLD) + // linear ramp down of acceleration + _targetOmega[i] = -posKp * error; + else + // v = -sqrt(2 * F * x / m) is target stopping velocity based on distance + _targetOmega[i] = -Math.Sqrt(2 * _maxAlpha[i] * (Math.Abs(error) - effLD)) * Math.Sign(error); + + if (useStoppingTime) + { + _maxOmega[i] = _maxAlpha[i] * maxStoppingTime; + + if (useFlipTime) + _maxOmega[i] = Math.Max(_maxOmega[i], Math.PI / minFlipTime); + + _targetOmega[i] = AttitudeUtils.Clamp(_targetOmega[i], -_maxOmega[i], _maxOmega[i]); + } + + if (useControlRange && _errorTotal * Mathf.Rad2Deg > rollControlRange) + _targetOmega[1] = 0.0; + + _pid[i].Kp = VelKp / (_maxAlpha[i] * warpFactor); + _pid[i].Ki = VelKi / (_maxAlpha[i] * warpFactor * warpFactor); + _pid[i].Kd = VelKd / _maxAlpha[i]; + _pid[i].N = VelN / warpFactor; + _pid[i].B = VelB; + _pid[i].C = VelC; + _pid[i].Ts = deltaT; + _pid[i].SmoothIn = AttitudeUtils.Clamp01(VelSmoothIn); + _pid[i].SmoothOut = AttitudeUtils.Clamp01(VelSmoothOut); + _pid[i].MinOutput = -1; + _pid[i].MaxOutput = 1; + + // need the negative from the pid due to KSP's orientation of actuation + _actuation[i] = -_pid[i].Update(_targetOmega[i], _omega0[i]); + + if (Math.Abs(_actuation[i]) < EPS || double.IsNaN(_actuation[i])) + _actuation[i] = 0; + } + + _error1 = _error0; + } + + private void MechJebResetResetPID(int i) + { + _pid[i].Reset(); + _omega0[i] = _error0[i] = _error1[i] = double.NaN; + } + + #endregion + } + + public static class AttitudeUtils + { + /// Return false if the value equals NaN or Infinity, true otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool IsFiniteOrZero(this double value) + { + long doubleAsLong = BitConverter.DoubleToInt64Bits(value); + return (doubleAsLong & 0x7FFFFFFFFFFFFFFFL) < 9218868437227405312L; + } + + /// Return false if any component of the vector equals NaN or Infinity, true otherwise + public static bool IsFiniteOrZero(this Vector3d vector) + { + return vector.x.IsFiniteOrZero() && vector.y.IsFiniteOrZero() && vector.z.IsFiniteOrZero(); + } + + public static bool IsNaN(this Vector3 vector) + { +#pragma warning disable CS1718 // Comparison made to same variable + return vector.x != vector.x || vector.y != vector.y || vector.z != vector.z; +#pragma warning restore CS1718 // Comparison made to same variable + } + + public static Vector3 ClampComponents(this Vector3 v, Vector3 min, Vector3 max) => + new Vector3(Mathf.Clamp(v.x, min.x, max.x), + Mathf.Clamp(v.y, min.y, max.y), + Mathf.Clamp(v.z, min.z, max.z)); + + /// Clamp a value between min and max + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(double value, double min, double max) + { + if (min > max) + throw new ArgumentException($"{min} cannot be greater than {max}"); + + if (value < min) + return min; + else if (value > max) + return max; + + return value; + } + + /// Clamp a value between 0.0 and 1.0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp01(double value) + { + if (value < 0.0) + return 0.0; + else if (value > 1.0) + return 1.0; + + return value; + } + + public static double ClampRadiansTwoPi(double angle) + { + angle = angle % (2.0 * Math.PI); + if (angle < 0) return angle + 2.0 * Math.PI; + else return angle; + } + + public static double ClampRadiansPi(double angle) + { + angle = ClampRadiansTwoPi(angle); + if (angle > Math.PI) angle -= 2.0 * Math.PI; + return angle; + } + + } +} diff --git a/KSPCommunityFixes/QoL/RCSLimiter.cs b/KSPCommunityFixes/QoL/RCSLimiter.cs new file mode 100644 index 0000000..5b0b8e5 --- /dev/null +++ b/KSPCommunityFixes/QoL/RCSLimiter.cs @@ -0,0 +1,690 @@ +/* +This patch implements two extra tweakables in the RCS module "Actuation Toggles" Part Action Window. + +By default, ModuleRCS will scale down the actuation level of each nozzle depending on how far the +thrust force is from the "ideal" angle for a given actuation request (unless the "always full action" +toggle is enabled). + +This patch gives the ability to define a separate angle threshold for linear and rotation actuation. +If the resulting angle from a nozzle thrust force is below that threshold, that nozzle won't fire at +all instead of firing at a reduced level. This allow to optimize efficiency, especially in the case +of multi-nozzle RCS parts that are impossible to fine-tune with only the actuation toggles. + +The default angle limits can be defined in the ModuleRCS / ModuleRCSFX configuration by adding +`minRotationAlignement` and `minlinearAlignement` fields (value in degrees). If they aren't defined, +they default to 90° (no limit, behavior similar to stock). + +To make RCS tweaking easier, the patch also add a potential torque/force readout to the actuation +toggles PAW items. In the editor, the actuation orientation is defined by the first found command +module, starting from the root part (matching the command module that will be selected as the control +point when launching the ship). The readout also takes the RCS module ISP curve into account, and +uses the currently selected body and state (sea level/altitude/vacuum) of the stock DeltaV app. + +The modification to the RCS control scheme is taken into account by the custom KSPCF +ModuleRCS.GetPotentialTorque() implementation. As of writing, all mods reimplement their own +version of that method, and all of them are ignoring the stock control scheme anyway, so the behavior +change introduced in this patch won't make a significant difference in most cases. +Note that RCSBuildAid tries to simulate the stock control scheme, but its implementation doesn't +reproduce stock behavior correctly, which is why its torque readout doesn't always match the KSPCF one. +*/ + +using System; +using System.Collections; +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using KSP.Localization; +using UnityEngine; +using KSPCommunityFixes.Modding; + +namespace KSPCommunityFixes.QoL +{ + [PatchPriority(Order = 10)] + class RCSLimiter : BasePatch + { + protected override Version VersionMin => new Version(1, 12, 3); + + protected override bool CanApplyPatch(out string reason) + { + if (KSPCommunityFixes.GetPatchInstance() == null) + { + reason = $"The {nameof(RCSLimiter)} patch requires the {nameof(BaseFieldListUseFieldHost)} to be enabled"; + return false; + } + return base.CanApplyPatch(out reason); + } + + protected override void ApplyPatches(List patches) + { + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.OnAwake)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.OnDestroy)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.UpdateToggles)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Prefix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.FixedUpdate)), + this)); + + patches.Add(new PatchInfo( + PatchMethodType.Postfix, + AccessTools.Method(typeof(ModuleRCS), nameof(ModuleRCS.Update)), + this)); + + moduleRCSExtensions = new Dictionary(); + + autoLOC_6001330_Pitch = Localizer.Format("#autoLOC_6001330"); + autoLOC_6001331_Yaw = Localizer.Format("#autoLOC_6001331"); + autoLOC_6001332_Roll = Localizer.Format("#autoLOC_6001332"); + + autoLOC_6001364_PortStbd = Localizer.Format("#autoLOC_6001364"); + string[] autoLOC_6001364 = autoLOC_6001364_PortStbd.Split('/'); + if (autoLOC_6001364.Length == 2) + { + autoLOC_Port = autoLOC_6001364[0]; + autoLOC_Stbd = autoLOC_6001364[1]; + } + + autoLOC_6001365_DorsalVentral = Localizer.Format("#autoLOC_6001365"); + string[] autoLOC_6001365 = autoLOC_6001365_DorsalVentral.Split('/'); + if (autoLOC_6001365.Length == 2) + { + autoLOC_Dorsal = autoLOC_6001365[0]; + autoLOC_Ventral = autoLOC_6001365[1]; + } + + autoLOC_6001366_ForeAft = Localizer.Format("#autoLOC_6001366"); + string[] autoLOC_6001366 = autoLOC_6001366_ForeAft.Split('/'); + if (autoLOC_6001366.Length == 2) + { + autoLOC_Fore = autoLOC_6001366[0]; + autoLOC_Aft = autoLOC_6001366[1]; + } + } + + private static string autoLOC_6001330_Pitch; + private static string autoLOC_6001331_Yaw; + private static string autoLOC_6001332_Roll; + private static string autoLOC_6001364_PortStbd; + private static string autoLOC_6001365_DorsalVentral; + private static string autoLOC_6001366_ForeAft; + + private static string autoLOC_Port; + private static string autoLOC_Stbd; + private static string autoLOC_Dorsal; + private static string autoLOC_Ventral; + private static string autoLOC_Fore; + private static string autoLOC_Aft; + + internal static Dictionary moduleRCSExtensions; + + static void ModuleRCS_OnAwake_Postfix(ModuleRCS __instance) + { + moduleRCSExtensions.Add(__instance, new ModuleRCSExtension(__instance)); + } + + static void ModuleRCS_OnDestroy_Postfix(ModuleRCS __instance) + { + moduleRCSExtensions.Remove(__instance); + } + + static void ModuleRCS_UpdateToggles_Postfix(ModuleRCS __instance) + { + if (moduleRCSExtensions.TryGetValue(__instance, out ModuleRCSExtension rcsExt)) + { + bool guiActive = __instance.showToggles && __instance.currentShowToggles && __instance.moduleIsEnabled; + rcsExt.minRotationAlignementField.guiActive = guiActive; + rcsExt.minRotationAlignementField.guiActiveEditor = guiActive; + rcsExt.minLinearAlignementField.guiActive = guiActive; + rcsExt.minLinearAlignementField.guiActiveEditor = guiActive; + } + } + + static bool ActuationToggleDisplayed(ModuleRCS mrcs) + { + if (!mrcs.showToggles || !mrcs.currentShowToggles) + return false; + + if (mrcs.part.PartActionWindow == null || !mrcs.part.PartActionWindow.isActiveAndEnabled) + return false; + + return true; + } + + internal class ModuleRCSExtension + { + public static FieldInfo minRotationAlignementFieldInfo = AccessTools.Field(typeof(ModuleRCSExtension), nameof(minRotationAlignement)); + public static KSPField minRotationAlignementKSPField = new KSPField(); + public static UI_FloatRange minRotationAlignementControl = new UI_FloatRange(); + + public static FieldInfo minLinearAlignementFieldInfo = AccessTools.Field(typeof(ModuleRCSExtension), nameof(minLinearAlignement)); + public static KSPField minLinearAlignementKSPField = new KSPField(); + public static UI_FloatRange minLinearAlignementControl = new UI_FloatRange(); + + static ModuleRCSExtension() + { + minRotationAlignementKSPField.guiActive = false; + minRotationAlignementKSPField.guiActiveEditor = false; + minRotationAlignementKSPField.guiName = "Min rotation alignement"; + minRotationAlignementKSPField.guiFormat = "0°"; + minRotationAlignementKSPField.isPersistant = true; + + minRotationAlignementControl.minValue = 5f; + minRotationAlignementControl.maxValue = 90f; + minRotationAlignementControl.stepIncrement = 1f; + minRotationAlignementControl.affectSymCounterparts = UI_Scene.All; + + minLinearAlignementKSPField.guiActive = false; + minLinearAlignementKSPField.guiActiveEditor = false; + minLinearAlignementKSPField.guiName = "Min translation alignement"; + minLinearAlignementKSPField.guiFormat = "0°"; + minLinearAlignementKSPField.isPersistant = true; + + minLinearAlignementControl.minValue = 5f; + minLinearAlignementControl.maxValue = 90f; + minLinearAlignementControl.stepIncrement = 1f; + minLinearAlignementControl.affectSymCounterparts = UI_Scene.All; + } + + public ModuleRCS module; + + public BaseField minRotationAlignementField; + public float minRotationAlignement; + + public BaseField minLinearAlignementField; + public float minLinearAlignement; + + public float lastTorqueUpdateFrame; + + public BaseField enablePitchField; + public BaseField enableRollField; + public BaseField enableYawField; + public BaseField enableXField; + public BaseField enableYField; + public BaseField enableZField; + + public ModuleRCSExtension(ModuleRCS module) + { + this.module = module; + + minRotationAlignementField = new BaseField(minRotationAlignementKSPField, minRotationAlignementFieldInfo, this); + minRotationAlignementField.uiControlEditor = minRotationAlignementControl; + minRotationAlignementField.uiControlFlight = minRotationAlignementControl; + module.Fields.Add(minRotationAlignementField); + + minLinearAlignementField = new BaseField(minLinearAlignementKSPField, minLinearAlignementFieldInfo, this); + minLinearAlignementField.uiControlEditor = minLinearAlignementControl; + minLinearAlignementField.uiControlFlight = minLinearAlignementControl; + module.Fields.Add(minLinearAlignementField); + + enablePitchField = module.Fields[nameof(ModuleRCS.enablePitch)]; + enableRollField = module.Fields[nameof(ModuleRCS.enableRoll)]; + enableYawField = module.Fields[nameof(ModuleRCS.enableYaw)]; + enableXField = module.Fields[nameof(ModuleRCS.enableX)]; + enableYField = module.Fields[nameof(ModuleRCS.enableY)]; + enableZField = module.Fields[nameof(ModuleRCS.enableZ)]; + + int moduleIndex = module.part.modules.IndexOf(module); + if (moduleIndex != -1 + && module.part.partInfo?.partPrefab != null + && moduleIndex < module.part.partInfo.partPrefab.modules.Count + && module.part.partInfo.partPrefab.modules[moduleIndex] is ModuleRCS prefabModule + && moduleRCSExtensions.TryGetValue(prefabModule, out ModuleRCSExtension prefabLimits)) + { + minRotationAlignement = prefabLimits.minRotationAlignement; + minLinearAlignement = prefabLimits.minLinearAlignement; + } + else + { + minRotationAlignement = 90f; + minLinearAlignement = 90f; + } + } + + public void UpdatePAWTorqueAndForces(Transform referenceTransform, float thrustForce, Vector3 currentCoM) + { + Vector3 posTorque = Vector3.zero; + Vector3 negTorque = Vector3.zero; + Vector3 posForce = Vector3.zero; + Vector3 negForce = Vector3.zero; + + Quaternion controlRotation = referenceTransform.rotation; + + Vector3 pitchCtrl = controlRotation * Vector3.right; + Vector3 rollCtrl = controlRotation * Vector3.up; + Vector3 yawCtrl = controlRotation * Vector3.forward; + + float minRotActuation = Mathf.Max(Mathf.Cos(minRotationAlignement * Mathf.Deg2Rad), 0f); + float minLinActuation = Mathf.Max(Mathf.Cos(minLinearAlignement * Mathf.Deg2Rad), 0f); + + for (int i = module.thrusterTransforms.Count - 1; i >= 0; i--) + { + Transform thruster = module.thrusterTransforms[i]; + + if (!thruster.gameObject.activeInHierarchy) + continue; + + Vector3 thrusterPosFromCoM = thruster.position - currentCoM; + Vector3 thrusterDirFromCoM = thrusterPosFromCoM.normalized; + Vector3 thrustDirection = module.useZaxis ? thruster.forward : thruster.up; + + Vector3 thrusterThrust = thrustDirection * thrustForce; + Vector3 thrusterTorque = Vector3.Cross(thrusterPosFromCoM, thrusterThrust); + // transform in vessel control space + thrusterTorque = referenceTransform.InverseTransformDirection(thrusterTorque); + + if (module.enablePitch && Math.Abs(thrusterTorque.x) > 0.0001f) + { + Vector3 pitchRot = Vector3.Cross(pitchCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, pitchCtrl)); + float actuation = Vector3.Dot(thrustDirection, pitchRot.normalized); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posTorque.x += thrusterTorque.x * actuation; + else + negTorque.x += thrusterTorque.x * actuation; + } + } + + if (module.enableRoll && Math.Abs(thrusterTorque.y) > 0.0001f) + { + Vector3 rollRot = Vector3.Cross(rollCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, rollCtrl)); + float actuation = Vector3.Dot(thrustDirection, rollRot.normalized); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posTorque.y += thrusterTorque.y * actuation; + else + negTorque.y += thrusterTorque.y * actuation; + } + } + + if (module.enableYaw && Math.Abs(thrusterTorque.z) > 0.0001f) + { + Vector3 yawRot = Vector3.Cross(yawCtrl, Vector3.ProjectOnPlane(thrusterDirFromCoM, yawCtrl)); + float actuation = Vector3.Dot(thrustDirection, yawRot.normalized); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minRotActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posTorque.z += thrusterTorque.z * actuation; + else + negTorque.z += thrusterTorque.z * actuation; + } + } + + if (module.enableX) + { + float actuation = Vector3.Dot(thrustDirection, pitchCtrl); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minLinActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posForce.x += thrustForce * actuation; + else + negForce.x -= thrustForce * actuation; + } + } + + if (module.enableY) + { + float actuation = Vector3.Dot(thrustDirection, yawCtrl); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minLinActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posForce.z += thrustForce * actuation; + else + negForce.z -= thrustForce * actuation; + } + } + + if (module.enableZ) + { + float actuation = Vector3.Dot(thrustDirection, rollCtrl); + float actuationMagnitude = Math.Abs(actuation); + + if (actuationMagnitude < minLinActuation) + actuation = 0f; + else if (module.fullThrust && actuationMagnitude > module.fullThrustMin) + actuation = Math.Sign(actuation); + + if (actuation != 0f) + { + if (actuation > 0f) + posForce.y += thrustForce * actuation; + else + negForce.y -= thrustForce * actuation; + } + } + } + + if (module.enablePitch) + SetToggleGuiName(enablePitchField, $"{autoLOC_6001330_Pitch}: {Math.Round(posTorque.x, 3):G3} / {-Math.Round(negTorque.x, 3):G3} kNm"); + else + SetToggleGuiName(enablePitchField, autoLOC_6001330_Pitch); + + if (module.enableRoll) + SetToggleGuiName(enableRollField, $"{autoLOC_6001332_Roll}: {Math.Round(posTorque.y, 3):G3} / {-Math.Round(negTorque.y, 3):G3} kNm"); + else + SetToggleGuiName(enableRollField, autoLOC_6001332_Roll); + + if (module.enableYaw) + SetToggleGuiName(enableYawField, $"{autoLOC_6001331_Yaw}: {Math.Round(posTorque.z, 3):G3} / {-Math.Round(negTorque.z, 3):G3} kNm"); + else + SetToggleGuiName(enableYawField, autoLOC_6001331_Yaw); + + if (module.enableX) + SetToggleGuiName(enableXField, autoLOC_Port == null + ? $"{autoLOC_6001364_PortStbd}: {Math.Round(posForce.x, 3):G1} / {Math.Round(negForce.x, 3):G1} kN" + : $"{autoLOC_Port}: {Math.Round(posForce.x, 3):G1}kN / {autoLOC_Stbd}: {Math.Round(negForce.x, 3):G1}kN"); + else + SetToggleGuiName(enableXField, autoLOC_6001364_PortStbd); + + if (module.enableY) + SetToggleGuiName(enableYField, autoLOC_Dorsal == null + ? $"{autoLOC_6001365_DorsalVentral}: {Math.Round(posForce.z, 3):G1} / {Math.Round(negForce.z, 3):G1} kN" + : $"{autoLOC_Dorsal}: {Math.Round(posForce.z, 3):G1}kN / {autoLOC_Ventral}: {Math.Round(negForce.z, 3):G1}kN"); + else + SetToggleGuiName(enableYField, autoLOC_6001365_DorsalVentral); + + if (module.enableZ) + SetToggleGuiName(enableZField, autoLOC_Fore == null + ? $"{autoLOC_6001366_ForeAft}: {Math.Round(posForce.y, 3):G1} / {Math.Round(negForce.y, 3):G1} kN" + : $"{autoLOC_Fore}: {Math.Round(posForce.y, 3):G1}kN / {autoLOC_Aft}: {Math.Round(negForce.y, 3):G1}kN"); + else + SetToggleGuiName(enableZField, autoLOC_6001366_ForeAft); + } + + public void DisableTorquePAWInfo() + { + SetToggleGuiName(enablePitchField, autoLOC_6001330_Pitch); + SetToggleGuiName(enableRollField, autoLOC_6001332_Roll); + SetToggleGuiName(enableYawField, autoLOC_6001331_Yaw); + SetToggleGuiName(enableXField, autoLOC_6001364_PortStbd); + SetToggleGuiName(enableYField, autoLOC_6001365_DorsalVentral); + SetToggleGuiName(enableZField, autoLOC_6001366_ForeAft); + } + + public static void SetToggleGuiName(BaseField baseField, string guiName) + { + baseField.guiName = guiName; + + UIPartActionToggle toggle; + + if (!ReferenceEquals(baseField.uiControlEditor.partActionItem, null)) + toggle = (UIPartActionToggle)baseField.uiControlEditor.partActionItem; + else if (!ReferenceEquals(baseField.uiControlFlight.partActionItem, null)) + toggle = (UIPartActionToggle)baseField.uiControlFlight.partActionItem; + else + return; + + toggle.fieldName.text = guiName; + toggle.fieldName.rectTransform.sizeDelta = new Vector2(150f, toggle.fieldName.rectTransform.sizeDelta.y); + + } + } + + #region Editor PAW update + + static void ModuleRCS_Update_Postfix(ModuleRCS __instance) + { + if (HighLogic.LoadedScene != GameScenes.EDITOR) + return; + + if (!ActuationToggleDisplayed(__instance)) + return; + + if (!moduleRCSExtensions.TryGetValue(__instance, out ModuleRCSExtension rcsExt)) + return; + + if (__instance.part.frozen + || !__instance.moduleIsEnabled + || !__instance.rcsEnabled + || __instance.isJustForShow + || (__instance.part.ShieldedFromAirstream && !__instance.shieldedCanThrust) + || !EditorSetupPropellant(__instance)) + { + rcsExt.DisableTorquePAWInfo(); + return; + } + + if (EditorPhysics.TryGetAndUpdate(out EditorPhysics editorPhysics)) + { + if (rcsExt.lastTorqueUpdateFrame < editorPhysics.lastShipModificationFrame) + { + rcsExt.lastTorqueUpdateFrame = editorPhysics.lastShipModificationFrame; + __instance.StartCoroutine(UpdatePAWDelayed(__instance, rcsExt)); + } + } + } + + private static IEnumerator UpdatePAWDelayed(ModuleRCS mrcs, ModuleRCSExtension rcsExt) + { + yield return null; + + if (EditorPhysics.TryGetAndUpdate(out EditorPhysics editorPhysics)) + { + mrcs.realISP = mrcs.atmosphereCurve.Evaluate((float)editorPhysics.atmStaticPressure); + double exhaustVel = mrcs.realISP * mrcs.G * mrcs.ispMult; + float thrustForce = (float)(exhaustVel * mrcs.maxFuelFlow * mrcs.flowMult * mrcs.thrustPercentage * 0.01); + + rcsExt.UpdatePAWTorqueAndForces(editorPhysics.referenceTransform, thrustForce, editorPhysics.CoM); + } + } + + + // ModuleRCS populate the propellant list in OnLoad(), and because the list isn't defined as serializable, it isn't populated for part instantiated in the editor. + // So check if the list is populated, and if not copy the prefab list. + private static bool EditorSetupPropellant(ModuleRCS mrcs) + { + if (mrcs.propellants.Count > 0) + return true; + + int moduleIdx = mrcs.part.modules.IndexOf(mrcs); + if (moduleIdx >= 0 && moduleIdx < mrcs.part.partInfo.partPrefab.modules.Count && mrcs.part.partInfo.partPrefab.modules[moduleIdx] is ModuleRCS prefabModule) + { + foreach (Propellant prefabPropellant in prefabModule.propellants) + mrcs.propellants.Add(JsonUtility.FromJson(JsonUtility.ToJson(prefabPropellant))); // Propellant is [Serializable], so lazy but effective + + mrcs.mixtureDensity = prefabModule.mixtureDensity; + mrcs.mixtureDensityRecip = prefabModule.mixtureDensityRecip; + mrcs.maxFuelFlow = prefabModule.maxFuelFlow; + } + + return mrcs.propellants.Count > 0; + } + + #endregion + + #region FixedUpdate + + static bool ModuleRCS_FixedUpdate_Prefix(ModuleRCS __instance) + { + __instance.isOperating = false; + if (!HighLogic.LoadedSceneIsFlight) + { + return false; + } + + if (TimeWarp.CurrentRate > 1f && TimeWarp.WarpMode == TimeWarp.Modes.HIGH) + { + if (ActuationToggleDisplayed(__instance) && moduleRCSExtensions.TryGetValue(__instance, out ModuleRCSExtension rcsExt)) + rcsExt.DisableTorquePAWInfo(); + + __instance.DeactivatePowerFX(); + return false; + } + + __instance.tC = __instance.thrusterTransforms.Count; + if (__instance.thrustForces.Length != __instance.tC) + { + __instance.thrustForces = new float[__instance.tC]; + } + + int thrusterIdx = __instance.tC; + while (thrusterIdx-- > 0) + { + __instance.thrustForces[thrusterIdx] = 0f; + } + + __instance.totalThrustForce = 0f; + __instance.realISP = __instance.atmosphereCurve.Evaluate((float)__instance.part.staticPressureAtm); + __instance.exhaustVel = __instance.realISP * __instance.G * __instance.ispMult; + __instance.thrustForceRecip = 1f / __instance.thrusterPower; + if (__instance.moduleIsEnabled && __instance.vessel != null && __instance.rcsEnabled && !__instance.IsAdjusterBreakingRCS() && (!__instance.part.ShieldedFromAirstream || __instance.shieldedCanThrust)) + { + ModuleRCSExtension rcsExt = null; + bool rcsActive; + if ((rcsActive = __instance.vessel.ActionGroups[KSPActionGroup.RCS]) != __instance.rcs_active) + { + __instance.rcs_active = rcsActive; + } + if (__instance.rcs_active && (__instance.inputRot != Vector3.zero || __instance.inputLin != Vector3.zero)) + { + __instance.predictedCOM = __instance.vessel.CurrentCoM; + + float rotLimit, linLimit; + if (moduleRCSExtensions.TryGetValue(__instance, out rcsExt)) + { + rotLimit = Mathf.Max(Mathf.Cos(rcsExt.minRotationAlignement * Mathf.Deg2Rad), 0f); + linLimit = Mathf.Max(Mathf.Cos(rcsExt.minLinearAlignement * Mathf.Deg2Rad), 0f); + } + else + { + rotLimit = 0f; + linLimit = 0f; + } + + bool success = false; + thrusterIdx = __instance.tC; + while (thrusterIdx-- > 0) + { + __instance.currentThruster = __instance.thrusterTransforms[thrusterIdx]; + if (__instance.currentThruster.position == Vector3.zero || !__instance.currentThruster.gameObject.activeInHierarchy) + continue; + + Vector3 thrustDir = ((!__instance.useZaxis) ? __instance.currentThruster.up : __instance.currentThruster.forward); + __instance.rot = Vector3.Cross(__instance.inputRot, Vector3.ProjectOnPlane(__instance.currentThruster.position - __instance.predictedCOM, __instance.inputRot).normalized); + __instance.currentThrustForce = Vector3.Dot(thrustDir, __instance.rot); + + if (__instance.currentThrustForce < rotLimit) + __instance.currentThrustForce = 0f; + + float linDot = Vector3.Dot(thrustDir, __instance.inputLin); + if (linDot >= linLimit) + __instance.currentThrustForce += linDot; + + if (__instance.currentThrustForce == 0f) + { + __instance.thrustForces[thrusterIdx] = 0f; + __instance.isOperating |= false; + __instance.UpdatePowerFX(false, thrusterIdx, 0f); + continue; + } + + if (__instance.currentThrustForce > 1f) + { + __instance.currentThrustForce = 1f; + } + if (__instance.fullThrust && __instance.currentThrustForce >= __instance.fullThrustMin) + { + __instance.currentThrustForce = 1f; + } + if (__instance.usePrecision) + { + if (__instance.useLever) + { + __instance.leverDistance = __instance.GetLeverDistance(__instance.currentThruster, -thrustDir, __instance.predictedCOM); + if (__instance.leverDistance > 1f) + { + __instance.currentThrustForce /= __instance.leverDistance; + } + } + else + { + __instance.currentThrustForce *= __instance.precisionFactor; + } + } + __instance.UpdatePropellantStatus(); + __instance.currentThrustForce = __instance.CalculateThrust(__instance.currentThrustForce, out success); + __instance.thrustForces[thrusterIdx] = __instance.currentThrustForce; + bool isThrusting = __instance.currentThrustForce > 0f && success; + __instance.isOperating |= isThrusting; + __instance.UpdatePowerFX(isThrusting, thrusterIdx, Mathf.Clamp(__instance.currentThrustForce * __instance.thrustForceRecip, 0.1f, 1f)); + if (isThrusting && !__instance.isJustForShow) + { + __instance.totalThrustForce += __instance.currentThrustForce; + __instance.part.AddForceAtPosition(-thrustDir * __instance.currentThrustForce, __instance.currentThruster.transform.position); + } + } + } + else + { + __instance.DeactivateFX(); + } + + if (ActuationToggleDisplayed(__instance) && (rcsExt != null || moduleRCSExtensions.TryGetValue(__instance, out rcsExt))) + { + float thrustForce = (float)(__instance.exhaustVel * __instance.maxFuelFlow * __instance.flowMult * __instance.thrustPercentage * 0.01); + rcsExt.UpdatePAWTorqueAndForces(__instance.vessel.ReferenceTransform, thrustForce, __instance.vessel.CurrentCoM); + } + } + else + { + __instance.DeactivateFX(); + + if (ActuationToggleDisplayed(__instance) && moduleRCSExtensions.TryGetValue(__instance, out ModuleRCSExtension rcsExt)) + rcsExt.DisableTorquePAWInfo(); + } + + return false; + } + + #endregion + + + } +} From c19113e1c88258d7ed9d9cd58523938f04c11fff Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:32:18 +0100 Subject: [PATCH 2/3] Removed the ReactionWheelsPotentialTorque patch (superseded by the GetPotentialTorqueFixes patch) --- GameData/KSPCommunityFixes/Settings.cfg | 5 --- .../BugFixes/ReactionWheelsPotentialTorque.cs | 36 ------------------- KSPCommunityFixes/KSPCommunityFixes.csproj | 1 - README.md | 1 - 4 files changed, 43 deletions(-) delete mode 100644 KSPCommunityFixes/BugFixes/ReactionWheelsPotentialTorque.cs diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index eb05ab3..72ccdfa 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -63,11 +63,6 @@ KSP_COMMUNITY_FIXES // Fix a bug causing the ROC manager to crash during loading with Kopernicus modified systems ROCValidationOOR = true - // Fix reaction wheels reporting incorrect available torque when the "Wheel Authority" - // tweakable is set below 100%. This fix instability issues with the stock SAS and other - // attitude controllers from various mods. - ReactionWheelsPotentialTorque = true - // Make the stock alarm to respect the day/year length defined by mods like // Kronometer. Fix the underlying AppUIMemberDateTime UI widget API to use the // custom IDateTimeFormatter if implemented. diff --git a/KSPCommunityFixes/BugFixes/ReactionWheelsPotentialTorque.cs b/KSPCommunityFixes/BugFixes/ReactionWheelsPotentialTorque.cs deleted file mode 100644 index 85ddcb2..0000000 --- a/KSPCommunityFixes/BugFixes/ReactionWheelsPotentialTorque.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using HarmonyLib; -using System.Collections.Generic; -using UnityEngine; -using static ModuleReactionWheel; - -namespace KSPCommunityFixes -{ - public class ReactionWheelsPotentialTorque : BasePatch - { - protected override Version VersionMin => new Version(1, 8, 0); - - protected override void ApplyPatches(List patches) - { - patches.Add(new PatchInfo( - PatchMethodType.Prefix, - AccessTools.Method(typeof(ModuleReactionWheel), nameof(ModuleReactionWheel.GetPotentialTorque)), - this)); - } - - static bool ModuleReactionWheel_GetPotentialTorque_Prefix(ModuleReactionWheel __instance, out Vector3 pos, out Vector3 neg) - { - if (__instance.moduleIsEnabled && __instance.wheelState == WheelState.Active && __instance.actuatorModeCycle != 2) - { - float authorityLimiter = __instance.authorityLimiter * 0.01f; - neg.x = (pos.x = __instance.PitchTorque * authorityLimiter); - neg.y = (pos.y = __instance.RollTorque * authorityLimiter); - neg.z = (pos.z = __instance.YawTorque * authorityLimiter); - return false; - } - - pos = (neg = Vector3.zero); - return false; - } - } -} diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index f741ec2..0a5b417 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -134,7 +134,6 @@ - diff --git a/README.md b/README.md index a85c445..7920c74 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ User options are available from the "ESC" in-game settings menu :
Fix ROCManager crashing during loading with Kopernicus modified systems. -- **ReactionWheelsPotentialTorque** [KSP 1.8.0 - 1.12.5]
Fix reaction wheels reporting incorrect available torque when "Wheel Authority" is set below 100%. Fix stock SAS (and possibly other attitude controllers) instability issues. - **StockAlarmCustomFormatterDate** [KSP 1.12.0 - 1.12.5]
Make the stock alarm respect the day/year length defined by mods like Kronometer. Fix the underlying AppUIMemberDateTime UI widget API to use the mod-provided IDateTimeFormatter if present. - **[StockAlarmDescPreserveLineBreak](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/19)** [KSP 1.12.0 - 1.12.5]
Stock alarm preserve line breaks (and tabs) in the description field. - **ExtendedDeployableParts** [KSP 1.12.0 - 1.12.5]
Fix deployable parts (antennas, solar panels, radiators...) always starting in the extended state when the model isn't exported in the retracted state. This bug affect parts from various mods (ex : Ven's stock revamp solar panels). From 5f36f6a9c5c63679e7186065af030f3259ca92df Mon Sep 17 00:00:00 2001 From: gotmachine <24925209+gotmachine@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:18:44 +0100 Subject: [PATCH 3/3] Fixed incorrect potential torque cache checking + a few tweaks here and there --- KSPCommunityFixes.sln | 9 ++- .../BugFixes/GetPotentialTorqueFixes.cs | 64 +++++++++++-------- KSPCommunityFixes/KSPCommunityFixes.csproj | 9 +++ KSPCommunityFixes/QoL/BetterSAS.cs | 3 +- .../MultipleModuleInPartAPI.csproj | 8 +++ 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/KSPCommunityFixes.sln b/KSPCommunityFixes.sln index ada51a7..254056c 100644 --- a/KSPCommunityFixes.sln +++ b/KSPCommunityFixes.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30804.86 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPCommunityFixes", "KSPCommunityFixes\KSPCommunityFixes.csproj", "{4E405C02-5AEB-4975-B26C-07582BB3FB15}" EndProject @@ -18,15 +18,20 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Profiling|Any CPU = Profiling|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Profiling|Any CPU.ActiveCfg = Profiling|Any CPU + {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Profiling|Any CPU.Build.0 = Profiling|Any CPU {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E405C02-5AEB-4975-B26C-07582BB3FB15}.Release|Any CPU.Build.0 = Release|Any CPU {32B601F4-B648-4C69-AA98-620FE7BA070C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {32B601F4-B648-4C69-AA98-620FE7BA070C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32B601F4-B648-4C69-AA98-620FE7BA070C}.Profiling|Any CPU.ActiveCfg = Profiling|Any CPU + {32B601F4-B648-4C69-AA98-620FE7BA070C}.Profiling|Any CPU.Build.0 = Profiling|Any CPU {32B601F4-B648-4C69-AA98-620FE7BA070C}.Release|Any CPU.ActiveCfg = Release|Any CPU {32B601F4-B648-4C69-AA98-620FE7BA070C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection diff --git a/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs b/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs index b215858..b29135b 100644 --- a/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs +++ b/KSPCommunityFixes/BugFixes/GetPotentialTorqueFixes.cs @@ -147,11 +147,12 @@ private IEnumerator CustomUpdate() private static string autoLOC_6001331_Yaw; private static string autoLOC_6001332_Roll; - static ProfilerMarker rwProfiler = new ProfilerMarker("ModuleReactionWheel.GetPotentialTorque"); - static ProfilerMarker rcsProfiler = new ProfilerMarker("ModuleRCS.GetPotentialTorque"); - static ProfilerMarker ctrlSrfProfiler = new ProfilerMarker("ModuleControlSurface.GetPotentialTorque"); - static ProfilerMarker gimbalProfiler = new ProfilerMarker("ModuleGimbal.GetPotentialTorque"); - static ProfilerMarker gimbalCacheProfiler = new ProfilerMarker("ModuleGimbal.GetPotentialTorque.CacheCheck"); + static ProfilerMarker rwProfiler = new ProfilerMarker("KSPCF.ModuleReactionWheel.GetPotentialTorque"); + static ProfilerMarker rcsProfiler = new ProfilerMarker("KSPCF.ModuleRCS.GetPotentialTorque"); + static ProfilerMarker ctrlSrfProfiler = new ProfilerMarker("KSPCF.ModuleControlSurface.GetPotentialTorque"); + static ProfilerMarker ctrlSrfCacheProfiler = new ProfilerMarker("KSPCF.ModuleControlSurface.GetPotentialTorque.CacheCheck"); + static ProfilerMarker gimbalProfiler = new ProfilerMarker("KSPCF.ModuleGimbal.GetPotentialTorque"); + static ProfilerMarker gimbalCacheProfiler = new ProfilerMarker("KSPCF.ModuleGimbal.GetPotentialTorque.CacheCheck"); #region ModuleReactionWheel @@ -412,6 +413,8 @@ public static ModuleCtrlSrfExtension Get(ModuleControlSurface module) private bool lastPitch; private bool lastRoll; private bool lastYaw; + private float QLiftThreshold; + private float timeThreshold; private bool pawTorqueEnabled; private BaseField ignorePitchField; @@ -428,6 +431,9 @@ public ModuleCtrlSrfExtension(ModuleControlSurface module) module.part.OnJustAboutToBeDestroyed += OnDestroy; instances.Add(module, this); + + QLiftThreshold = Random.Range(0.04f, 0.06f); + timeThreshold = Random.Range(0.75f, 1.25f); } public void OnDestroy() @@ -473,18 +479,18 @@ public void UpdateCachedState(Vector3 worldCoM, Vector3 localCoM) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsCacheValid(Vector3 worldCoM, Vector3 localCoM) { - if ((QLift > 0.0 && Math.Abs((module.Qlift / QLift) - 1.0) > Random.Range(0.04f, 0.06f)) + if ((QLift > 0.0 && Math.Abs((module.Qlift / QLift) - 1.0) > QLiftThreshold) || (this.localCoM - localCoM).sqrMagnitude > 0.1f * 0.1f || (lastBaseLiftForce - module.baseLiftForce).sqrMagnitude > 0.1f * 0.1f - || lastTime + Random.Range(0.75f, 1.25f) < Time.fixedTime + || Time.fixedTime > lastTime + timeThreshold || currentDeployAngle != module.currentDeployAngle || lastPitch != module.ignorePitch || lastRoll != module.ignoreRoll || lastYaw != module.ignoreYaw) { UpdateCachedState(worldCoM, localCoM); - return true; + return false; } - return false; + return true; } public void UpdateEditorState(Transform referenceTransform, EditorPhysics editorPhysics) @@ -639,7 +645,10 @@ static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface if (__instance.displaceVelocity) { if (isEditor) + { + ctrlSrfProfiler.End(); return false; + } // This case is for handling "propeller blade" control surfaces. Those have a completely different behavior and // actuation scheme (and why this wasn't implemented as a separate module is beyond my understanding). @@ -667,7 +676,7 @@ static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface { if (!EditorPhysics.TryGetAndUpdate(out EditorPhysics editorPhysics) || editorPhysics.atmDensity == 0.0) { - gimbalProfiler.End(); + ctrlSrfProfiler.End(); return false; } @@ -679,18 +688,18 @@ static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface { vesselReferenceTransform = __instance.vessel.ReferenceTransform; - gimbalCacheProfiler.Begin(); + ctrlSrfCacheProfiler.Begin(); moduleExt = ModuleCtrlSrfExtension.Get(__instance); - if (!moduleExt.IsCacheValid(__instance.vessel.CurrentCoM, vesselReferenceTransform.InverseTransformPoint(__instance.vessel.CurrentCoM))) + if (moduleExt.IsCacheValid(__instance.vessel.CurrentCoM, vesselReferenceTransform.InverseTransformPoint(__instance.vessel.CurrentCoM))) { pos = moduleExt.pos; neg = moduleExt.neg; - gimbalCacheProfiler.End(); - gimbalProfiler.End(); + ctrlSrfCacheProfiler.End(); + ctrlSrfProfiler.End(); return false; } - gimbalCacheProfiler.End(); + ctrlSrfCacheProfiler.End(); } Vector3 potentialForcePos = GetPotentialLiftAndDrag(__instance, moduleExt, moduleExt.currentDeployAngle, true); @@ -846,6 +855,9 @@ static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface #endif } + moduleExt.pos = pos; + moduleExt.neg = neg; + #if DEBUG TorqueUIModule ui = __instance.part.FindModuleImplementing(); if (ui != null) @@ -865,9 +877,6 @@ static bool ModuleControlSurface_GetPotentialTorque_Prefix(ModuleControlSurface ui.Fields["negAction"].guiActive = true; ui.negAction = negAction; } - - moduleExt.pos = pos; - moduleExt.neg = neg; #endif } @@ -925,9 +934,9 @@ private static Vector3 GetDragForce(ModuleControlSurface mcs, ModuleCtrlSrfExten return Vector3.zero; } - #endregion +#endregion - #region ModuleGimbal +#region ModuleGimbal private class ModuleGimbalExtension { @@ -953,6 +962,7 @@ public static ModuleGimbalExtension Get(ModuleGimbal module) private bool lastPitch; private bool lastRoll; private bool lastYaw; + private float timeThreshold; private bool pawTorqueEnabled; private BaseField enablePitchField; @@ -969,6 +979,8 @@ public ModuleGimbalExtension(ModuleGimbal module) module.part.OnJustAboutToBeDestroyed += OnDestroy; instances.Add(module, this); + + timeThreshold = Random.Range(0.75f, 1.25f); } public void OnDestroy() @@ -991,19 +1003,19 @@ public void UpdateLastState(Vector3 localCoM, float thrustForce) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool UpdateRequired(Vector3 localCoM, float thrustForce) + public bool IsCacheValid(Vector3 localCoM, float thrustForce) { if (Math.Abs(lastThrustForce - thrustForce) > 1.0 || (lastLocalCoM - localCoM).sqrMagnitude > 0.1f * 0.1f - || lastTime + Random.Range(0.75f, 1.25f) < Time.fixedTime + || Time.fixedTime > lastTime + timeThreshold || lastGimbalLimiter != module.gimbalLimiter || lastPitch != module.enablePitch || lastRoll != module.enableRoll || lastYaw != module.enableYaw) { UpdateLastState(localCoM, thrustForce); - return true; + return false; } - return false; + return true; } public static void UpdateInstances() @@ -1189,7 +1201,7 @@ static bool ModuleGimbal_GetPotentialTorque_Prefix(ModuleGimbal __instance, out gimbalCacheProfiler.Begin(); gimbalCache = ModuleGimbalExtension.Get(__instance); - if (!gimbalCache.UpdateRequired(localCoM, totalThrust)) + if (gimbalCache.IsCacheValid(localCoM, totalThrust)) { pos = gimbalCache.pos; neg = gimbalCache.neg; @@ -1415,7 +1427,7 @@ static Quaternion GetGimbalWorldRotation(ModuleGimbal mg, Transform referenceTra return gimbalRotation; } - #endregion +#endregion } #if DEBUG diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index 0a5b417..5045f8d 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -37,6 +37,15 @@ 4 true + + portable + true + bin\Profiling\ + TRACE;ENABLE_PROFILER + prompt + 4 + true + diff --git a/KSPCommunityFixes/QoL/BetterSAS.cs b/KSPCommunityFixes/QoL/BetterSAS.cs index cc59525..99a7e1d 100644 --- a/KSPCommunityFixes/QoL/BetterSAS.cs +++ b/KSPCommunityFixes/QoL/BetterSAS.cs @@ -257,7 +257,8 @@ public KSPCFVesselSAS(Vessel v) : base(v) int moduleIdx = part.Modules.Count; while (moduleIdx-- > 0) { - if (part.Modules[moduleIdx] is ITorqueProvider torqueProvider && torqueProvider != null) + PartModule pm = part.Modules[moduleIdx]; + if (!pm.IsDestroyed() && pm is ITorqueProvider torqueProvider) { torqueProvider.GetPotentialTorque(out Vector3 pos, out Vector3 neg); posTorque += pos; diff --git a/MultipleModuleInPartAPI/MultipleModuleInPartAPI.csproj b/MultipleModuleInPartAPI/MultipleModuleInPartAPI.csproj index 08970b5..3757c74 100644 --- a/MultipleModuleInPartAPI/MultipleModuleInPartAPI.csproj +++ b/MultipleModuleInPartAPI/MultipleModuleInPartAPI.csproj @@ -40,6 +40,14 @@ prompt 4 + + portable + true + bin\Profiling\ + TRACE + prompt + 4 + System (KSP/Mono)