using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using LLBML.Messages; using LLBML.Players; using LLBML.Settings; using LLBML.Utils; using Microsoft.CodeAnalysis; using Multiplayer; using SyncFix.FrameRecorder; using SyncFix.Utils; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: AssemblyCompany("ca.gov.mechasoulindustries.llb.my.cute.syncfix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.1.0")] [assembly: AssemblyInformationalVersion("1.0.1+02a17a1dab34791fd4f9d0ed063a7f4c997a6ea2")] [assembly: AssemblyProduct("Sync Fix")] [assembly: AssemblyTitle("ca.gov.mechasoulindustries.llb.my.cute.syncfix")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 SyncFix { public class FrameAccumulator { public readonly int playerIndex; public readonly float updateRate; public readonly float threshold; public float currentValue = -1f; public float accumulator; public float upperBound; public float lowerBound; public Func upperBoundFunc; public Func lowerBoundFunc; public FrameAccumulator(int playerIndex, float updateRate, float threshold) { this.playerIndex = playerIndex; this.updateRate = updateRate; this.threshold = threshold; } public void Reset() { currentValue = -1f; accumulator = 0f; } private void UpdateValue(float newValue) { if (currentValue == -1f) { currentValue = newValue; } else { currentValue = Mathf.Lerp(currentValue, newValue, updateRate); } } public void FrameUpdate(int frame, float frameValue) { upperBound = upperBoundFunc(frame, playerIndex); lowerBound = lowerBoundFunc(frame, playerIndex); _ = currentValue; UpdateValue(frameValue); if (currentValue > upperBound) { float num = currentValue - upperBound; accumulator += num; } else if (currentValue < lowerBound) { float num2 = currentValue - lowerBound; accumulator = Math.Max(accumulator + num2, 0f); } } public bool ThresholdReached() { return accumulator >= threshold; } public bool ThresholdVeryReached() { return accumulator >= threshold * 10f; } } public interface ITimeSyncComponent { void FrameUpdate(); float GetSleepInterval(); void OnSleep(float frames); void Reset(); bool ShouldEmergencySleep(); } [BepInPlugin("ca.gov.mechasoulindustries.llb.my.cute.syncfix", "Sync Fix", "1.0.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInProcess("LLBlaze.exe")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Logger; private Harmony _harmony; public static Plugin Instance { get; private set; } private void Awake() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown Logger = ((BaseUnityPlugin)this).Logger; Instance = this; SyncFixConfig.LoadConfig(((BaseUnityPlugin)this).Config); PathUtils.Init(((BaseUnityPlugin)this).Info); _harmony = new Harmony("ca.gov.mechasoulindustries.llb.my.cute.syncfix"); _harmony.PatchAll(); Logger.LogInfo((object)"Plugin ca.gov.mechasoulindustries.llb.my.cute.syncfix is loaded!"); } private void Start() { StateManager.RegisterLobbyMessages(); SyncFixManager.RegisterGameMessages(); ModDependenciesUtils.RegisterToModMenu(((BaseUnityPlugin)this).Info, new List { "Fixes host advantage" }); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } public class RollbackStats { private static int numRollbacks = 0; private static float total = 0f; private static float recentSize = -1f; private static int numSleeps = 0; public static int NumRollbacks => numRollbacks; public static float Average => total / (float)NumRollbacks; public static float RecentSize => recentSize; public static int NumSleeps => numSleeps; public static void Reset() { numRollbacks = 0; total = 0f; recentSize = -1f; numSleeps = 0; } public static void AddRollback(int size) { numRollbacks++; total += size; if (recentSize == -1f) { recentSize = size; } else { recentSize = Mathf.Lerp(recentSize, (float)size, 0.1f); } } public static void AddSleep(float size) { numSleeps++; } public static string GetStats() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("rollbacks: "); stringBuilder.Append(NumRollbacks); stringBuilder.AppendLine(); stringBuilder.Append("average: "); stringBuilder.Append(Average); stringBuilder.AppendLine(); stringBuilder.Append("recent: "); stringBuilder.Append(RecentSize); stringBuilder.AppendLine(); stringBuilder.Append("sleeps: "); stringBuilder.Append(NumSleeps); stringBuilder.AppendLine(); return stringBuilder.ToString(); } } public class StateManager { public enum SyncFixMode { SOLO, GROUP } public enum LobbyPeerModStatus { UNKNOWN, CONFIRMED } public static SyncFixMode CurrentMode = SyncFixMode.SOLO; private static readonly LobbyPeerModStatus[] peerModStatus = new LobbyPeerModStatus[4]; public static bool HostHasSyncFix = false; public static void RegisterLobbyMessages() { MessageApi.RegisterCustomMessage(((BaseUnityPlugin)Plugin.Instance).Info, (ushort)5040, SyncFixMessages.LOBBY_MOD_CHECK.ToString(), (Action)ReceiveModCheck); MessageApi.RegisterCustomMessage(((BaseUnityPlugin)Plugin.Instance).Info, (ushort)5041, SyncFixMessages.LOBBY_MOD_REPLY.ToString(), (Action)ReceiveModReply); MessageApi.RegisterCustomMessage(((BaseUnityPlugin)Plugin.Instance).Info, (ushort)5042, SyncFixMessages.GAME_USE_GROUP.ToString(), (Action)ReceiveGroupMessage); } public static void PeerJoined(int playerIndex) { if (ShouldManageState()) { if (IsLocalPeer(playerIndex)) { SetPeerModStatus(playerIndex, LobbyPeerModStatus.CONFIRMED); } else { SendModCheck(playerIndex); } } } public static void PeerLeft(int playerIndex) { if (ShouldManageState()) { SetPeerModStatus(playerIndex, LobbyPeerModStatus.UNKNOWN); } } public static void SendModCheck(int playerIndex) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) if (ShouldManageState()) { P2P.SendToPlayerNr(playerIndex, new Message((Msg)5040, ((Peer)P2P.localPeer).playerNr, -1, (object)null, -1)); Plugin.Logger.LogInfo((object)$"sent mod check to player {playerIndex}"); } } public static void ReceiveModCheck(Message message) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001b: 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_0047: Unknown result type (might be due to invalid IL or missing references) if (SyncFixConfig.Instance.Enabled) { if (message.playerNr == 0) { HostHasSyncFix = true; } P2P.SendToPlayerNr(message.playerNr, new Message((Msg)5041, ((Peer)P2P.localPeer).playerNr, -1, (object)null, -1)); Plugin.Logger.LogInfo((object)$"received mod check from player {message.playerNr}, sent reply"); } } public static void ReceiveModReply(Message message) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (ShouldManageState()) { SetPeerModStatus(message.playerNr, LobbyPeerModStatus.CONFIRMED); Plugin.Logger.LogInfo((object)$"received mod reply from player {message.playerNr}, set CONFIRMED"); } } public static bool AllPeersConfirmed() { bool flag = !peerModStatus.Where((LobbyPeerModStatus status, int index) => Player.GetPlayer(index).IsInMatch && status == LobbyPeerModStatus.UNKNOWN).Any(); Plugin.Logger.LogInfo((object)$"all peers confirmed: {flag}"); return flag; } public static void SendAllGroupMessage() { if (ShouldManageState()) { ForAllRemotePeersInMatch(delegate(int i) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) P2P.SendToPlayerNr(i, new Message((Msg)5042, ((Peer)P2P.localPeer).playerNr, -1, (object)null, -1)); }); CurrentMode = SyncFixMode.GROUP; Plugin.Logger.LogInfo((object)"sent all peers group message"); } } public static void ReceiveGroupMessage(Message message) { if (SyncFixConfig.Instance.Enabled) { CurrentMode = SyncFixMode.GROUP; Plugin.Logger.LogInfo((object)"received group message"); } } public static bool IsUsingGroup() { return CurrentMode == SyncFixMode.GROUP; } private static void SetPeerModStatus(int playerIndex, LobbyPeerModStatus status) { if (ShouldManageState()) { peerModStatus[playerIndex] = status; } } public static void ResetState() { ResetPeerModStatus(); ResetMode(); } public static void ResetPeerModStatus() { if (SyncFixConfig.Instance.Enabled && P2P.isConnected) { for (int i = 0; i < peerModStatus.Length; i++) { SetPeerModStatus(i, (i == ((Peer)P2P.localPeer).playerNr) ? LobbyPeerModStatus.CONFIRMED : LobbyPeerModStatus.UNKNOWN); } if (P2P.isHost) { HostHasSyncFix = true; } else { HostHasSyncFix = false; } Plugin.Logger.LogInfo((object)("reset status: " + string.Join(", ", peerModStatus.Select((LobbyPeerModStatus status) => status.ToString()).ToArray()))); } } public static void ResetMode() { CurrentMode = SyncFixMode.SOLO; } private static bool IsLocalPeer(int playerIndex) { return playerIndex == ((Peer)P2P.localPeer).playerNr; } private static bool ShouldManageState() { if (P2P.isHost) { return SyncFixConfig.Instance.Enabled; } return false; } private static void ForAllRemotePeersInMatch(Action action) { Player.ForAllInMatch((Action)delegate(Player player) { if (!IsLocalPeer(player.nr)) { action(player.nr); } }); } } public class SyncFixConfig { private static SyncFixConfig instance; private readonly ConfigEntry enabled; private readonly ConfigEntry showDebugInfo; private readonly ConfigEntry debugInfoKey; private readonly ConfigEntry recordDebugInfo; public static SyncFixConfig Instance { get { return instance; } private set { instance = value; } } public bool Enabled { get { return enabled.Value; } set { enabled.Value = value; } } public bool ShowDebugInfo { get { return showDebugInfo.Value; } set { showDebugInfo.Value = value; } } public KeyCode DebugInfoKey { get { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return debugInfoKey.Value; } set { //IL_0006: Unknown result type (might be due to invalid IL or missing references) debugInfoKey.Value = value; } } public bool RecordDebugInfo { get { return recordDebugInfo.Value; } set { recordDebugInfo.Value = value; } } private SyncFixConfig(ConfigFile configFile) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown enabled = configFile.Bind(new ConfigDefinition("Sync Fix", "Enable host advantage fix"), true, (ConfigDescription)null); showDebugInfo = configFile.Bind(new ConfigDefinition("Sync Fix", "Show debug info ingame"), false, (ConfigDescription)null); debugInfoKey = configFile.Bind("Sync Fix", "Toggle debug info key", (KeyCode)0, (ConfigDescription)null); recordDebugInfo = configFile.Bind(new ConfigDefinition("Sync Fix", "Save debug info to disk at match end"), false, (ConfigDescription)null); } internal static void LoadConfig(ConfigFile configFile) { if (Instance != null) { throw new InvalidOperationException("config already loaded"); } configFile.SaveOnConfigSet = true; Instance = new SyncFixConfig(configFile); } } public class SyncFixManager { [CompilerGenerated] private sealed class d__32 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public float initialDelay; public float time; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__32(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_007e: Unknown result type (might be due to invalid IL or missing references) switch (<>1__state) { default: return false; case 0: <>1__state = -1; Plugin.Logger.LogInfo((object)$"delaying self-timesync by {initialDelay}"); <>2__current = (object)new WaitForSeconds(initialDelay); <>1__state = 1; return true; case 1: <>1__state = -1; P2P.SendToPlayerNr(((Peer)P2P.localPeer).playerNr, new Message((Msg)189, Sync.matchNr, Mathf.RoundToInt(time * 1000f), (object)null, -1)); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static readonly SyncFixManager instance; public static readonly int GROUP_SLEEP_CHECK_INTERVAL; public static readonly int GROUP_ADVANTAGE_UPDATE_INTERVAL; public TimeSync[] timeSync = new TimeSync[4] { new TimeSync(0), new TimeSync(1), new TimeSync(2), new TimeSync(3) }; private int nextAdvantageUpdate = int.MaxValue; private int nextRecommendedSleep = int.MaxValue; private int lastSleep = -1; public static SyncFixManager Instance => instance; public int NextAdvantageUpdate => nextAdvantageUpdate; public int NextRecommendedSleep => nextRecommendedSleep; public int LastSleep => lastSleep; static SyncFixManager() { instance = new SyncFixManager(); GROUP_SLEEP_CHECK_INTERVAL = 120; GROUP_ADVANTAGE_UPDATE_INTERVAL = 60; } public static void RegisterGameMessages() { MessageApi.RegisterCustomMessage(((BaseUnityPlugin)Plugin.Instance).Info, (ushort)5043, SyncFixMessages.GAME_LOCAL_ADVANTAGE.ToString(), (Action)Instance.ReceiveRemoteAdvantage); } public void Reset() { if (SyncFixConfig.Instance.Enabled) { for (int i = 0; i < Sync.nPlayers; i++) { timeSync[i].Reset(); } nextAdvantageUpdate = int.MaxValue; nextRecommendedSleep = int.MaxValue; lastSleep = -1; } } public void Start() { if (SyncFixConfig.Instance.Enabled) { UpdateNextRecommendedSleep(); UpdateNextAdvantageTime(); } } public void MidMatchReset() { if (SyncFixConfig.Instance.Enabled) { for (int i = 0; i < Sync.nPlayers; i++) { timeSync[i].ResetActiveComponent(); } UpdateNextAdvantageTime(); UpdateNextRecommendedSleep(); lastSleep = -1; } } public void UpdateNextRecommendedSleep() { nextRecommendedSleep = Sync.curFrame + GROUP_SLEEP_CHECK_INTERVAL; } public void UpdateLastSleep() { lastSleep = Sync.curFrame; } public void OnSleep(float sleepDuration) { UpdateNextRecommendedSleep(); UpdateLastSleep(); if (StateManager.IsUsingGroup()) { ForAllValidOthers(delegate(int i) { float frames = sleepDuration * (float)World.FPS; timeSync[i].OnSleep(frames); SendLocalAdvantageToPlayer(i, notifySleep: true); }); } UpdateNextAdvantageTime(); } public void UpdateNextAdvantageTime() { nextAdvantageUpdate = Sync.curFrame + GROUP_ADVANTAGE_UPDATE_INTERVAL; } public float GetRecommendedSleepInterval(int playerIndex) { if (!SyncFixConfig.Instance.Enabled) { throw new InvalidOperationException("asked for sleep interval when sync fix disabled?"); } return timeSync[playerIndex].GetSleepInterval(); } public float GetCurrentLocalAdvantage(int playerIndex) { if (!SyncFixConfig.Instance.Enabled) { throw new InvalidOperationException("asked for local advantage when sync fix disabled?"); } return timeSync[playerIndex].GetCurrentLocalAdvantage(); } public void SendLocalAdvantageToPlayer(int i, bool notifySleep = false) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (SyncFixConfig.Instance.Enabled) { byte[] bytes = BitConverter.GetBytes(GetCurrentLocalAdvantage(i)); Message val = default(Message); ((Message)(ref val))..ctor((Msg)5043, ((Peer)P2P.localPeer).playerNr, notifySleep ? 1 : 0, (object)bytes, bytes.Length); P2P.SendToPlayerNr(i, val); } } public void ReceiveRemoteAdvantage(Message message) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (SyncFixConfig.Instance.Enabled) { float remoteAdvantage = BitConverter.ToSingle((byte[])message.ob, 0); UpdateRemoteAdvantage(message.playerNr, remoteAdvantage); } } public void UpdateRemoteAdvantage(int playerIndex, float remoteAdvantage) { timeSync[playerIndex].UpdateRemoteFrameAdvantage(remoteAdvantage); } public void GroupAlignTimes() { if (Sync.isAwaiting) { return; } ForAllValidOthers(delegate(int i) { timeSync[i].FrameUpdate(); }); bool flag = Sync.curFrame > NextRecommendedSleep; for (int j = 0; j < Sync.nPlayers; j++) { if (Sync.IsValidOther(j)) { flag = flag || timeSync[j].ShouldEmergencySleep(); if (flag) { break; } } } if (!flag || Sync.isAwaiting) { return; } float num = 0f; for (int k = 0; k < Sync.nPlayers; k++) { if (Sync.othersInfo[k] != null) { num = Math.Max(num, GetRecommendedSleepInterval(k)); } } if (num > 0f) { Plugin.Logger.LogInfo((object)$"waiting for {num}s"); P2P.Wait(num); } } public void SoloHostAlignTimes() { //IL_00de: Unknown result type (might be due to invalid IL or missing references) if (Sync.isAwaiting) { return; } float num = float.MaxValue; for (int i = 0; i < Sync.nPlayers; i++) { timeSync[i].FrameUpdate(); if (timeSync[i].GetCurrentFrameEstimate() < num) { num = timeSync[i].GetCurrentFrameEstimate(); } } for (int j = 0; j < Sync.nPlayers; j++) { timeSync[j].UpdateRunAheadEstimate(num); if (!timeSync[j].CanSleep()) { continue; } float sleepInterval = timeSync[j].GetSleepInterval(); if (sleepInterval > 0f) { Plugin.Logger.LogInfo((object)$"sleeping p{j + 1} for {sleepInterval}"); timeSync[j].OnSleep(sleepInterval * (float)World.FPS); if (j == 0) { SendSelfTimeAlignAfterDelay(sleepInterval); } else { P2P.SendToPlayerNr(j, new Message((Msg)189, Sync.matchNr, Mathf.RoundToInt(sleepInterval * 1000f), (object)null, -1)); } } } } private static void SendSelfTimeAlignAfterDelay(float time) { float num = (from player in Player.EPlayers() where player.LAADACKBGLL() && Sync.IsValidOther(player.CJFLMDNNMIE) select player).Average((ALDOKEMAOMB player) => player.KLEEADMGHNE.ping); num *= 0.49f; ((MonoBehaviour)P2P.instance).StartCoroutine(CSendSelfTimeAlignAfterDelay(num, time)); } private static IEnumerator CSendSelfTimeAlignAfterDelay(float initialDelay, float time) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__32(0) { initialDelay = initialDelay, time = time }; } public static void ForAllValidOthers(Action action) { for (int i = 0; i < Sync.nPlayers; i++) { if (Sync.IsValidOther(i)) { action(i); } } } } public enum SyncFixMessages { LOBBY_MOD_CHECK = 5040, LOBBY_MOD_REPLY, GAME_USE_GROUP, GAME_LOCAL_ADVANTAGE } public class TimeSync { protected readonly int playerIndex; private readonly TimeSyncGroupComponent groupComponent; private readonly TimeSyncSoloHostComponent soloComponent; private ITimeSyncComponent activeComponent; public TimeSync(int playerIndex) { this.playerIndex = playerIndex; groupComponent = new TimeSyncGroupComponent(playerIndex); soloComponent = new TimeSyncSoloHostComponent(playerIndex); SetActiveComponent(); } public void Reset() { SetActiveComponent(); activeComponent.Reset(); } public void ResetActiveComponent() { activeComponent.Reset(); } public void FrameUpdate() { activeComponent.FrameUpdate(); } public float GetSleepInterval() { return activeComponent.GetSleepInterval(); } public void OnSleep(float frames) { activeComponent.OnSleep(frames); } public void UpdateRemoteFrameAdvantage(float remoteAdvantage) { groupComponent.UpdateRemoteFrameAdvantage(remoteAdvantage); } public float GetCurrentLocalAdvantage() { return groupComponent.CurrentLocalAdvantage; } public void UpdateRunAheadEstimate(float minimumFrame) { soloComponent.UpdateRunAheadEstimate(minimumFrame); } public float GetCurrentFrameEstimate() { return soloComponent.CurrentFrameEstimate; } public bool CanSleep() { if (Sync.curFrame <= soloComponent.NextRecommendedSleep) { return soloComponent.ShouldEmergencySleep(); } return true; } public bool ShouldEmergencySleep() { return activeComponent.ShouldEmergencySleep(); } public void SetActiveComponent() { if (StateManager.IsUsingGroup()) { activeComponent = groupComponent; } else { activeComponent = soloComponent; } } } public abstract class TimeSyncComponentBase : ITimeSyncComponent { protected static readonly float RECENT_SLEEP_WINDOW_HOST = 1800f; protected static readonly float RECENT_SLEEP_WINDOW_CLIENT = 1800f; protected static readonly float ALIGN_TIMES_FACTOR = 0.8f; protected readonly int playerIndex; protected readonly Queue recentSleeps = new Queue(); public TimeSyncComponentBase(int playerIndex) { this.playerIndex = playerIndex; } public abstract float GetSleepInterval(); public abstract bool ShouldEmergencySleep(); protected abstract float GetRecentSleepBaseFactor(); protected abstract float GetRecentSleepWindow(); public virtual void Reset() { recentSleeps.Clear(); } public virtual void FrameUpdate() { while (recentSleeps.Count > 0 && (float)Sync.curFrame > (float)recentSleeps.Peek() + GetRecentSleepWindow()) { recentSleeps.Dequeue(); } } public virtual void OnSleep(float frames) { recentSleeps.Enqueue(Sync.curFrame); } protected float GetRecentSleepFactor() { float num = 0f; foreach (int recentSleep in recentSleeps) { num += (GetRecentSleepWindow() - (float)(Sync.curFrame - recentSleep)) / GetRecentSleepWindow() * GetRecentSleepBaseFactor(); } return num; } protected static float LinearClamped(float x, float xStart, float xEnd, float yMin, float yMax) { return Mathf.Clamp((yMax - yMin) / (xEnd - xStart) * (x - xStart) + yMin, yMin, yMax); } } public class TimeSyncGroupComponent : TimeSyncComponentBase { private static readonly float MAX_SLEEP_DURATION = 0.5f; private static readonly float MIN_SLEEP_DURATION = World.DELTA_TIME; private static readonly float LOCAL_ADVANTAGE_UPDATE_RATE = 0.1f; private static readonly float RECENT_SLEEP_BASE_FACTOR = 0.25f; private float currentLocalAdvantage; private float currentRemoteAdvantage; private float lastRemoteAdvantage; private readonly FrameAccumulator accumulator; public float CurrentLocalAdvantage => currentLocalAdvantage; public float CurrentRemoteAdvantage => currentRemoteAdvantage; public TimeSyncGroupComponent(int playerIndex) : base(playerIndex) { accumulator = new FrameAccumulator(base.playerIndex, 0.25f, 1.5f); accumulator.upperBoundFunc = (int frame, int i) => TimeSyncComponentBase.LinearClamped(NetUtils.MaxPing, 0.03f, 0.3f, 0.5f, 0.7f) * (1f + GetRecentSleepFactor()); accumulator.lowerBoundFunc = (int frame, int i) => TimeSyncComponentBase.LinearClamped(NetUtils.MaxPing, 0.03f, 0.3f, 0.1f, 0.4f); } public override void Reset() { currentLocalAdvantage = 0f; currentRemoteAdvantage = 0f; lastRemoteAdvantage = 0f; accumulator.Reset(); base.Reset(); } public override void FrameUpdate() { int curFrame = Sync.curFrame; float num = Sync.statusInput.otherReceived[playerIndex]; if (!(num < 0f)) { float travelTimeEstimate = NetUtils.GetTravelTimeEstimate(Sync.othersInfo[playerIndex].peer.ping); float num2 = num + travelTimeEstimate - (float)curFrame; currentLocalAdvantage = Mathf.Lerp(CurrentLocalAdvantage, num2, LOCAL_ADVANTAGE_UPDATE_RATE); currentRemoteAdvantage = Mathf.Lerp(CurrentRemoteAdvantage, lastRemoteAdvantage, 0.2f); accumulator.FrameUpdate(Sync.curFrame, (CurrentRemoteAdvantage - CurrentLocalAdvantage) / 2f); base.FrameUpdate(); } } public override float GetSleepInterval() { if (!accumulator.ThresholdReached()) { return 0f; } return Mathf.Clamp(accumulator.currentValue * World.DELTA_TIME * TimeSyncComponentBase.ALIGN_TIMES_FACTOR, MIN_SLEEP_DURATION, MAX_SLEEP_DURATION); } public override bool ShouldEmergencySleep() { return accumulator.ThresholdVeryReached(); } public override void OnSleep(float frames) { currentLocalAdvantage += frames; currentRemoteAdvantage -= frames; lastRemoteAdvantage -= frames; accumulator.Reset(); base.OnSleep(frames); } public void UpdateRemoteFrameAdvantage(float remoteAdvantage) { lastRemoteAdvantage = remoteAdvantage; } protected override float GetRecentSleepBaseFactor() { return RECENT_SLEEP_BASE_FACTOR; } protected override float GetRecentSleepWindow() { return TimeSyncComponentBase.RECENT_SLEEP_WINDOW_CLIENT; } } public class TimeSyncSoloHostComponent : TimeSyncComponentBase { private static readonly float MAX_SLEEP_DURATION = 0.5f; private static readonly float MIN_SLEEP_DURATION = World.DELTA_TIME; private static readonly int ESTIMATE_SLEEP_CHECK_INTERVAL = 120; private static readonly float RUN_AHEAD_UPDATE_RATE = 0.1f; private static readonly float RUN_AHEAD_ACCUMULATOR_THRESHOLD = 1.5f; private static readonly float RECENT_SLEEP_BASE_FACTOR = 0.3f; private int nextRecommendedSleep = int.MaxValue; private float currentFrameEstimate = -1f; private int noRunAheadUpdatesUntil = -1; private readonly FrameAccumulator accumulator; public int NextRecommendedSleep => nextRecommendedSleep; public float CurrentFrameEstimate => currentFrameEstimate; public float RunAheadEstimate => accumulator.currentValue; public TimeSyncSoloHostComponent(int playerIndex) : base(playerIndex) { accumulator = new FrameAccumulator(base.playerIndex, RUN_AHEAD_UPDATE_RATE, RUN_AHEAD_ACCUMULATOR_THRESHOLD); accumulator.upperBoundFunc = (int frame, int i) => TimeSyncComponentBase.LinearClamped(NetUtils.MaxPing, 0.03f, 0.3f, 0.55f, 1.08f) * (1f + GetRecentSleepFactor()); accumulator.lowerBoundFunc = (int frame, int i) => TimeSyncComponentBase.LinearClamped(NetUtils.MaxPing, 0.03f, 0.3f, 0.25f, 0.5f); } public override void Reset() { nextRecommendedSleep = 60; currentFrameEstimate = -1f; noRunAheadUpdatesUntil = -1; accumulator.Reset(); base.Reset(); } public override void FrameUpdate() { currentFrameEstimate = EstimateCurrentFrame(); base.FrameUpdate(); } public float EstimateCurrentFrame() { if (playerIndex == 0) { return Sync.curFrame; } float travelTimeEstimate = NetUtils.GetTravelTimeEstimate(Player.GetPlayer(playerIndex).peer.ping); return (float)Sync.statusInput.otherReceived[playerIndex] + travelTimeEstimate; } public void UpdateRunAheadEstimate(float minimumFrame) { if (Sync.curFrame >= noRunAheadUpdatesUntil) { if (Sync.curFrame == noRunAheadUpdatesUntil) { noRunAheadUpdatesUntil = -1; } float frameValue = CurrentFrameEstimate - minimumFrame; accumulator.FrameUpdate(Sync.curFrame, frameValue); } } public override bool ShouldEmergencySleep() { return accumulator.ThresholdVeryReached(); } public override float GetSleepInterval() { if (!accumulator.ThresholdReached()) { return 0f; } return Mathf.Clamp(RunAheadEstimate * World.DELTA_TIME * TimeSyncComponentBase.ALIGN_TIMES_FACTOR, MIN_SLEEP_DURATION, MAX_SLEEP_DURATION); } public override void OnSleep(float frames) { noRunAheadUpdatesUntil = (int)((double)Sync.curFrame + Math.Ceiling(frames + NetUtils.MaxPing * (float)World.FPS) + 2.0); nextRecommendedSleep = Sync.curFrame + ESTIMATE_SLEEP_CHECK_INTERVAL; accumulator.Reset(); base.OnSleep(frames); } protected override float GetRecentSleepBaseFactor() { return RECENT_SLEEP_BASE_FACTOR; } protected override float GetRecentSleepWindow() { if (playerIndex != 0) { return TimeSyncComponentBase.RECENT_SLEEP_WINDOW_CLIENT; } return TimeSyncComponentBase.RECENT_SLEEP_WINDOW_HOST; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "ca.gov.mechasoulindustries.llb.my.cute.syncfix"; public const string PLUGIN_NAME = "Sync Fix"; public const string PLUGIN_VERSION = "1.0.1"; } } namespace SyncFix.Utils { public class InstructionBuilder { private List instructions = new List(); private Queue previousOpCodes = new Queue(); private bool expectingOpCode = true; public InstructionBuilder OpCode(OpCode opcode) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown if (!expectingOpCode) { throw new InvalidOperationException($"tried to add opcode {opcode.Name} when expecting operand (previous instruction: {instructions.LastOrDefault()})"); } previousOpCodes.Enqueue(opcode); expectingOpCode = false; if (opcode.OperandType == OperandType.InlineNone) { instructions.Add(new CodeInstruction(previousOpCodes.Dequeue(), (object)null)); expectingOpCode = true; } return this; } public InstructionBuilder Operand(object operand) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Expected O, but got Unknown if (expectingOpCode) { if (operand == null) { CodeInstruction? obj = instructions.LastOrDefault(); if (obj != null && obj.opcode.OperandType == OperandType.InlineNone) { return this; } } throw new InvalidOperationException($"tried to add operand {operand} when expecting opcode (previous instruction: {instructions.LastOrDefault()}, opcode: {previousOpCodes.LastOrDefault()}"); } if (previousOpCodes.Count == 0) { throw new InvalidOperationException($"tried to add operand {operand} without previously adding an opcode (also this shouldnt happen?)"); } instructions.Add(new CodeInstruction(previousOpCodes.Dequeue(), operand)); expectingOpCode = true; return this; } public CodeInstruction[] Build() { return instructions.ToArray(); } public CodeMatch[] BuildAsMatch() { return ((IEnumerable)instructions).Select((Func)((CodeInstruction instruction) => new CodeMatch(instruction, (string)null))).ToArray(); } } public class NetUtils { private static float maxPing = 0f; private static int maxPingPlayer = -1; public static float MaxPing => maxPing; public static float GetTravelTimeEstimate(float ping) { return Mathf.Pow(ping, 2f) * -17.3f + ping * 36.2f + 0.23f; } public static void UpdateMaxPing(Peer peer) { if (peer.playerNr == maxPingPlayer) { if (peer.ping > maxPing) { maxPing = peer.ping; } else { maxPing = GetMaxPing(out maxPingPlayer); } } else if (peer.ping > maxPing) { maxPing = peer.ping; maxPingPlayer = peer.playerNr; } } public static float GetMaxPing(out int maxPlayerIndex) { float num = -1f; int num2 = -1; foreach (ALDOKEMAOMB item in Player.EPlayers()) { if (item.LAADACKBGLL() && (!Sync.isActive || Sync.IsValidOther(item.CJFLMDNNMIE)) && item.KLEEADMGHNE.ping > num) { num = item.KLEEADMGHNE.ping; num2 = item.CJFLMDNNMIE; } } maxPlayerIndex = num2; return num; } public static void ResetMaxPing() { maxPing = 0f; maxPingPlayer = -1; } } internal class PathUtils { public static DirectoryInfo ModdingFolder { get; private set; } public static string ModdingFolderName { get; private set; } public static void Init(PluginInfo info) { ModdingFolder = ModdingFolder.GetModSubFolder(info); ModdingFolderName = ModdingFolder.FullName; } public static string GetFilepath(string resourceName) { return Utility.CombinePaths(new string[2] { ModdingFolderName, resourceName }); } public static string GetCurrentGameDebugPath() { return Utility.CombinePaths(new string[3] { ModdingFolderName, GetCurrentUserId(), GetCurrentGameString() }); } private static string GetCurrentUserId() { KIIIINKJKNI gIGAKBJGFDI = CGLLJHHAJAK.GIGAKBJGFDI; return ((gIGAKBJGFDI != null) ? gIGAKBJGFDI.ECEAOMHNGOL() : null) ?? "no_id"; } private static string GetCurrentGameString() { if (!Sync.isActive) { return ""; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(Sync.matchNr); stringBuilder.Append("_"); for (int i = 0; i < Sync.nPlayers; i++) { if (Sync.IsValidOther(i)) { stringBuilder.Append(Player.GetPlayer(i).peer.peerId); stringBuilder.Append("_"); } } stringBuilder.Append($"P{((Peer)P2P.localPeer).playerNr}"); stringBuilder.Append("_"); stringBuilder.Append(StateManager.IsUsingGroup() ? "GROUP" : "SOLO"); return stringBuilder.ToString(); } } } namespace SyncFix.Patches { [HarmonyPatch] public class Debug_Patches { [HarmonyPatch(typeof(Sync), "StopNow")] [HarmonyPrefix] public static bool RedirectStopNow() { if (SyncFixConfig.Instance.RecordDebugInfo) { FrameRecorders.SaveAll(); } return true; } [HarmonyPatch(typeof(World), "Init1")] [HarmonyPostfix] public static void Init1Postfix(World __instance) { if (GameSettings.IsOnline) { RollbackStats.Reset(); ((Component)__instance).gameObject.AddComponent().parent = __instance; } } [HarmonyPatch(typeof(World), "Update")] [HarmonyPrefix] public static bool RedirectUpdate(World __instance) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) if (Input.GetKeyDown(SyncFixConfig.Instance.DebugInfoKey)) { SyncFixConfig.Instance.ShowDebugInfo = !SyncFixConfig.Instance.ShowDebugInfo; } return true; } [HarmonyPatch(typeof(Sync), "Rollback")] [HarmonyPrefix] public static bool RedirectRollback(int frame) { int num = Sync.curFrame - frame; if (SyncFixConfig.Instance.RecordDebugInfo) { FrameRecorders.Record("rollbacks", Sync.curFrame, num); } RollbackStats.AddRollback(num); return true; } [HarmonyPatch(typeof(P2P), "Wait")] [HarmonyPrefix] public static bool RedirectWait(float wait) { if (SyncFixConfig.Instance.RecordDebugInfo) { FrameRecorders.Record("wait", Sync.curFrame, wait); } RollbackStats.AddSleep(wait); return true; } } public class DebugInfo : MonoBehaviour { public World parent; public GUIStyle guiStyle; private void Awake() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //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) guiStyle = new GUIStyle(); guiStyle.fontSize = 12; guiStyle.normal.textColor = Color32.op_Implicit(new Color32((byte)100, (byte)100, (byte)100, byte.MaxValue)); } private void OnGUI() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (SyncFixConfig.Instance.ShowDebugInfo) { GUI.Label(new Rect(20f, 20f, 100f, 100f), RollbackStats.GetStats(), guiStyle); } } } [HarmonyPatch] public class LobbyState_Patches { [HarmonyPatch(typeof(LocalHost), "OnOtherLeft")] [HarmonyPostfix] public static void OnOtherLeftPostfix(LocalHost __instance, Peer otherPeer) { StateManager.PeerLeft(otherPeer.playerNr); } [HarmonyPatch(typeof(LocalHost), "OnOtherJoined")] [HarmonyPostfix] public static void OnOtherJoinedPostfix(LocalHost __instance, string otherPeerId, string otherPeerName, int otherPlayerNr) { StateManager.PeerJoined(otherPlayerNr); } [HarmonyPatch(typeof(HDLIJDBFGKN), "OAACLLGMFLH", new Type[] { })] [HarmonyPostfix] public static void OAACLLGMFLHPostfix(HDLIJDBFGKN __instance) { if (StateManager.AllPeersConfirmed()) { StateManager.SendAllGroupMessage(); } } [HarmonyPatch(typeof(HDLIJDBFGKN), "DJLJONJDDDO")] [HarmonyPostfix] public static void DJLJONJDDDOPostfix(HDLIJDBFGKN __instance, ref Action BDNDCAJCNCC) { Action oldCallback = BDNDCAJCNCC; BDNDCAJCNCC = delegate(bool result) { oldCallback(result); KAfterOpenPostfix(__instance, result); }; } private static void KAfterOpenPostfix(HDLIJDBFGKN gameStatesLobbyOnline, bool success) { if (success) { if (gameStatesLobbyOnline.FBJIDODJNFN) { StateManager.ResetState(); } else { StateManager.ResetMode(); } NetUtils.ResetMaxPing(); } } [HarmonyPatch(typeof(Peer), "ResetPing")] [HarmonyPostfix] public static void ResetPingPostfix(Peer __instance) { __instance.pingsPrev[0] = -1f; __instance.ping = 0f; } } [HarmonyPatch] public class TimeSyncGroup_Patches { [HarmonyPatch(typeof(Sync), "AlignTimes")] [HarmonyPrefix] public static bool RedirectAlignTimes() { if (!SyncFixConfig.Instance.Enabled) { return true; } if (Sync.doAwait) { SyncFixManager.Instance.MidMatchReset(); } if (Sync.isAwaiting) { return false; } if (StateManager.IsUsingGroup()) { SyncFixManager.Instance.GroupAlignTimes(); return false; } if (P2P.isHost) { if (Sync.curFrame < 60) { return false; } SyncFixManager.Instance.SoloHostAlignTimes(); return false; } return true; } [HarmonyPatch(typeof(Sync), "Init")] [HarmonyPostfix] public static void InitPostfix() { SyncFixManager.Instance.Reset(); } [HarmonyPatch(typeof(LocalPeer), "SendToPlayerNr")] [HarmonyPrefix] public static bool RedirectSendToPlayerNr(LocalPeer __instance, int receiverPlayerNr, ref Message message) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!SyncFixConfig.Instance.Enabled) { return true; } if ((int)message.msg == 190) { JKMAAHELEMF val = (JKMAAHELEMF)message.ob; float num = (float)val.CGJJEHPPOAN * 0.5f; if (val.GCPKPHMKLBN == 126) { num += 30000f * World.DELTA_TIME; } val.CGJJEHPPOAN = (int)num; message = new Message(message.msg, message.playerNr, message.index, (object)val, message.obSize); } return true; } [HarmonyPatch(typeof(OGONAGCFDPK), "IMEGGOOAADG")] [HarmonyTranspiler] public static IEnumerable IMEGGOOAADGTranspiler(IEnumerable instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown CodeMatcher val = new CodeMatcher(instructions, (ILGenerator)null); val.MatchStartForward((CodeMatch[])(object)new CodeMatch[1] { new CodeMatch((OpCode?)OpCodes.Call, (object)AccessTools.Method(typeof(Sync), "Start", (Type[])null, (Type[])null), (string)null) }); val.Advance(1).Insert((CodeInstruction[])(object)new CodeInstruction[1] { Transpilers.EmitDelegate((Action)delegate { if (SyncFixConfig.Instance.Enabled) { SyncFixManager.Instance.Start(); } }) }); return val.InstructionEnumeration(); } [HarmonyPatch(typeof(P2P), "Update")] [HarmonyPostfix] public static void UpdatePostfix(P2P __instance) { if (SyncFixConfig.Instance.Enabled && P2P.isPinging && Sync.isActive && !Sync.doAwait && !Sync.isAwaiting && Sync.curFrame > SyncFixManager.Instance.NextAdvantageUpdate && Sync.curFrame > SyncFixManager.Instance.LastSleep + 1 && StateManager.IsUsingGroup()) { SyncFixManager.ForAllValidOthers(delegate(int i) { SyncFixManager.Instance.SendLocalAdvantageToPlayer(i); }); SyncFixManager.Instance.UpdateNextAdvantageTime(); } } [HarmonyPatch(typeof(P2P), "Wait")] [HarmonyTranspiler] public static IEnumerable WaitTranspiler(IEnumerable instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown CodeMatcher val = new CodeMatcher(instructions, (ILGenerator)null); val.End(); val.Insert((CodeInstruction[])(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Ldarg_0, (object)null), Transpilers.EmitDelegate>((Action)delegate(float f) { if (SyncFixConfig.Instance.Enabled) { SyncFixManager.Instance.OnSleep(f); } }) }); return val.InstructionEnumeration(); } [HarmonyPatch(typeof(Sync), "UpdateAwait")] [HarmonyTranspiler] public static IEnumerable UpdateAwaitTranspiler(IEnumerable instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) CodeMatcher val = new CodeMatcher(instructions, (ILGenerator)null); val.MatchStartForward(new InstructionBuilder().OpCode(OpCodes.Ldsfld).Operand(AccessTools.Field(typeof(Sync), "statusInput")).OpCode(OpCodes.Ldfld) .Operand(AccessTools.Field(typeof(FrameStatus), "handledByAll")) .OpCode(OpCodes.Call) .Operand(AccessTools.PropertyGetter(typeof(Sync), "curFrame")) .OpCode(OpCodes.Ldc_I4_S) .Operand((sbyte)30) .OpCode(OpCodes.Sub) .BuildAsMatch()); val.Advance(2); val.RemoveInstructions(3); val.Insert(new InstructionBuilder().OpCode(OpCodes.Ldsfld).Operand(AccessTools.Field(typeof(Sync), "awaitFrame")).OpCode(OpCodes.Ldc_I4_S) .Operand((sbyte)120) .OpCode(OpCodes.Add) .Build()); return val.InstructionEnumeration(); } } [HarmonyPatch] public class TimeSyncSolo_Patches { [HarmonyPatch(typeof(Sync), "AlignTimes")] [HarmonyPostfix] public static void AlignTimesPostfix(Sync __instance) { if (SyncFixConfig.Instance.Enabled && !P2P.isHost && !StateManager.HostHasSyncFix && Sync.curFrame % 60 == 0) { SelfVanillaAlignTimes(); } } private static void SelfVanillaAlignTimes() { //IL_00a6: Unknown result type (might be due to invalid IL or missing references) float fixedDeltaTime = Time.fixedDeltaTime; float num = (float)Sync.curFrame * fixedDeltaTime; float num2 = num; for (int i = 0; i < Sync.nPlayers; i++) { OtherInfo val = Sync.othersInfo[i]; if (val != null) { float num3 = (float)Sync.statusInput.otherReceived[i] * fixedDeltaTime; num3 += val.peer.ping * 0.5f; if (num3 < num2) { num2 = num3; } } } float num4 = num - num2; num4 *= 0.75f; if (num4 >= 0.02f) { num4 = Mathf.Min(num4, 0.5f); P2P.SendToPlayerNr(((Peer)P2P.localPeer).playerNr, new Message((Msg)189, Sync.matchNr, Mathf.RoundToInt(num4 * 1000f), (object)null, -1)); } } [HarmonyPatch(typeof(LocalPeer), "OnReceiveMessage")] [HarmonyPrefix] public static bool RedirectOnReceiveMessage(LocalPeer __instance, Envelope envelope) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (!SyncFixConfig.Instance.Enabled) { return true; } if ((int)envelope.message.msg == 189 && envelope.sender != ((Peer)P2P.localPeer).peerId && !StateManager.HostHasSyncFix) { return false; } return true; } [HarmonyPatch(typeof(Peer), "ResolvePing", new Type[] { typeof(int) })] [HarmonyPostfix] public static void ResolvePingPostfix(Peer __instance) { NetUtils.UpdateMaxPing(__instance); } } } namespace SyncFix.FrameRecorder { internal class FrameRecord : IComparable> { public int frame; public T value; public FrameRecord(int frame, T value) { this.frame = frame; this.value = value; } public int CompareTo(FrameRecord other) { return frame.CompareTo(other.frame); } public override string ToString() { return $"{frame},{value}"; } } internal class FrameRecorder : IFrameRecorder { public readonly string name; public List> records; private Func, string> toStringFunction = (FrameRecord record) => record.ToString(); public Func, string> ToStringFunc { get { return toStringFunction; } set { toStringFunction = value; } } public FrameRecorder(string name) { this.name = name; records = new List>(); } public void Record(int frame, U value) { if (!(value is T)) { throw new InvalidCastException($"tried to record a {typeof(U)} in a FrameRecorder<{typeof(T)}>"); } object obj = value; T value2 = (T)((obj is T) ? obj : null); records.Add(new FrameRecord(frame, value2)); } public void SaveToFile() { if (records.Count == 0) { return; } records.Sort(); StringBuilder stringBuilder = new StringBuilder(); foreach (FrameRecord record in records) { stringBuilder.Append(ToStringFunc(record)); stringBuilder.AppendLine(); } string path = Utility.CombinePaths(new string[2] { PathUtils.GetCurrentGameDebugPath(), name + ".csv" }); Directory.CreateDirectory(Directory.GetParent(path).FullName); File.AppendAllText(path, stringBuilder.ToString()); } public void Clear() { records.Clear(); } } internal class FrameRecorders { private static readonly Dictionary _frameRecorders; static FrameRecorders() { _frameRecorders = new Dictionary(); } public static IFrameRecorder GetFrameRecorder(string name) { if (_frameRecorders.TryGetValue(name, out var value)) { return value; } IFrameRecorder frameRecorder = new FrameRecorder(name); _frameRecorders.Add(name, frameRecorder); return frameRecorder; } public static void Record(string name, int frame, T value) { GetFrameRecorder(name).Record(frame, value); } public static void SaveAll() { foreach (IFrameRecorder value in _frameRecorders.Values) { value.SaveToFile(); value.Clear(); } } public static void ClearAll() { foreach (IFrameRecorder value in _frameRecorders.Values) { value.Clear(); } } } internal interface IFrameRecorder { void Record(int frame, T value); void SaveToFile(); void Clear(); } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }