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);
}
}
}