From 07b9f1caa5796dca9fe9002736c1b4cb839a023b Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Sun, 12 Apr 2026 09:12:20 -0700 Subject: [PATCH 1/9] Fix inverse rotation maneuver bugs GetOrbitalStateVectorsAtUT() is particularly whack in the way that it applies the inverse rotation constructed at a future time via using Planetarium.ZupAtT(). That means that for the most part it is only useful for constructing values which can only be compared to other vectors constructed at the same time. This bug only occurs when the vessel is below the inverse rotation threshold, though, so most of the time works fine when there's no rotation being applied. The changes to RightHandedStateVectorsAtUT mean that we apply our own rotation in the current frames rotation to get RH rotating vectors. This is consistent with the old API, but should probably be retired and everything migrated to RH non-rotating vectors now that I can see how to get them out of the API correctly. This may also fix other bugs in consumers of the underlying maneuvers class (e.g. rendezvous autopilot, etc). Signed-off-by: Lamont Granquist --- .idea/.idea.MechJeb2/.idea/.name | 1 + MechJeb2.sln.DotSettings | 3 +++ MechJeb2/OrbitExtensions.cs | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .idea/.idea.MechJeb2/.idea/.name diff --git a/.idea/.idea.MechJeb2/.idea/.name b/.idea/.idea.MechJeb2/.idea/.name new file mode 100644 index 000000000..43692dea2 --- /dev/null +++ b/.idea/.idea.MechJeb2/.idea/.name @@ -0,0 +1 @@ +MechJeb2 \ No newline at end of file diff --git a/MechJeb2.sln.DotSettings b/MechJeb2.sln.DotSettings index feb718a5d..3bb805e6a 100644 --- a/MechJeb2.sln.DotSettings +++ b/MechJeb2.sln.DotSettings @@ -24,6 +24,7 @@ KSP LAN LD + LH LQR MET MJ @@ -32,6 +33,8 @@ PVG RC RCS + RH + RHS RK RO SI diff --git a/MechJeb2/OrbitExtensions.cs b/MechJeb2/OrbitExtensions.cs index 26cae7de2..7519f2201 100644 --- a/MechJeb2/OrbitExtensions.cs +++ b/MechJeb2/OrbitExtensions.cs @@ -71,7 +71,10 @@ public static class OrbitExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (V3 pos, V3 vel) RightHandedStateVectorsAtUT(this Orbit o, double ut) { - o.GetOrbitalStateVectorsAtUT(ut, out Vector3d pos, out Vector3d vel); + // GetOrbitalStateVectorsAtUT() uses a crazy future-rotation-at-UT to rotate vectors, so carefully avoid it. + o.GetOrbitalStateVectorsAtTrueAnomaly(o.TrueAnomalyAtT(o.getObtAtUT(ut)), ut, false, out Vector3d pos, out Vector3d vel); + pos = Planetarium.Zup.WorldToLocal(pos); + vel = Planetarium.Zup.WorldToLocal(vel); return (pos.ToV3(), vel.ToV3()); } From 92cb9f8f5de245d7a872ac410cef6e945e62329b Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Sun, 12 Apr 2026 14:05:20 -0700 Subject: [PATCH 2/9] Remove old orbit manipulation APIs These use GetOrbitalStateVectorsAtUT() which is problematic, they don't call Init() and have other sketchy looking behavior and nothing has used them in awhile. Signed-off-by: Lamont Granquist --- MechJeb2/OrbitExtensions.cs | 45 ------------------------------------- 1 file changed, 45 deletions(-) diff --git a/MechJeb2/OrbitExtensions.cs b/MechJeb2/OrbitExtensions.cs index 7519f2201..3669970fa 100644 --- a/MechJeb2/OrbitExtensions.cs +++ b/MechJeb2/OrbitExtensions.cs @@ -123,51 +123,6 @@ public static Vector3d North(this Orbit o, double ut) => public static Orbit PerturbedOrbit(this Orbit o, double ut, Vector3d dV) => MuUtils.OrbitFromStateVectors(o.WorldPositionAtUT(ut), o.WorldOrbitalVelocityAtUT(ut) + dV, o.referenceBody, ut); - // returns a new orbit that is identical to the current one (although the epoch will change) - // (i tried many different APIs in the orbit class, but the GetOrbitalStateVectors/UpdateFromStateVectors route was the only one that worked) - public static Orbit Clone(this Orbit o, double ut = double.NegativeInfinity) - { - // hack up a dynamic default value to the current time - if (double.IsNegativeInfinity(ut)) - ut = Planetarium.GetUniversalTime(); - - var newOrbit = new Orbit(); - o.GetOrbitalStateVectorsAtUT(ut, out Vector3d pos, out Vector3d vel); - newOrbit.UpdateFromStateVectors(pos, vel, o.referenceBody, ut); - - return newOrbit; - } - - // calculate the next patch, which makes patchEndTransition be valid - // - public static Orbit CalculateNextOrbit(this Orbit o, double ut = double.NegativeInfinity) - { - var solverParameters = new PatchedConics.SolverParameters(); - - // hack up a dynamic default value to the current time - if (double.IsNegativeInfinity(ut)) - ut = Planetarium.GetUniversalTime(); - - o.StartUT = ut; - o.EndUT = o.eccentricity >= 1.0 ? o.period : ut + o.period; - var nextOrbit = new Orbit(); - PatchedConics.CalculatePatch(o, nextOrbit, ut, solverParameters, null); - - return nextOrbit; - } - - // This does not allocate a new orbit object and the caller should call new Orbit if/when required - public static void MutatedOrbit(this Orbit o, double periodOffset = double.NegativeInfinity) - { - double ut = Planetarium.GetUniversalTime(); - - if (!periodOffset.IsFinite()) - return; - - o.GetOrbitalStateVectorsAtUT(ut + o.period * periodOffset, out Vector3d pos, out Vector3d vel); - o.UpdateFromStateVectors(pos, vel, o.referenceBody, ut); - } - // circular orbital speed at this instant [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double CircularOrbitSpeed(this Orbit o) => Astro.CircularVelocity(o.referenceBody.gravParameter, o.radius); From d189c9663d01beaadf3178e08c5ae039bbe99161 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Sun, 12 Apr 2026 14:14:59 -0700 Subject: [PATCH 3/9] Transfer planner: GetOrbitalStateVectorsAtUT fix This fixes a (now) obvious bug in the transfer planner in the use of GetOrbitalStateVectorsAtUT(), although it seems to only affect debug log output. Signed-off-by: Lamont Granquist --- MechJeb2/Maneuver/TransferCalculator.cs | 6 +++--- MechJeb2/OrbitExtensions.cs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/MechJeb2/Maneuver/TransferCalculator.cs b/MechJeb2/Maneuver/TransferCalculator.cs index e94e8897e..cc8ae8027 100644 --- a/MechJeb2/Maneuver/TransferCalculator.cs +++ b/MechJeb2/Maneuver/TransferCalculator.cs @@ -351,11 +351,11 @@ private void AdjustPeriapsis(ManeuverParameters maneuver, ref double utArrival) Debug.Log("maneuver guess dV: " + maneuver.dV); Debug.Log("maneuver guess UT: " + maneuver.UT); Debug.Log("arrival guess UT: " + utArrival); - _initialOrbit.GetOrbitalStateVectorsAtUT(maneuver.UT, out Vector3d r1, out Vector3d v1); + _initialOrbit.FixedGetOrbitalStateVectorsAtUT(maneuver.UT, out Vector3d r1, out Vector3d v1); Debug.Log($"initial orbit at {maneuver.UT} x = {r1}; v = {v1}"); - _initialOrbit.referenceBody.orbit.GetOrbitalStateVectorsAtUT(maneuver.UT, out Vector3d r2, out Vector3d v2); + _initialOrbit.referenceBody.orbit.FixedGetOrbitalStateVectorsAtUT(maneuver.UT, out Vector3d r2, out Vector3d v2); Debug.Log($"source at {maneuver.UT} x = {r2}; v = {v2}"); - _targetBody.orbit.GetOrbitalStateVectorsAtUT(utArrival, out Vector3d r3, out Vector3d v3); + _targetBody.orbit.FixedGetOrbitalStateVectorsAtUT(utArrival, out Vector3d r3, out Vector3d v3); Debug.Log($"source at {utArrival} x = {r3}; v = {v3}"); _impulseScale = maneuver.dV.magnitude; diff --git a/MechJeb2/OrbitExtensions.cs b/MechJeb2/OrbitExtensions.cs index 3669970fa..6450e0163 100644 --- a/MechJeb2/OrbitExtensions.cs +++ b/MechJeb2/OrbitExtensions.cs @@ -78,6 +78,14 @@ public static (V3 pos, V3 vel) RightHandedStateVectorsAtUT(this Orbit o, double return (pos.ToV3(), vel.ToV3()); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FixedGetOrbitalStateVectorsAtUT(this Orbit o, double ut, out Vector3d pos, out Vector3d vel) + { + o.GetOrbitalStateVectorsAtTrueAnomaly(o.TrueAnomalyAtT(o.getObtAtUT(ut)), ut, false, out pos, out vel); + pos = Planetarium.Zup.WorldToLocal(pos); + vel = Planetarium.Zup.WorldToLocal(vel); + } + //normalized vector perpendicular to the orbital plane //convention: as you look down along the orbit normal, the satellite revolves counterclockwise [MethodImpl(MethodImplOptions.AggressiveInlining)] From d6adb4eebfa871a668a02fe3189a3687d0272656 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Sun, 12 Apr 2026 15:55:46 -0700 Subject: [PATCH 4/9] Prevent Reflection errors from other mods tanking MJ Avoids using the KSP.IO.File.Exist() API that walks the loaded assemblies and throws, looking for the path to the assembly with the type T. The MuUtils helper replicates the side effect of this API of creating the directory. Since we construct the path afterwards anyway to load the file the only reason I can see for the reflection-driven-API is for that side-effect, and to cause weird bugs if someone ever moves the DLL location around and makes the two APIs start to disagree. --- MechJeb2/MechJebCore.cs | 4 ++-- MechJeb2/MechJebModuleWaypointWindow.cs | 2 +- MechJeb2/MuUtils.cs | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/MechJeb2/MechJebCore.cs b/MechJeb2/MechJebCore.cs index 36f6f2881..cf9333768 100644 --- a/MechJeb2/MechJebCore.cs +++ b/MechJeb2/MechJebCore.cs @@ -787,7 +787,7 @@ public override void OnLoad(ConfigNode sfsNode) LoadComputerModules(); var global = new ConfigNode("MechJebGlobalSettings"); - if (File.Exists("mechjeb_settings_global.cfg")) + if (MuUtils.FileExistsCreateDirectory(MuUtils.GetCfgPath("mechjeb_settings_global.cfg"))) { try { @@ -809,7 +809,7 @@ public override void OnLoad(ConfigNode sfsNode) vessel != null ? string.Join("_", vessel.vesselName.Split(Path.GetInvalidFileNameChars())) : ""; // Strip illegal char from the filename - if (vessel != null && File.Exists("mechjeb_settings_type_" + vesselName + ".cfg")) + if (vessel != null && MuUtils.FileExistsCreateDirectory(MuUtils.GetCfgPath("mechjeb_settings_type_" + vesselName + ".cfg"))) { try { diff --git a/MechJeb2/MechJebModuleWaypointWindow.cs b/MechJeb2/MechJebModuleWaypointWindow.cs index 5e3a50dca..de00bfff7 100644 --- a/MechJeb2/MechJebModuleWaypointWindow.cs +++ b/MechJeb2/MechJebModuleWaypointWindow.cs @@ -366,7 +366,7 @@ public override void OnLoad(ConfigNode local, ConfigNode type, ConfigNode global if (!FlightGlobals.ready) return; // bodies not loaded yet var wps = new ConfigNode("Routes"); - if (File.Exists("mechjeb_routes.cfg")) + if (MuUtils.FileExistsCreateDirectory(MuUtils.GetCfgPath("mechjeb_routes.cfg"))) { try { diff --git a/MechJeb2/MuUtils.cs b/MechJeb2/MuUtils.cs index b7fbdf829..4458776cb 100644 --- a/MechJeb2/MuUtils.cs +++ b/MechJeb2/MuUtils.cs @@ -11,6 +11,15 @@ public static class MuUtils public static string GetCfgPath(string file) => Path.Combine(_cfgPath, file); + // Duplicates the KSP.IO.File.Exist style API where it creates the directory if it doesn't exist + public static bool FileExistsCreateDirectory(string path) + { + string directoryName = Path.GetDirectoryName(path); + if (directoryName != null && !Directory.Exists(directoryName)) + Directory.CreateDirectory(directoryName); + return File.Exists(path); + } + public static string PadPositive(double x, string format = "F3") { string s = x.ToString(format); From 47b3328d52fc85d20a6c334c24c71f6cc4d08e96 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Tue, 14 Apr 2026 09:18:49 -0700 Subject: [PATCH 5/9] Fix another NRE fixes #1611 by just creating a lazy accessor Signed-off-by: Lamont Granquist --- MechJeb2/MechJebModuleAscentMenu.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/MechJeb2/MechJebModuleAscentMenu.cs b/MechJeb2/MechJebModuleAscentMenu.cs index 92ae1c1ec..d8f1117da 100644 --- a/MechJeb2/MechJebModuleAscentMenu.cs +++ b/MechJeb2/MechJebModuleAscentMenu.cs @@ -49,18 +49,11 @@ private bool _launchingToLan private bool _launchingWithAnyPlaneControl => _launchingToPlane || _launchingToRendezvous || _launchingToMatchLan || _launchingToLan; - private MechJebModuleAscentBaseAutopilot _autopilot => Core.Ascent; - private MechJebModuleAscentSettings _ascentSettings => Core.AscentSettings; - private MechJebModuleAscentClassicPathMenu _classicPathMenu; - private MechJebModuleAscentPVGSettingsMenu _pvgSettingsMenu; - private MechJebModuleAscentSettingsMenu _settingsMenu; - - public override void OnStart(PartModule.StartState state) - { - _pvgSettingsMenu = Core.GetComputerModule(); - _settingsMenu = Core.GetComputerModule(); - _classicPathMenu = Core.GetComputerModule(); - } + private MechJebModuleAscentBaseAutopilot _autopilot => Core.Ascent; + private MechJebModuleAscentSettings _ascentSettings => Core.AscentSettings; + private MechJebModuleAscentClassicPathMenu _classicPathMenu => Core.GetComputerModule(); + private MechJebModuleAscentPVGSettingsMenu _pvgSettingsMenu => Core.GetComputerModule(); + private MechJebModuleAscentSettingsMenu _settingsMenu => Core.GetComputerModule(); [UsedImplicitly] [Persistent(pass = (int)Pass.GLOBAL)] From d15b46c0d76183eb55ed21ade9c13c5c51eda2ad Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Thu, 16 Apr 2026 12:13:37 -0700 Subject: [PATCH 6/9] Fix staging controller for stock fairings somehow i had convinced myself the stock fairing was a moduledecouple when it is actually not. --- MechJeb2/MechJebModuleStagingController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MechJeb2/MechJebModuleStagingController.cs b/MechJeb2/MechJebModuleStagingController.cs index 9ef5e7c1b..7f17f7bd7 100644 --- a/MechJeb2/MechJebModuleStagingController.cs +++ b/MechJeb2/MechJebModuleStagingController.cs @@ -684,7 +684,7 @@ private bool HasFairingUncached(int inverseStage) // payload fairing now (fixing payload fairings causing stacks to not decouple). // if a user requires an interstage fairing that is alone in a stage with no stack decoupler, engine, or // anything else in the stage (for cinematics?) then the user MUST use proc fairings. - return _partsInStage.Slinq().All(p => p.IsDecoupler() && !p.IsLaunchClamp() && p.children.Count == 0); + return _partsInStage.Slinq().All(p => (p.IsDecoupler() || p.HasModule()) && !p.IsLaunchClamp() && p.children.Count == 0); } } } From b8fe8fe060ce8e3455478a9fbab6937907aea6da Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Thu, 16 Apr 2026 20:50:41 -0700 Subject: [PATCH 7/9] Fix stock fairings correctly This makes stock fairings work correctly when they also have a payload --- MechJeb2/MechJebModuleStagingController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MechJeb2/MechJebModuleStagingController.cs b/MechJeb2/MechJebModuleStagingController.cs index 7f17f7bd7..ef3364a31 100644 --- a/MechJeb2/MechJebModuleStagingController.cs +++ b/MechJeb2/MechJebModuleStagingController.cs @@ -684,7 +684,7 @@ private bool HasFairingUncached(int inverseStage) // payload fairing now (fixing payload fairings causing stacks to not decouple). // if a user requires an interstage fairing that is alone in a stage with no stack decoupler, engine, or // anything else in the stage (for cinematics?) then the user MUST use proc fairings. - return _partsInStage.Slinq().All(p => (p.IsDecoupler() || p.HasModule()) && !p.IsLaunchClamp() && p.children.Count == 0); + return _partsInStage.Slinq().All(p => ((p.IsDecoupler() && p.children.Count == 0) || p.HasModule() ) && !p.IsLaunchClamp()); } } } From 82bde4ca28e9e27ccc8243ecc1ac665088e4a924 Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Tue, 28 Apr 2026 12:48:25 -0700 Subject: [PATCH 8/9] Fix node executor to align COT to the node direction Should help fix e.g. shuttles work with the node executor Signed-off-by: Lamont Granquist --- MechJeb2/MechJebModuleNodeExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MechJeb2/MechJebModuleNodeExecutor.cs b/MechJeb2/MechJebModuleNodeExecutor.cs index 83473513a..dffee32e2 100644 --- a/MechJeb2/MechJebModuleNodeExecutor.cs +++ b/MechJeb2/MechJebModuleNodeExecutor.cs @@ -300,7 +300,7 @@ private void StateBurn() private void SetAttitude() { - Core.Attitude.attitudeTo(_worldDirection, AttitudeReference.INERTIAL, this, killRollRotation:KillRollRotation); + Core.Attitude.attitudeTo(_worldDirection, AttitudeReference.INERTIAL_COT, this, killRollRotation:KillRollRotation); } private bool ShouldTerminatePrincipia() From 70e6c322c6c5e792145413d6dfa342c82243163a Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Wed, 29 Apr 2026 14:27:50 -0700 Subject: [PATCH 9/9] v2.15.3 This also updates the other versions and establishes a more sensible versioning policy. --- MechJeb2/Properties/AssemblyInfo.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/MechJeb2/Properties/AssemblyInfo.cs b/MechJeb2/Properties/AssemblyInfo.cs index b70c7f70d..0a1df9a5d 100644 --- a/MechJeb2/Properties/AssemblyInfo.cs +++ b/MechJeb2/Properties/AssemblyInfo.cs @@ -23,18 +23,9 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("a903d9fe-4604-47b8-b9d9-95728538f769")] -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.5.1.0")] // We should not change it anymore. It break mods that links MJ ( cf http://support.microsoft.com/kb/556041 ) -[assembly: AssemblyFileVersion("2.15.2.0")] // this one we can change all we want +// We use a 2.[Major].[Minor/Patch].0 naming convention +[assembly: AssemblyVersion("2.15.0.0")] // This should be bumped for major versions/breaking changes when the 2nd number changes +[assembly: AssemblyFileVersion("2.15.3.0")] // this one is bumped every single time for both minor/patch [assembly: AssemblyInformationalVersion("")] // Displayed in the window title if not empty (used to display dev #) -[assembly: KSPAssembly("MechJeb2", 2, 5)] +[assembly: KSPAssembly("MechJeb2", 2, 15, 3)] // this one is bumped every single time for both minor/patch