Decompiled source of TimelinePerformanceFix v0.1.0

BepInEx/plugins/TimelinePerformanceFix/TimelinePerformanceFix.dll

Decompiled 3 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Peak;
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("TactiKot")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Fixes timeline-related frame-rate drops by correcting player-history sampling and snapshot refresh.")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+fdedd981a1dab24a23ce6c35d0e2f457e4dd735c")]
[assembly: AssemblyProduct("Timeline Performance Fix")]
[assembly: AssemblyTitle("Timeline Performance Fix")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace TimelinePerformanceFix
{
	internal sealed class DiagnosticThrottle
	{
		private readonly object sync = new object();

		private readonly double interval;

		private bool reported;

		private double lastReport;

		private long suppressed;

		internal DiagnosticThrottle(double intervalSeconds)
		{
			interval = intervalSeconds;
		}

		internal bool ShouldReport(double nowSeconds, out long suppressedSinceLastReport)
		{
			lock (sync)
			{
				suppressedSinceLastReport = 0L;
				if (reported && nowSeconds - lastReport < interval)
				{
					if (suppressed < long.MaxValue)
					{
						suppressed++;
					}
					return false;
				}
				suppressedSinceLastReport = suppressed;
				suppressed = 0L;
				reported = true;
				lastReport = nowSeconds;
				return true;
			}
		}
	}
	internal static class IdentityPatches
	{
		private const string JoinOwner = "com.tactikot.peak.alwaysjoin";

		internal static HarmonyMethod AfterJoin(Type type, string method)
		{
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: Expected O, but got Unknown
			HarmonyMethod val = new HarmonyMethod(type, method, (Type[])null);
			val.after = new string[1] { "com.tactikot.peak.alwaysjoin" };
			return val;
		}

		internal static IEnumerable<CodeInstruction> HostSpawn(IEnumerable<CodeInstruction> input)
		{
			List<CodeInstruction> list = input.Select((Func<CodeInstruction, CodeInstruction>)((CodeInstruction val) => new CodeInstruction(val))).ToList();
			int i = Single(list, (CodeInstruction instruction) => IsLookup(instruction));
			int num = Previous(list, i);
			int num2 = Previous(list, num);
			Require(IsAddress(list[num2]) && IsAddress(list[num]), "host output arguments");
			int num3 = Previous(list, num2);
			int num4 = (IsCall(list[num3], "System.Int32", "ToString") ? Previous(list, Previous(list, Previous(list, Previous(list, num3)))) : num3);
			Require(IsCall(list[num4], "Photon.Realtime.Player", "get_UserId"), "host UserId source");
			Require(IdentityEnd(list, num4) == num2, "host identity argument");
			return list;
		}

		internal static IEnumerable<CodeInstruction> Quicksave(IEnumerable<CodeInstruction> input)
		{
			List<CodeInstruction> code = input.Select((Func<CodeInstruction, CodeInstruction>)((CodeInstruction i) => new CodeInstruction(i))).ToList();
			int[] array = (from i in Enumerable.Range(0, code.Count)
				where IsCall(code[i], "Peak.Quicksave+PlayerRunData", "get_UserId")
				select i).ToArray();
			Require(array.Length == 2, "quicksave UserId sources");
			int num = IdentityEnd(code, array[0]);
			Require(IsCall(code[num], "Portningsbolaget.Platforms.PlatformManager", "get_Platform"), "host platform");
			num = Next(code, num);
			Require(IsCall(code[num], "Portningsbolaget.Platforms.IPlatform", "get_UserID"), "host platform UserID");
			int num2 = Next(code, num);
			int num3 = Next(code, num2);
			int num4 = Next(code, num3);
			Require(IsStore(code[num2]) && IsAddress(code[num3]) && SameLocal(code[num2], code[num3]) && IsCall(code[num4], "System.UInt64", "ToString"), "host UserID string conversion");
			num = IdentityEnd(code, num4);
			Require(IsCall(code[num], "System.String", "op_Equality"), "exact host identity comparison");
			num = IdentityEnd(code, array[1]);
			Require(IsAddress(code[num]), "saved reconnect data address");
			num = Next(code, num);
			Require(IsCall(code[num], "Peak.Quicksave+PlayerRunData", "GetReconnectData"), "saved reconnect data");
			num = Next(code, num);
			Require(IsAddress(code[num]), "saved achievements address");
			num = Next(code, num);
			Require(IsCall(code[num], "Peak.Quicksave+PlayerRunData", "GetAchievementProgress"), "saved achievements");
			num = Next(code, num);
			Require(IsCall(code[num], "ReconnectHandler", "PopulateReconnectRecord"), "saved record insertion");
			return code;
		}

		private static int IdentityEnd(List<CodeInstruction> code, int source)
		{
			int num = Next(code, source);
			if (!IsCall(code[num], "System.Object", "GetHashCode") && !IsCall(code[num], "System.String", "GetHashCode"))
			{
				return num;
			}
			int num2 = Next(code, num);
			int num3 = Next(code, num2);
			int num4 = Next(code, num3);
			Require(code[num].operand is MethodInfo { IsStatic: false } methodInfo && methodInfo.ReturnType == typeof(int) && methodInfo.GetParameters().Length == 0 && IsStore(code[num2]) && IsAddress(code[num3]) && SameLocal(code[num2], code[num3]) && IsCall(code[num4], "System.Int32", "ToString") && code[num4].operand is MethodInfo { IsStatic: false } methodInfo2 && methodInfo2.ReturnType == typeof(string) && methodInfo2.GetParameters().Length == 0, "identity hash conversion");
			for (int i = num; i <= num4; i++)
			{
				IList list = (IList)typeof(CodeInstruction).GetField("labels").GetValue(code[i]);
				Require((i == num || list.Count == 0) && code[i].blocks.Count == 0, "identity control-flow boundaries");
				code[i].opcode = OpCodes.Nop;
				code[i].operand = null;
			}
			return Next(code, num4);
		}

		private static bool IsLookup(CodeInstruction instruction)
		{
			if (!(instruction.operand is MethodInfo { IsStatic: not false } methodInfo) || methodInfo.ReturnType != typeof(bool))
			{
				return false;
			}
			ParameterInfo[] parameters = methodInfo.GetParameters();
			if (parameters.Length == 3 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType.FullName == "ReconnectData&" && parameters[2].ParameterType.FullName == "SerializableRunBasedValues&")
			{
				if (!IsCall(instruction, "ReconnectHandler", "TryGetReconnectData"))
				{
					return IsCall(instruction, "AlwaysJoin.SpawnCoordinator", "Resolve");
				}
				return true;
			}
			return false;
		}

		private static int Single(List<CodeInstruction> code, Func<CodeInstruction, bool> predicate)
		{
			int[] array = (from i in Enumerable.Range(0, code.Count)
				where predicate(code[i])
				select i).ToArray();
			Require(array.Length == 1, "one host reconnect lookup");
			return array[0];
		}

		private static int Next(List<CodeInstruction> code, int i)
		{
			do
			{
				i++;
				Require(i < code.Count, "instruction after identity");
			}
			while (code[i].opcode == OpCodes.Nop);
			return i;
		}

		private static int Previous(List<CodeInstruction> code, int i)
		{
			do
			{
				i--;
				Require(i >= 0, "instruction before identity");
			}
			while (code[i].opcode == OpCodes.Nop);
			return i;
		}

		private static bool IsCall(CodeInstruction i, string type, string name)
		{
			if ((i.opcode == OpCodes.Call || i.opcode == OpCodes.Callvirt) && i.operand is MethodInfo methodInfo && methodInfo.DeclaringType?.FullName == type)
			{
				return methodInfo.Name == name;
			}
			return false;
		}

		private static bool IsAddress(CodeInstruction i)
		{
			if (!(i.opcode == OpCodes.Ldloca))
			{
				return i.opcode == OpCodes.Ldloca_S;
			}
			return true;
		}

		private static bool IsStore(CodeInstruction i)
		{
			if (!(i.opcode == OpCodes.Stloc) && !(i.opcode == OpCodes.Stloc_S) && !(i.opcode == OpCodes.Stloc_0) && !(i.opcode == OpCodes.Stloc_1) && !(i.opcode == OpCodes.Stloc_2))
			{
				return i.opcode == OpCodes.Stloc_3;
			}
			return true;
		}

		private static bool SameLocal(CodeInstruction a, CodeInstruction b)
		{
			if (Local(a) >= 0)
			{
				return Local(a) == Local(b);
			}
			return false;
		}

		private static int Local(CodeInstruction i)
		{
			if (i.opcode == OpCodes.Stloc_0)
			{
				return 0;
			}
			if (i.opcode == OpCodes.Stloc_1)
			{
				return 1;
			}
			if (i.opcode == OpCodes.Stloc_2)
			{
				return 2;
			}
			if (i.opcode == OpCodes.Stloc_3)
			{
				return 3;
			}
			if (i.operand is LocalBuilder localBuilder)
			{
				return localBuilder.LocalIndex;
			}
			if (i.operand is LocalVariableInfo localVariableInfo)
			{
				return localVariableInfo.LocalIndex;
			}
			object operand = i.operand;
			if (operand is int)
			{
				return (int)operand;
			}
			operand = i.operand;
			if (operand is byte)
			{
				return (byte)operand;
			}
			operand = i.operand;
			if (operand is short)
			{
				return (short)operand;
			}
			return -1;
		}

		private static void Require(bool condition, string path)
		{
			if (!condition)
			{
				throw new InvalidOperationException("Unsupported player identity code: " + path + ".");
			}
		}
	}
	internal static class IlPatches
	{
		internal static IEnumerable<CodeInstruction> GuardSnapshotConstructor(IEnumerable<CodeInstruction> instructions)
		{
			List<CodeInstruction> list = instructions.ToList();
			if (list.Count != 12 || list[0].opcode != OpCodes.Ldarg_1 || list[1].opcode != OpCodes.Callvirt || !(list[1].operand is MethodInfo methodInfo) || methodInfo.Name != "get_Count" || methodInfo.ReturnType != typeof(int) || list[2].opcode != OpCodes.Ldc_I4 || !object.Equals(list[2].operand, 512) || (list[3].opcode != OpCodes.Ble_S && list[3].opcode != OpCodes.Ble) || !Targets(list[3], list[8]) || list[4].opcode != OpCodes.Ldarg_1 || list[5].opcode != OpCodes.Ldc_I4 || !object.Equals(list[5].operand, 512) || !Calls(list[6], "CharacterStats", "Downsample") || (list[7].opcode != OpCodes.Starg_S && list[7].opcode != OpCodes.Starg) || list[8].opcode != OpCodes.Ldarg_0 || list[9].opcode != OpCodes.Ldarg_1 || !Calls(list[10], "CharacterStats+SyncData", "set_Timeline") || list[11].opcode != OpCodes.Ret)
			{
				throw new InvalidOperationException("Unsupported SyncData constructor; expected its original 512-point ownership path.");
			}
			return list;
		}

		private static bool Targets(CodeInstruction branch, CodeInstruction destination)
		{
			IEnumerable source = (IEnumerable)typeof(CodeInstruction).GetField("labels").GetValue(destination);
			if (branch.operand?.GetType().FullName == "System.Reflection.Emit.Label")
			{
				return source.Cast<object>().Any((object label) => object.Equals(label, branch.operand));
			}
			return false;
		}

		internal static List<CodeInstruction> FixUpdatePeriod(IEnumerable<CodeInstruction> instructions, MethodInfo periodicUpdater)
		{
			if (!periodicUpdater.IsStatic || periodicUpdater.ReturnType != typeof(void) || periodicUpdater.GetParameters().Length != 2 || periodicUpdater.GetParameters().Any((ParameterInfo p) => p.ParameterType != typeof(object)))
			{
				throw new ArgumentException("Periodic updater must have signature static void Update(object, object).", "periodicUpdater");
			}
			List<CodeInstruction> list = instructions.Select((Func<CodeInstruction, CodeInstruction>)((CodeInstruction i) => new CodeInstruction(i))).ToList();
			List<int> list2 = new List<int>();
			for (int num = 0; num + 9 < list.Count; num++)
			{
				if (Calls(list[num], "Photon.Pun.PhotonNetwork", "get_ServerTimestamp") && LoadsLocal(list[num + 1].opcode) && list[num + 2].opcode == OpCodes.Ldfld && list[num + 2].operand is FieldInfo fieldInfo && fieldInfo.DeclaringType?.FullName == "ReconnectHandler+ReconnectDataRecord" && fieldInfo.Name == "Timestamp" && list[num + 3].opcode == OpCodes.Sub && list[num + 4].opcode == OpCodes.Ldc_I4 && list[num + 4].operand is int num2 && num2 == 1000 && list[num + 6].opcode == OpCodes.Ldarg_0 && LoadsLocal(list[num + 7].opcode) && list[num + 8].opcode == OpCodes.Callvirt && list[num + 8].operand is MethodInfo methodInfo && methodInfo.DeclaringType?.FullName == "Player" && methodInfo.Name == "get_character" && IsPeriodicPath(list[num + 5], list[num + 9]))
				{
					list2.Add(num + 5);
				}
			}
			if (list2.Count != 1)
			{
				throw new InvalidOperationException($"Expected one PEAK reconnect interval check; found {list2.Count}. No interval patch applied.");
			}
			int num3 = list2[0];
			if (list[num3].opcode == OpCodes.Blt || list[num3].opcode == OpCodes.Blt_S)
			{
				return list;
			}
			list[num3].opcode = ((list[num3].opcode == OpCodes.Bge_S) ? OpCodes.Blt_S : OpCodes.Blt);
			list[num3 + 4].operand = periodicUpdater;
			return list;
		}

		private static bool IsPeriodicPath(CodeInstruction branch, CodeInstruction call)
		{
			if (!(call.operand is MethodInfo methodInfo) || call.opcode != OpCodes.Call || methodInfo.ReturnType != typeof(void))
			{
				return false;
			}
			ParameterInfo[] parameters = methodInfo.GetParameters();
			if (branch.opcode == OpCodes.Bge || branch.opcode == OpCodes.Bge_S)
			{
				if (methodInfo.DeclaringType?.FullName == "ReconnectHandler" && methodInfo.Name == "UpdateReconnectData" && !methodInfo.IsStatic && parameters.Length == 1)
				{
					return parameters[0].ParameterType.FullName == "Character";
				}
				return false;
			}
			if ((branch.opcode == OpCodes.Blt || branch.opcode == OpCodes.Blt_S) && methodInfo.IsStatic && methodInfo.Name == "PeriodicRefresh" && methodInfo.DeclaringType?.FullName == "AlwaysJoin.GameHooks" && parameters.Length == 2 && parameters[0].ParameterType.FullName == "ReconnectHandler")
			{
				return parameters[1].ParameterType.FullName == "Character";
			}
			return false;
		}

		private static bool Calls(CodeInstruction instruction, string type, string method)
		{
			if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo methodInfo && methodInfo.DeclaringType?.FullName == type)
			{
				return methodInfo.Name == method;
			}
			return false;
		}

		private static bool LoadsLocal(OpCode code)
		{
			if (!(code == OpCodes.Ldloc) && !(code == OpCodes.Ldloc_S) && !(code == OpCodes.Ldloc_0) && !(code == OpCodes.Ldloc_1) && !(code == OpCodes.Ldloc_2))
			{
				return code == OpCodes.Ldloc_3;
			}
			return true;
		}
	}
	internal sealed class PeriodicRefreshGate
	{
		private sealed class Attempt
		{
			internal double LastAttempt = double.NegativeInfinity;
		}

		private readonly ConditionalWeakTable<object, Attempt> attempts = new ConditionalWeakTable<object, Attempt>();

		internal bool ShouldAttempt(object character, double nowSeconds)
		{
			Attempt value = attempts.GetValue(character, (object _) => new Attempt());
			lock (value)
			{
				if (nowSeconds - value.LastAttempt < 1.0)
				{
					return false;
				}
				value.LastAttempt = nowSeconds;
				return true;
			}
		}
	}
	[BepInProcess("PEAK.exe")]
	[BepInPlugin("com.tactikot.peak.timelineperformancefix", "Timeline Performance Fix", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Id = "com.tactikot.peak.timelineperformancefix";

		public const string DisplayName = "Timeline Performance Fix";

		public const string Version = "0.1.0";

		private static readonly PeriodicRefreshGate PeriodicRefreshes = new PeriodicRefreshGate();

		private static MethodInfo? updateSnapshot;

		private Harmony? harmony;

		private void Awake()
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Expected O, but got Unknown
			//IL_010d: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Expected O, but got Unknown
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0187: Expected O, but got Unknown
			//IL_0187: Expected O, but got Unknown
			//IL_01dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01eb: Expected O, but got Unknown
			TimelinePatch.Log = ((BaseUnityPlugin)this).Logger;
			harmony = new Harmony("com.tactikot.peak.timelineperformancefix");
			try
			{
				TimelinePatch.VerifyNoteLayout();
				Type obj = AccessTools.TypeByName("ReconnectHandler") ?? throw new MissingMemberException("ReconnectHandler was not found.");
				Type type = AccessTools.TypeByName("CharacterStats") ?? throw new MissingMemberException("CharacterStats was not found.");
				MethodInfo methodInfo = AccessTools.DeclaredMethod(obj, "Update", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("ReconnectHandler.Update was not found.");
				updateSnapshot = obj.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "UpdateReconnectData" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.FullName == "Character" && m.ReturnType == typeof(void));
				MethodInfo methodInfo2 = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Downsample" && m.GetParameters().Length == 2 && m.GetParameters()[1].ParameterType == typeof(int));
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, IdentityPatches.AfterJoin(typeof(Plugin), "UpdateTranspiler"), (HarmonyMethod)null, (HarmonyMethod)null);
				harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(TimelinePatch), "Prefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				ConstructorInfo constructorInfo = typeof(SyncData).GetConstructor(new Type[1] { typeof(List<TimelineInfo>) }) ?? throw new MissingMethodException("SyncData timeline constructor was not found.");
				harmony.Patch((MethodBase)constructorInfo, new HarmonyMethod(typeof(SnapshotPatch), "PrepareSnapshot", (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(IlPatches), "GuardSnapshotConstructor", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null);
				MethodInfo methodInfo3 = AccessTools.DeclaredMethod(type, "GetCaughtUp", new Type[2]
				{
					typeof(SyncData),
					typeof(bool)
				}, (Type[])null) ?? throw new MissingMethodException("CharacterStats.GetCaughtUp was not found.");
				harmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(SnapshotPatch), "PrepareRestore", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				MethodInfo methodInfo4 = typeof(CharacterSpawner).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name.Contains("g__HostUpdate") && m.GetParameters().Length == 0);
				MethodInfo methodInfo5 = AccessTools.DeclaredMethod(typeof(Quicksave), "PopulateMapAndPlayerStates", (Type[])null, (Type[])null) ?? throw new MissingMethodException("Quicksave.PopulateMapAndPlayerStates was not found.");
				harmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, (HarmonyMethod)null, IdentityPatches.AfterJoin(typeof(IdentityPatches), "HostSpawn"), (HarmonyMethod)null, (HarmonyMethod)null);
				harmony.Patch((MethodBase)methodInfo5, (HarmonyMethod)null, (HarmonyMethod)null, IdentityPatches.AfterJoin(typeof(IdentityPatches), "Quicksave"), (HarmonyMethod)null, (HarmonyMethod)null);
				UpdatePeriodicSnapshot(null, null);
				TimelinePatch.VerifyInstalledPatch();
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Enabled: event-preserving timeline selection with time-based LTTB replaces CharacterStats.Downsample. Snapshots and restored live histories own separate lists. Periodic reconnect attempts are limited to once per second per character; event-driven updates remain immediate. Host lookup and quicksave import use exact UserId strings, including when AlwaysJoin is present. Network data formats are unchanged.");
			}
			catch (Exception arg)
			{
				RemoveOwnPatches();
				((BaseUnityPlugin)this).Logger.LogError((object)$"Fix was not enabled; its patches were removed. Unsupported game code or conflicting patch: {arg}");
			}
		}

		private static IEnumerable<CodeInstruction> UpdateTranspiler(IEnumerable<CodeInstruction> instructions)
		{
			return IlPatches.FixUpdatePeriod(instructions, typeof(Plugin).GetMethod("UpdatePeriodicSnapshot", BindingFlags.Static | BindingFlags.NonPublic));
		}

		private static void UpdatePeriodicSnapshot(object handler, object character)
		{
			Object val = (Object)((character is Object) ? character : null);
			if (val == null || !Object.op_Implicit(val))
			{
				return;
			}
			double nowSeconds = (double)Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
			if (!PeriodicRefreshes.ShouldAttempt(character, nowSeconds))
			{
				return;
			}
			try
			{
				updateSnapshot.Invoke(handler, new object[1] { character });
			}
			catch (TargetInvocationException ex) when (ex.InnerException != null)
			{
				ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
			}
		}

		private void OnDestroy()
		{
			RemoveOwnPatches();
		}

		private void RemoveOwnPatches()
		{
			if (harmony != null)
			{
				MethodBase[] array = harmony.GetPatchedMethods().ToArray();
				foreach (MethodBase methodBase in array)
				{
					harmony.Unpatch(methodBase, (HarmonyPatchType)0, "com.tactikot.peak.timelineperformancefix");
				}
				harmony = null;
			}
		}
	}
	internal static class SnapshotPatch
	{
		internal const int Capacity = 512;

		internal static void PrepareSnapshot(ref List<TimelineInfo> __0)
		{
			if (__0 == null)
			{
				throw new ArgumentNullException("__0");
			}
			if (__0.Count <= 512)
			{
				__0 = new List<TimelineInfo>(__0);
			}
		}

		internal static void PrepareRestore(ref SyncData __0)
		{
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			__0 = new SyncData(((SyncData)(ref __0)).Timeline);
		}
	}
	internal readonly struct TimelineSample
	{
		internal readonly double Time;

		internal readonly double Height;

		internal readonly byte Note;

		internal readonly int Biome;

		internal TimelineSample(double time, double height, byte note = 0, int biome = 0)
		{
			Time = time;
			Height = height;
			Note = note;
			Biome = biome;
		}
	}
	internal readonly struct TimelineSelection
	{
		internal readonly int[] Indices;

		internal readonly int OmittedEvents;

		internal readonly int OmittedBoundaries;

		internal readonly bool UsedIndexAxis;

		internal TimelineSelection(int[] indices, int events = 0, int boundaries = 0, bool indexAxis = false)
		{
			Indices = indices;
			OmittedEvents = events;
			OmittedBoundaries = boundaries;
			UsedIndexAxis = indexAxis;
		}
	}
	internal static class TimelineDownsampler
	{
		private sealed class Segment
		{
			internal readonly int Left;

			internal readonly int Right;

			internal readonly double Weight;

			internal int Quota;

			internal double Priority => Weight / (double)(Quota + 1);

			internal bool Full => Quota == Right - Left - 1;

			internal Segment(int left, int right, double weight)
			{
				Left = left;
				Right = right;
				Weight = weight;
			}
		}

		private sealed class AllocationHeap
		{
			private readonly List<Segment> segments;

			private readonly int[] heap;

			private int count;

			internal AllocationHeap(List<Segment> segments)
			{
				this.segments = segments;
				heap = new int[segments.Count];
				for (int i = 0; i < segments.Count; i++)
				{
					if (!segments[i].Full)
					{
						heap[count++] = i;
					}
				}
				for (int num = count / 2 - 1; num >= 0; num--)
				{
					Down(num);
				}
			}

			internal void AllocateOne()
			{
				if (count == 0)
				{
					throw new InvalidOperationException("Timeline intervals have insufficient capacity.");
				}
				Segment segment = segments[heap[0]];
				segment.Quota++;
				if (segment.Full)
				{
					heap[0] = heap[--count];
				}
				if (count > 0)
				{
					Down(0);
				}
			}

			private bool Higher(int a, int b)
			{
				int num = segments[a].Priority.CompareTo(segments[b].Priority);
				if (num <= 0)
				{
					if (num == 0)
					{
						return segments[a].Left < segments[b].Left;
					}
					return false;
				}
				return true;
			}

			private void Down(int index)
			{
				while (index * 2 + 1 < count)
				{
					int num = index * 2 + 1;
					if (num + 1 < count && Higher(heap[num + 1], heap[num]))
					{
						num++;
					}
					if (Higher(heap[num], heap[index]))
					{
						int num2 = heap[index];
						heap[index] = heap[num];
						heap[num] = num2;
						index = num;
						continue;
					}
					break;
				}
			}
		}

		internal static TimelineSelection Select(IReadOnlyList<TimelineSample> input, int target)
		{
			if (input == null)
			{
				throw new ArgumentNullException("input");
			}
			int count = input.Count;
			target = Math.Min(count, Math.Max(0, target));
			if (target == count)
			{
				int[] array = new int[count];
				for (int i = 0; i < count; i++)
				{
					array[i] = i;
				}
				return new TimelineSelection(array);
			}
			byte[] array2 = new byte[count];
			for (int j = 0; j < count; j++)
			{
				if (input[j].Note > 2)
				{
					array2[j] = 3;
				}
				else if (input[j].Note != 0)
				{
					array2[j] = Math.Max(array2[j], (byte)1);
				}
				if (j > 0 && (input[j].Note != input[j - 1].Note || input[j].Biome != input[j - 1].Biome))
				{
					array2[j - 1] = Math.Max(array2[j - 1], (byte)2);
					array2[j] = Math.Max(array2[j], (byte)2);
				}
			}
			bool[] array3 = new bool[count];
			double[] x = null;
			double[] y = null;
			bool indexAxis = false;
			int num = 0;
			if (target > 0)
			{
				array3[count - 1] = true;
				num++;
			}
			if (target > 1)
			{
				array3[0] = true;
				num++;
			}
			byte b = 3;
			while (b > 0 && num < target)
			{
				List<int> list = new List<int>();
				for (int k = 0; k < count; k++)
				{
					if (!array3[k] && array2[k] == b)
					{
						list.Add(k);
					}
				}
				int num2 = target - num;
				if (list.Count <= num2)
				{
					foreach (int item in list)
					{
						array3[item] = true;
					}
					num += list.Count;
				}
				else if (b > 1)
				{
					if (x == null)
					{
						BuildCoordinates(input, out x, out y, out indexAxis);
					}
					SelectOverflow(list, num2, x, y, array3);
					num += num2;
				}
				b--;
			}
			if (num < target)
			{
				if (x == null)
				{
					BuildCoordinates(input, out x, out y, out indexAxis);
				}
				List<Segment> list2 = new List<Segment>();
				int num3 = 0;
				for (int l = 1; l < count; l++)
				{
					if (array3[l])
					{
						if (l - num3 > 1)
						{
							double num4 = x[l] - x[num3];
							list2.Add(new Segment(num3, l, (num4 > 0.0) ? num4 : ((double)(l - num3) / (double)(count - 1))));
						}
						num3 = l;
					}
				}
				AllocationHeap allocationHeap = new AllocationHeap(list2);
				for (int num5 = target - num; num5 > 0; num5--)
				{
					allocationHeap.AllocateOne();
				}
				foreach (Segment item2 in list2)
				{
					SelectTriangles(item2, x, y, array3);
				}
			}
			int[] array4 = new int[target];
			int num6 = 0;
			int num7 = 0;
			int num8 = 0;
			for (int m = 0; m < count; m++)
			{
				if (array3[m])
				{
					array4[num6++] = m;
				}
				else if (array2[m] == 3)
				{
					num7++;
				}
				else if (array2[m] == 2)
				{
					num8++;
				}
			}
			if (num6 != target)
			{
				throw new InvalidOperationException("Timeline selection violated its size invariant.");
			}
			return new TimelineSelection(array4, num7, num8, indexAxis);
		}

		private static void SelectOverflow(List<int> candidates, int quota, double[] x, double[] y, bool[] kept)
		{
			int num = candidates.Count + 1;
			double[] array = new double[num + 1];
			double[] array2 = new double[num + 1];
			bool[] array3 = new bool[num + 1];
			array[0] = x[0];
			array2[0] = y[0];
			array[num] = x[^1];
			array2[num] = y[^1];
			for (int i = 0; i < candidates.Count; i++)
			{
				array[i + 1] = x[candidates[i]];
				array2[i + 1] = y[candidates[i]];
			}
			SelectTriangles(new Segment(0, num, 1.0)
			{
				Quota = quota
			}, array, array2, array3);
			for (int j = 0; j < candidates.Count; j++)
			{
				if (array3[j + 1])
				{
					kept[candidates[j]] = true;
				}
			}
		}

		private static void BuildCoordinates(IReadOnlyList<TimelineSample> input, out double[] x, out double[] y, out bool indexAxis)
		{
			int count = input.Count;
			x = new double[count];
			y = new double[count];
			double time = input[0].Time;
			double num = input[count - 1].Time - time;
			indexAxis = !Finite(time) || !Finite(num) || num <= 0.0;
			double num2 = double.PositiveInfinity;
			double num3 = double.NegativeInfinity;
			for (int i = 0; i < count; i++)
			{
				if (!Finite(input[i].Time) || (i > 0 && input[i].Time < input[i - 1].Time))
				{
					indexAxis = true;
				}
				if (Finite(input[i].Height))
				{
					num2 = Math.Min(num2, input[i].Height);
					num3 = Math.Max(num3, input[i].Height);
				}
			}
			double num4 = num3 - num2;
			for (int j = 0; j < count; j++)
			{
				x[j] = (indexAxis ? ((double)j / (double)(count - 1)) : ((input[j].Time - time) / num));
				y[j] = ((Finite(input[j].Height) && Finite(num4) && num4 > 0.0) ? ((input[j].Height - num2) / num4) : 0.0);
			}
		}

		private static bool Finite(double value)
		{
			if (!double.IsNaN(value))
			{
				return !double.IsInfinity(value);
			}
			return false;
		}

		private static void SelectTriangles(Segment segment, double[] x, double[] y, bool[] kept)
		{
			int quota = segment.Quota;
			int num = segment.Right - segment.Left - 1;
			if (quota == 0)
			{
				return;
			}
			if (quota == num)
			{
				for (int i = segment.Left + 1; i < segment.Right; i++)
				{
					kept[i] = true;
				}
				return;
			}
			int num2 = segment.Left;
			int[] array = TimeBuckets(segment, x);
			for (int j = 0; j < quota; j++)
			{
				int num3 = array[j];
				int num4 = array[j + 1];
				int num5 = ((j + 1 == quota) ? (segment.Right + 1) : array[j + 2]);
				double num6 = 0.0;
				double num7 = 0.0;
				for (int k = num4; k < num5; k++)
				{
					num6 += x[k];
					num7 += y[k];
				}
				num6 /= (double)(num5 - num4);
				num7 /= (double)(num5 - num4);
				int num8 = num3;
				double num9 = -1.0;
				double num10 = double.PositiveInfinity;
				double num11 = (x[num3] + x[num4 - 1]) / 2.0;
				for (int l = num3; l < num4; l++)
				{
					double num12 = Math.Abs((x[num2] - num6) * (y[l] - y[num2]) - (x[num2] - x[l]) * (num7 - y[num2]));
					double num13 = Math.Abs(x[l] - num11);
					if (num12 > num9 || (num12 == num9 && num13 < num10))
					{
						num9 = num12;
						num10 = num13;
						num8 = l;
					}
				}
				kept[num8] = true;
				num2 = num8;
			}
		}

		private static int[] TimeBuckets(Segment segment, double[] x)
		{
			int quota = segment.Quota;
			int num = segment.Left + 1;
			double num2 = x[segment.Right] - x[segment.Left];
			List<Segment> list = new List<Segment>();
			int num3 = -1;
			for (int i = num; i < segment.Right; i++)
			{
				double num4 = ((num2 > 0.0) ? ((x[i] - x[segment.Left]) / num2) : ((double)(i - segment.Left) / (double)(segment.Right - segment.Left)));
				int num5 = Math.Min(quota - 1, Math.Max(0, (int)(num4 * (double)quota)));
				if (num5 != num3)
				{
					if (i > num)
					{
						list.Add(new Segment(num - 1, i, 1.0)
						{
							Quota = 1
						});
					}
					num = i;
					num3 = num5;
				}
			}
			list.Add(new Segment(num - 1, segment.Right, 1.0)
			{
				Quota = 1
			});
			AllocationHeap allocationHeap = new AllocationHeap(list);
			for (int num6 = quota - list.Count; num6 > 0; num6--)
			{
				allocationHeap.AllocateOne();
			}
			int[] array = new int[quota + 1];
			int num7 = 0;
			foreach (Segment item in list)
			{
				for (int j = 0; j < item.Quota; j++)
				{
					array[num7++] = item.Left + 1 + (int)((long)j * (long)(item.Right - item.Left - 1) / item.Quota);
				}
			}
			array[num7] = segment.Right;
			return array;
		}
	}
	internal static class TimelinePatch
	{
		private static readonly DiagnosticThrottle OverflowReports = new DiagnosticThrottle(30.0);

		internal static ManualLogSource? Log;

		internal static void VerifyNoteLayout()
		{
			Type typeFromHandle = typeof(TimelineNote);
			if (Enum.GetUnderlyingType(typeFromHandle) != typeof(byte) || Enum.GetName(typeFromHandle, (byte)0) != "None" || Enum.GetName(typeFromHandle, (byte)1) != "PassedOut" || Enum.GetName(typeFromHandle, (byte)2) != "Dead")
			{
				throw new InvalidOperationException("Unsupported timeline note layout; persistent states cannot be classified safely.");
			}
		}

		internal static bool Prefix(List<TimelineInfo> __0, int __1, ref List<TimelineInfo> __result)
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Expected I4, but got Unknown
			//IL_0058: Expected I4, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			if (__0 == null)
			{
				throw new ArgumentNullException("__0");
			}
			if (__0.Count <= __1)
			{
				__result = __0;
				return false;
			}
			TimelineSample[] array = new TimelineSample[__0.Count];
			for (int i = 0; i < array.Length; i++)
			{
				TimelineInfo val = __0[i];
				array[i] = new TimelineSample(((TimelineInfo)(ref val)).time, ((TimelineInfo)(ref val)).height, (byte)(int)((TimelineInfo)(ref val)).Note, (int)val.biome);
			}
			TimelineSelection timelineSelection = TimelineDownsampler.Select(array, __1);
			__result = new List<TimelineInfo>(timelineSelection.Indices.Length);
			int[] indices = timelineSelection.Indices;
			foreach (int index in indices)
			{
				__result.Add(__0[index]);
			}
			if (timelineSelection.OmittedEvents != 0 || timelineSelection.OmittedBoundaries != 0)
			{
				double nowSeconds = (double)Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
				if (OverflowReports.ShouldReport(nowSeconds, out var suppressedSinceLastReport))
				{
					ManualLogSource? log = Log;
					if (log != null)
					{
						log.LogWarning((object)($"Timeline capacity {Math.Max(0, __1)} cannot retain every semantic marker: " + $"{timelineSelection.OmittedEvents} events and {timelineSelection.OmittedBoundaries} transition anchors omitted. " + $"Retained markers span the history. Repeated reports suppressed: {suppressedSinceLastReport}."));
					}
				}
			}
			return false;
		}

		internal static void VerifyInstalledPatch()
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_0105: Unknown result type (might be due to invalid IL or missing references)
			//IL_0184: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_0234: Unknown result type (might be due to invalid IL or missing references)
			//IL_0270: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c0: Unknown result type (might be due to invalid IL or missing references)
			List<TimelineInfo> list = MakeHistory(6);
			list[4] = new TimelineInfo((BiomeType)0, (ushort)4, (ushort)4, (TimelineNote)4);
			List<TimelineInfo> list2 = CharacterStats.Downsample(list, 4);
			if (list2.Count != 4 || !list2.Exists((TimelineInfo p) => p.timestamp == 4 && (int)((TimelineInfo)(ref p)).Note == 4) || list2[0].timestamp != 0 || list2[3].timestamp != 5)
			{
				throw new InvalidOperationException("Patched timeline method failed its late-event startup check.");
			}
			List<TimelineInfo> list3 = MakeHistory(726);
			list3[724] = new TimelineInfo((BiomeType)0, (ushort)724, (ushort)724, (TimelineNote)4);
			list2 = CharacterStats.Downsample(list3, 512);
			if (list2.Count != 512 || !list2.Exists((TimelineInfo p) => p.timestamp == 724 && (int)((TimelineInfo)(ref p)).Note == 4))
			{
				throw new InvalidOperationException("Patched timeline method failed its 512-point startup check.");
			}
			for (int num = 0; num < list3.Count; num++)
			{
				int index = num;
				ushort num2 = (ushort)num;
				TimelineInfo val = list3[num];
				list3[index] = new TimelineInfo((BiomeType)0, num2, (ushort)0, ((TimelineInfo)(ref val)).Note);
			}
			list2 = CharacterStats.Downsample(list3, 512);
			if (list2.Count != 512 || !list2.Exists((TimelineInfo p) => (int)((TimelineInfo)(ref p)).Note == 4))
			{
				throw new InvalidOperationException("Patched timeline method failed its equal-time startup check.");
			}
			List<TimelineInfo> list4 = new List<TimelineInfo>(1024);
			for (int num3 = 0; num3 < 512; num3++)
			{
				list4.Add(new TimelineInfo((BiomeType)0, (ushort)0, (ushort)(num3 * 100), (TimelineNote)0));
			}
			for (int num4 = 1; num4 <= 512; num4++)
			{
				list4.Add(new TimelineInfo((BiomeType)0, (ushort)0, (ushort)(51100 + num4), (TimelineNote)0));
			}
			SyncData val2 = new SyncData(list4);
			list2 = ((SyncData)(ref val2)).Timeline;
			if (list2.Count != 512 || list2.FindAll((TimelineInfo p) => p.timestamp > 51100).Count > 32 || list4.Count != 1024 || list2[0].timestamp != 0 || list2[511].timestamp != 51612)
			{
				throw new InvalidOperationException("Patched SyncData failed its nonuniform-time startup check.");
			}
			List<TimelineInfo> list5 = MakeHistory(512);
			SyncData val3 = default(SyncData);
			((SyncData)(ref val3))..ctor(list5);
			list5.Add(new TimelineInfo((BiomeType)0, (ushort)512, (ushort)512, (TimelineNote)4));
			if (((SyncData)(ref val3)).Timeline.Count != 512 || ((SyncData)(ref val3)).Timeline == list5)
			{
				throw new InvalidOperationException("Snapshot timeline is still aliased to the live history.");
			}
			SyncData __ = val3;
			SnapshotPatch.PrepareRestore(ref __);
			((SyncData)(ref __)).Timeline.Add(new TimelineInfo((BiomeType)0, (ushort)513, (ushort)513, (TimelineNote)5));
			if (((SyncData)(ref val3)).Timeline.Count != 512 || ((SyncData)(ref __)).Timeline == ((SyncData)(ref val3)).Timeline)
			{
				throw new InvalidOperationException("Restored history is still aliased to the snapshot.");
			}
			ManualLogSource? log = Log;
			if (log != null)
			{
				log.LogInfo((object)"Timeline startup checks passed in Unity/Mono: late events, 726 -> 512, equal timestamps, nonuniform SyncData, independent snapshot and restore lists.");
			}
		}

		private static List<TimelineInfo> MakeHistory(int count)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			List<TimelineInfo> list = new List<TimelineInfo>(count);
			for (int i = 0; i < count; i++)
			{
				list.Add(new TimelineInfo((BiomeType)0, (ushort)i, (ushort)i, (TimelineNote)0));
			}
			return list;
		}
	}
}