Decompiled source of TechtonicaStutterFix v1.0.0

TechtonicaStutterFix.dll

Decompiled 4 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("TechtonicaStutterFix")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("TechtonicaStutterFix")]
[assembly: AssemblyTitle("TechtonicaStutterFix")]
[assembly: AssemblyVersion("1.0.0.0")]
[BepInPlugin("com.modder.techtonicastutterfix", "Techtonica Multiplayer Stutter Fix", "1.4.0")]
public class TechtonicaStutterFix : BaseUnityPlugin
{
	[HarmonyPatch(typeof(FactorySimManager), "SimUpdateAll")]
	public static class SimUpdateAll_Patch
	{
		public static void Prefix(FactorySimManager __instance, MachineManager[] ___machineManagers, int ___machineStateIndex, int ___prevMachineStateIndex, bool ____needInit, out long __state)
		{
			__state = Now();
			try
			{
				if (!ready || !FixEnabled.Value || !JitSnapshots.Value || !__instance.isMultiplayerClient || ____needInit || !TryPeekClientActionTick(out var tick))
				{
					return;
				}
				MachineManager val = ___machineManagers[___machineStateIndex];
				MachineManager val2 = ___machineManagers[___prevMachineStateIndex];
				if (val == null || val2 == null || val == val2)
				{
					return;
				}
				int curTick = val.curTick;
				if (tick < curTick)
				{
					lateActions++;
					return;
				}
				int num = curTick - val2.curTick;
				if (num >= 16 && !((double)num * tickEmaMs <= cloneEmaMs * 1.5))
				{
					createCache(__instance);
					jitSnaps++;
					errors = 0;
					__state = Now();
				}
			}
			catch (Exception e)
			{
				Fail(e);
			}
		}

		public static void Postfix(FactorySimManager __instance, MachineManager[] ___machineManagers, int ___machineStateIndex, int ___prevMachineStateIndex, bool ____needInit, long __state)
		{
			try
			{
				if (Diagnostics.Value)
				{
					MaybeReport();
				}
				if (!ready || !FixEnabled.Value || !__instance.isMultiplayerClient || ____needInit)
				{
					return;
				}
				MachineManager val = ___machineManagers[___machineStateIndex];
				MachineManager val2 = ___machineManagers[___prevMachineStateIndex];
				if (val == null || val2 == null || val == val2)
				{
					return;
				}
				int num = val.curTick - val2.curTick;
				int num2 = CurrentInterval();
				if (num > num2)
				{
					if (Ms(Now() - __state) > (double)DeferSimMs.Value && num < num2 * 2)
					{
						deferred++;
						return;
					}
					createCache(__instance);
					errors = 0;
				}
			}
			catch (Exception e)
			{
				Fail(e);
			}
		}
	}

	[HarmonyPatch]
	public static class UpdateAll_Timing
	{
		private static MethodBase TargetMethod()
		{
			return AccessTools.Method(typeof(MachineManager), "UpdateAll", new Type[2]
			{
				typeof(bool),
				typeof(bool)
			}, (Type[])null);
		}

		private static bool Prepare()
		{
			return TargetMethod() != null;
		}

		public static void Prefix()
		{
			tickStart = Now();
		}

		public static void Postfix()
		{
			tickEmaMs = Ema(tickEmaMs, Ms(Now() - tickStart), 0.01);
		}
	}

	[HarmonyPatch(typeof(FactorySimManager), "CreateNewCachedState")]
	public static class CreateNewCachedState_Timing
	{
		private static double worstClone
		{
			get
			{
				return worstCloneMs;
			}
			set
			{
				worstCloneMs = value;
			}
		}

		public static void Prefix(out long __state)
		{
			__state = Now();
		}

		public static void Postfix(long __state)
		{
			double num = Ms(Now() - __state);
			cloneEmaMs = Ema(cloneEmaMs, num, 0.3);
			snaps++;
			if (num > worstClone)
			{
				worstClone = num;
			}
		}
	}

	[HarmonyPatch(typeof(FactorySimManager), "SimulateClientMessages")]
	public static class SimulateClientMessages_Timing
	{
		public static void Prefix(MachineManager[] ___machineManagers, int ___machineStateIndex, int ___prevMachineStateIndex, out long __state)
		{
			__state = Now();
			if (!Diagnostics.Value)
			{
				return;
			}
			try
			{
				lastRewindGap = ___machineManagers[___machineStateIndex].curTick - ___machineManagers[___prevMachineStateIndex].curTick;
			}
			catch
			{
				lastRewindGap = -1;
			}
		}

		public static void Postfix(long __state)
		{
			if (Diagnostics.Value)
			{
				double num = Ms(Now() - __state);
				rewinds++;
				if (num > worstRewindMs)
				{
					worstRewindMs = num;
				}
				if (num >= (double)SpikeLogMs.Value)
				{
					((BaseUnityPlugin)Instance).Logger.LogInfo((object)$"[diag] remote-action rewind took {num:F1} ms (re-simulated ~{lastRewindGap} ticks)");
				}
			}
		}
	}

	internal static TechtonicaStutterFix Instance;

	private static ConfigEntry<bool> FixEnabled;

	private static ConfigEntry<bool> Adaptive;

	private static ConfigEntry<bool> JitSnapshots;

	private static ConfigEntry<bool> Diagnostics;

	private static ConfigEntry<int> MinInterval;

	private static ConfigEntry<float> ResimBudgetMs;

	private static ConfigEntry<float> CloneThresholdMs;

	private static ConfigEntry<float> DeferSimMs;

	private static ConfigEntry<float> SpikeLogMs;

	private const int VanillaInterval = 512;

	private static readonly double TicksToMs = 1000.0 / (double)Stopwatch.Frequency;

	private static Action<FactorySimManager> createCache;

	private static bool ready;

	private static int errors;

	private static double tickEmaMs;

	private static double cloneEmaMs;

	private static long tickStart;

	private static int snaps;

	private static int jitSnaps;

	private static int lateActions;

	private static int deferred;

	private static int rewinds;

	private static double worstRewindMs;

	private static double worstCloneMs;

	private static int lastRewindGap;

	private static long nextReport;

	private Harmony _harmony;

	private void Awake()
	{
		//IL_0091: Unknown result type (might be due to invalid IL or missing references)
		//IL_009b: Expected O, but got Unknown
		//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d8: Expected O, but got Unknown
		//IL_010b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0115: Expected O, but got Unknown
		//IL_0148: Unknown result type (might be due to invalid IL or missing references)
		//IL_0152: Expected O, but got Unknown
		//IL_0223: Unknown result type (might be due to invalid IL or missing references)
		//IL_022d: Expected O, but got Unknown
		Instance = this;
		FixEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Fix", "Enabled", true, "Master switch.");
		JitSnapshots = ((BaseUnityPlugin)this).Config.Bind<bool>("Fix", "SnapshotBeforeRemoteActions", true, "When a remote action is queued at/ahead of the live tick and the snapshot is stale, snapshot first so the replay is short.");
		Adaptive = ((BaseUnityPlugin)this).Config.Bind<bool>("Fix", "Adaptive", true, "Pick the periodic interval from measured sim/clone cost. If off, MinSnapshotIntervalTicks is used as a fixed interval.");
		MinInterval = ((BaseUnityPlugin)this).Config.Bind<int>("Fix", "MinSnapshotIntervalTicks", 256, new ConfigDescription("Floor for the periodic interval; it also bounds how far back late actions can rewind (~1x-2x this many ticks; 64 ticks = 1 s). Too low risks discarded actions/desync.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 512), Array.Empty<object>()));
		ResimBudgetMs = ((BaseUnityPlugin)this).Config.Bind<float>("Fix", "ResimBudgetMs", 60f, new ConfigDescription("Target max re-sim time in one frame for late actions.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 500f), Array.Empty<object>()));
		CloneThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Fix", "CloneThresholdMs", 8f, new ConfigDescription("If a clone costs more than this, periodic snapshots are spaced out proportionally (up to 4x).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 100f), Array.Empty<object>()));
		DeferSimMs = ((BaseUnityPlugin)this).Config.Bind<float>("Fix", "DeferSnapshotIfSimOverMs", 10f, new ConfigDescription("Skip a due periodic snapshot this frame if SimUpdateAll already took longer than this (deferred at most until 2x overdue).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 100f), Array.Empty<object>()));
		SpikeLogMs = ((BaseUnityPlugin)this).Config.Bind<float>("Debug", "LogRewindsOverMs", 20f, "With Diagnostics on, log every remote-action rewind slower than this.");
		Diagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "Diagnostics", false, "Log timing stats every 10 s.");
		try
		{
			MethodInfo mi = AccessTools.Method(typeof(FactorySimManager), "CreateNewCachedState", (Type[])null, (Type[])null);
			if (mi == null || mi.GetParameters().Length != 0)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"CreateNewCachedState not found; fix inactive (game update?).");
				return;
			}
			try
			{
				createCache = AccessTools.MethodDelegate<Action<FactorySimManager>>(mi, (object)null, true);
			}
			catch
			{
				createCache = delegate(FactorySimManager o)
				{
					mi.Invoke(o, null);
				};
			}
			ready = true;
			_harmony = new Harmony("com.modder.techtonicastutterfix");
			_harmony.PatchAll();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Techtonica Stutter Fix v1.4.0 loaded.");
		}
		catch (Exception ex)
		{
			ready = false;
			((BaseUnityPlugin)this).Logger.LogError((object)("Failed to initialize: " + ex));
		}
	}

	private void OnDestroy()
	{
		Harmony harmony = _harmony;
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
	}

	private static double Ms(long ticks)
	{
		return (double)ticks * TicksToMs;
	}

	private static double Ema(double old, double sample, double a)
	{
		if (!(old <= 0.0))
		{
			return old * (1.0 - a) + sample * a;
		}
		return sample;
	}

	private static long Now()
	{
		return Stopwatch.GetTimestamp();
	}

	private static void Fail(Exception e)
	{
		if (++errors >= 3)
		{
			ready = false;
			((BaseUnityPlugin)Instance).Logger.LogError((object)("Fix disabled after repeated errors: " + e));
		}
	}

	private static int CurrentInterval()
	{
		int value = MinInterval.Value;
		if (!Adaptive.Value)
		{
			return value;
		}
		double num = ((tickEmaMs > 0.0001) ? ((double)ResimBudgetMs.Value / tickEmaMs) : 512.0);
		if (cloneEmaMs > (double)CloneThresholdMs.Value)
		{
			num *= Math.Min(4.0, cloneEmaMs / (double)CloneThresholdMs.Value);
		}
		return (int)Math.Max(value, Math.Min(512.0, num));
	}

	private static bool TryPeekClientActionTick(out int tick)
	{
		tick = 0;
		NetworkMessageRelay instance = NetworkMessageRelay.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return false;
		}
		Queue<NetworkActionInfo> clientQueue = instance.ClientQueue;
		if (clientQueue == null || clientQueue.Count == 0)
		{
			return false;
		}
		tick = clientQueue.Peek().tick;
		return true;
	}

	private static void MaybeReport()
	{
		long num = Now();
		if (num >= nextReport)
		{
			nextReport = num + 10 * Stopwatch.Frequency;
			int num2 = CurrentInterval();
			((BaseUnityPlugin)Instance).Logger.LogInfo((object)($"[diag] tick={tickEmaMs:F2}ms clone={cloneEmaMs:F1}ms interval={num2} " + $"(est. worst late-action resim {(double)num2 * tickEmaMs:F0}ms) snapshots={snaps} (before-action={jitSnaps}) " + $"lateActions={lateActions} deferred={deferred} rewinds={rewinds} " + $"worstRewind={worstRewindMs:F0}ms worstClone={worstCloneMs:F0}ms"));
			snaps = (jitSnaps = (lateActions = (deferred = (rewinds = 0))));
			worstRewindMs = (worstCloneMs = 0.0);
		}
	}
}