Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make docking ports conserve momentum #160

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading