Skip to content

Commit

Permalink
Make docking ports conserve momentum (#160)
Browse files Browse the repository at this point in the history
Averages the docking force between the two ports. This fixes the
[docking port Kraken drive](https://wiki.kerbalspaceprogram.com/wiki/Docking_Port_Drive)
and other unphysical behavior.
  • Loading branch information
Jules-Bertholet committed Sep 13, 2023
1 parent d98a25b commit d7218b6
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
4 changes: 4 additions & 0 deletions GameData/KSPCommunityFixes/Settings.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ KSP_COMMUNITY_FIXES
// Fix leaking a camera and spotlight created by the thumbnail system on certain failures
ThumbnailSpotlight = true

// Make docking ports converve momentum by averaging acquire forces between the two ports.
// Notably, docking port Kraken drives will no longer work.
DockingPortConserveMomentum = true

// ##########################
// Obsolete bugfixes
// ##########################
Expand Down
241 changes: 241 additions & 0 deletions KSPCommunityFixes/BugFixes/DockingPortConserveMomentum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;

namespace KSPCommunityFixes.BugFixes
{
class DockingPortConserveMomentum : BasePatch
{
protected override Version VersionMin => new Version(1, 12, 3);

protected override void ApplyPatches(List<PatchInfo> patches)
{
// We need to patch a closure, which doesn't have a stable method name.
// So we patch everything that *might* be the closure we are looking for,
// but in the transpiler body we inspect the code contents to make sure we found the right one
// before making any modifications.

Traverse dockingNodeTraverse = Traverse.Create<ModuleDockingNode>();
foreach (string methodName in dockingNodeTraverse.Methods())
{
if (methodName.StartsWith("<SetupFSM>") && !methodName.Contains("_Patch"))
{
patches.Add(new PatchInfo(
PatchMethodType.Transpiler,
AccessTools.Method(typeof(ModuleDockingNode), methodName),
this,
"ModuleDockingNode_SetupFSMClosure_Transpiler"));
}
}
}

// Reimplementation of https://harmony.pardeike.net/api/HarmonyLib.CodeInstruction.html#HarmonyLib_CodeInstruction_StoreLocal_System_Int32_,
// which seems not to be available for some reason?
static CodeInstruction CodeInstructionStoreLocal(int index)
{
switch (index)
{
case 0:
return new CodeInstruction(OpCodes.Stloc_0);

case 1:
return new CodeInstruction(OpCodes.Stloc_1);

case 2:
return new CodeInstruction(OpCodes.Stloc_2);

case 3:
return new CodeInstruction(OpCodes.Stloc_3);

default:
if (index < 256)
{
return new CodeInstruction(OpCodes.Stloc_S, Convert.ToByte(index));
}
else
{
return new CodeInstruction(OpCodes.Stloc, index);
}

}
}

static CodeInstruction CodeInstructionLoadLocal(int index)
{
switch (index)
{
case 0:
return new CodeInstruction(OpCodes.Ldloc_0);

case 1:
return new CodeInstruction(OpCodes.Ldloc_1);

case 2:
return new CodeInstruction(OpCodes.Ldloc_2);

case 3:
return new CodeInstruction(OpCodes.Ldloc_3);

default:
if (index < 256)
{
return new CodeInstruction(OpCodes.Ldloc_S, Convert.ToByte(index));
}
else
{
return new CodeInstruction(OpCodes.Ldloc, index);
}
}
}

// Checks whether a sequence of instructions looks like the closure we want to patch, , by inspecting which fields it loads.
static bool IsTargetClosure(List<CodeInstruction> instructions)
{
bool acquireForce = false;
bool acquireTorque = false;
bool acquireTorqueRoll = false;
bool acquireForceTweak = false;
bool otherNode = false;

foreach (CodeInstruction instr in instructions)
{
if (instr.opcode == OpCodes.Ldfld)
{
if (!acquireForce && Equals(instr.operand, typeof(ModuleDockingNode).GetField("acquireForce")))
{
acquireForce = true;
}
else if (!acquireTorque && Equals(instr.operand, typeof(ModuleDockingNode).GetField("acquireTorque")))
{
acquireTorque = true;
}
else if (!acquireTorqueRoll && Equals(instr.operand, typeof(ModuleDockingNode).GetField("acquireTorqueRoll")))
{
acquireTorqueRoll = true;
}
else if (!acquireForceTweak && Equals(instr.operand, typeof(ModuleDockingNode).GetField("acquireForceTweak")))
{
acquireForceTweak = true;
}
else if (!otherNode && Equals(instr.operand, typeof(ModuleDockingNode).GetField("otherNode")))
{
otherNode = true;
}
}
}

return acquireForce & acquireTorque & acquireTorqueRoll & acquireForceTweak & otherNode;
}

static IEnumerable<CodeInstruction> ModuleDockingNode_SetupFSMClosure_Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator ilGen)
{
List<CodeInstruction> instrList = instructions.ToList();
// Check if this closure is the one we want to patch.
if (IsTargetClosure(instrList))
{
// This looks like the closure we want to patch, patch it.

// First, calculate the averages of the force values between the two modules.

LocalBuilder avgAcquireForce = ilGen.DeclareLocal(typeof(float));
LocalBuilder avgAcquireTorque = ilGen.DeclareLocal(typeof(float));
LocalBuilder avgAcquireTorqueRoll = ilGen.DeclareLocal(typeof(float));

// calculate avgAcquireForce
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForce");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForce");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Add);
yield return CodeInstructionStoreLocal(avgAcquireForce.LocalIndex);

// calculate avgAcquireTorque
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireTorque");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireTorque");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Add);
yield return CodeInstructionStoreLocal(avgAcquireTorque.LocalIndex);

// calculate avgAcquireTorqueRoll
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireTorqueRoll");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireForceTweak");
yield return new CodeInstruction(OpCodes.Ldarg_0); // this
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "otherNode");
yield return CodeInstruction.LoadField(typeof(ModuleDockingNode), "acquireTorqueRoll");
yield return new CodeInstruction(OpCodes.Mul);

yield return new CodeInstruction(OpCodes.Add);
yield return CodeInstructionStoreLocal(avgAcquireTorqueRoll.LocalIndex);

foreach (CodeInstruction instr in instrList)
{
// Replace any uses of the individual module force values with the average between both modules.

if (instr.LoadsField(typeof(ModuleDockingNode).GetField("acquireForceTweak")))
{
yield return new CodeInstruction(OpCodes.Pop);
// Why 0.5 and not 1? We didn't divide by 2 when calculating the average earlier, so we do it now.
yield return new CodeInstruction(OpCodes.Ldc_R4, 0.5f);
}
else if (instr.LoadsField(typeof(ModuleDockingNode).GetField("acquireForce")))
{
yield return new CodeInstruction(OpCodes.Pop);
yield return CodeInstructionLoadLocal(avgAcquireForce.LocalIndex);
}
else if (instr.LoadsField(typeof(ModuleDockingNode).GetField("acquireTorque")))
{
yield return new CodeInstruction(OpCodes.Pop);
yield return CodeInstructionLoadLocal(avgAcquireTorque.LocalIndex);
}
else if (instr.LoadsField(typeof(ModuleDockingNode).GetField("acquireTorqueRoll")))
{
yield return new CodeInstruction(OpCodes.Pop);
yield return CodeInstructionLoadLocal(avgAcquireTorqueRoll.LocalIndex);
}
else
{
yield return instr;
}
}
}
else
{
// This doesn't look like our patch target, pass it on unmodified.
foreach (CodeInstruction instr in instrList)
{
yield return instr;
}
}
}
}
}
1 change: 1 addition & 0 deletions KSPCommunityFixes/KSPCommunityFixes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
<Compile Include="BugFixes\StickySplashedFixer.cs" />
<Compile Include="BugFixes\AsteroidSpawnerUniqueFlightId.cs" />
<Compile Include="BugFixes\AutoStrutDrift.cs" />
<Compile Include="BugFixes\DockingPortConserveMomentum.cs" />
<Compile Include="BugFixes\DockingPortRotationDriftAndFixes.cs" />
<Compile Include="BugFixes\ExtendedDeployableParts.cs" />
<Compile Include="BugFixes\DeltaVHideWhenDisabled.cs" />
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ User options are available from the "ESC" in-game settings menu :<br/><img src="
- [**ReRootPreserveSurfaceAttach**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/142) [KSP 1.8.0 - 1.12.5]<br/>Disable the stock behavior of altering surface attachment nodes on re-rooting, a questionable QoL feature that doesn't work correctly, leading to permanently borked attachement nodes.
- [**ThumbnailSpotlight**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/149) [KSP 1.12.0 - 1.12.5], fix rogue spotlight staying in the scene when a part thumbnail fails to be generated.
- [**FixGetUnivseralTime**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/155) [KSP 1.8.0 - 1.12.5]<br/>Fix Planetarium.GetUniversalTime returning bad values in the editor.
- [**DockingPortConserveMomentum**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/160) [KSP 1.12.3 - 1.12.5]<br/>Make docking ports conserve momentum by averaging the acquire force between the two ports. Notably, docking port Kraken drives will no longer work.

#### Quality of Life tweaks

Expand Down Expand Up @@ -174,6 +175,10 @@ If doing so in the `Debug` configuration and if your KSP install is modified to

### Changelog

##### 1.31.0

- New KSP bugfix : [**DockingPortConserveMomentum**](https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/160) [KSP 1.12.3 - 1.12.5], make docking ports conserve momentum by averaging the acquire forces between the two ports.

##### 1.30.0
- **DragCubeGeneration** : disabled by default since it continues to cause issues with fairings and some other parts. Will be reenabled by default when issues are fixed.
- New KSP bugfix : [**FixGetUnivseralTime**](https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/155) [KSP 1.8.0 - 1.12.5], fix Planetarium.GetUniversalTime returning bad values in the editor.
Expand Down

0 comments on commit d7218b6

Please sign in to comment.