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