diff --git a/README.md b/README.md index 6289c97..a07488f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Currently, this mod includes the following life hacks: - **VAB Mission Tracker** by [munix](https://github.com/jan-bures) - Adds the Mission Tracker button to the VAB app tray. - **Disable Contrails** by [munix](https://github.com/jan-bures) - Disables contrails and wingtip vortices. Disabled by default, see the [Configuration](#Configuration) section for details. +- **Better Experiments** by [dmarcuse](https://github.com/dmarcuse) - Automatically resumes paused experiments when + re-entering the correct region, and ignores animation state. ## Installation ### Recommended 1. Use [CKAN](https://github.com/KSP-CKAN/CKAN/releases/latest) to download and install Kerbal Life Hacks. diff --git a/src/KerbalLifeHacks/Hacks/BetterExperiments/BetterExperiments.cs b/src/KerbalLifeHacks/Hacks/BetterExperiments/BetterExperiments.cs new file mode 100644 index 0000000..40826b9 --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/BetterExperiments/BetterExperiments.cs @@ -0,0 +1,91 @@ +using System.Reflection.Emit; +using HarmonyLib; +using KSP.Sim.impl; +using System.Reflection; +using KSP.Modules; +using KSP.Game.Science; + +namespace KerbalLifeHacks.Hacks.BetterExperiments; + +using PCMSE = PartComponentModule_ScienceExperiment; + +[Hack("Automatically resume paused experiments when they return to the appropriate region, and progress without waiting for animations", true)] +public class BetterExperiments : BaseHack +{ + public static BetterExperiments Instance; + + public override void OnInitialized() + { + Instance = this; + HarmonyInstance.PatchAll(typeof(BetterExperiments)); + } + + /// + /// Harmony transpiler to ignore part deployment state when running an + /// experiment, allowing it to progress during part animations. + /// + [HarmonyPatch(typeof(PCMSE), nameof(PCMSE.OnUpdate))] + [HarmonyTranspiler] + public static IEnumerable IgnorePartAnimationState(IEnumerable instructions) + { + FieldInfo dataScienceExpField = typeof(PCMSE) + .GetField(nameof(PCMSE.dataScienceExperiment), AccessTools.all); + FieldInfo isPartDeployedField = typeof(Data_ScienceExperiment) + .GetField(nameof(Data_ScienceExperiment.PartIsDeployed), AccessTools.all); + + return new CodeMatcher(instructions) + .MatchStartForward( + new CodeMatch(OpCodes.Ldarg_0), + new CodeMatch(OpCodes.Ldfld, dataScienceExpField), + new CodeMatch(OpCodes.Ldfld, isPartDeployedField) + ) + .Repeat( + matcher => matcher + // Rather than removing the instructions outright, we carefully + // replace them with nop/ldc to preserve labels. + .SetAndAdvance(OpCodes.Nop, null) + .SetAndAdvance(OpCodes.Nop, null) + .SetAndAdvance(OpCodes.Ldc_I4_1, null), + notFoundAction: message => + Instance.Logger.LogWarning($"did not find experiment animation code! {message}") + ) + .InstructionEnumeration(); + } + + /// + /// Automatically resume experiments when re-entering the correct research + /// location for a paused experiment. + /// + [HarmonyPatch(typeof(PCMSE), nameof(PCMSE.OnScienceSituationChanged))] + [HarmonyPostfix] + public static void AutomaticallyResumeExperiment(PCMSE __instance) + { + var newLocation = new ResearchLocation( + requiresRegion: __instance._currentLocation.RequiresRegion, + bodyName: __instance._currentLocation.BodyName, + scienceSituation: __instance._currentLocation.ScienceSituation, + scienceRegion: __instance._currentLocation.ScienceRegion + ); + + ref var standings = ref __instance.dataScienceExperiment.ExperimentStandings; + if (standings.Any(exp => exp.CurrentExperimentState == ExperimentState.RUNNING)) + { + // Only one experiment can run at a time, so bail out + return; + } + + for (int i = 0; i < standings.Count; i++) + { + newLocation.RequiresRegion = standings[i].RegionRequired; + if (standings[i].CurrentExperimentState == ExperimentState.PAUSED && standings[i].ExperimentLocation.Equals(newLocation)) + { + Instance.Logger.LogInfo($"Resuming experiment {__instance.Part.PartName}/{standings[i].ExperimentID}"); + // Experiment runtime can dip into negative values during high time warp, + // which causes RunExperiment to reset the experiment progress + standings[i].CurrentRunningTime = Math.Max(standings[i].CurrentRunningTime, 0.01); + __instance.RunExperiment(standings[i].ExperimentID); + return; + } + } + } +}