using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 PerformanceGuard { [BepInPlugin("local.performanceguard", "PerformanceGuard", "1.2.0")] public class PerformanceGuardPlugin : BaseUnityPlugin { public const string PluginGuid = "local.performanceguard"; public const string PluginName = "PerformanceGuard"; public const string PluginVersion = "1.2.0"; internal static ManualLogSource Log; internal static ConfigEntry FixEmptyAINodeCrash; internal static ConfigEntry LogSuppressedCrashes; internal static int SuppressedCrashCount; internal static ConfigEntry DungeonFrameBudget; internal static ConfigEntry FasterDungeon; private ConfigEntry _raiseConnectionTimeout; private ConfigEntry _connectionTimeoutSeconds; private ConfigEntry _raiseSceneLoadTimeout; private ConfigEntry _sceneLoadTimeoutSeconds; private ConfigEntry _fasterDungeonGeneration; private ConfigEntry _dungeonFrameBudgetMs; private ConfigEntry _autoThrottleMaskedAI; private ConfigEntry _throttleBelowFps; private ConfigEntry _restoreAboveFps; private ConfigEntry _fpsCheckSeconds; private Harmony _harmony; private float _fpsAccumulator; private int _fpsFrames; private float _nextFpsCheck; private bool _maskedAiThrottled; private Type _maskedAiType; private bool _lookedForMaskedAiType; private bool _timeoutsApplied; private float _nextCrashReport; private int _lastReportedCount; private void Awake() { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Expected O, but got Unknown //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Expected O, but got Unknown //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Expected O, but got Unknown //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Expected O, but got Unknown //IL_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Expected O, but got Unknown //IL_0217: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Expected O, but got Unknown //IL_0242: Unknown result type (might be due to invalid IL or missing references) //IL_024c: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; FixEmptyAINodeCrash = ((BaseUnityPlugin)this).Config.Bind("1 - Crash fixes", "FixEmptyAINodeCrash", true, "Stops the crash that happens when an enemy asks for the farthest AI node but has no nodes at all. This is the single biggest fix here: with LethalIntelligence installed it fires 50 times a second per masked enemy and can cost most of your frame rate. Turn off only if you suspect it of causing odd enemy behaviour."); LogSuppressedCrashes = ((BaseUnityPlugin)this).Config.Bind("1 - Crash fixes", "LogSuppressedCrashes", true, "true = report how many crashes were prevented, once every 30 seconds. Useful to confirm the fix is doing something. It never logs per crash, which would recreate the problem it is fixing."); _raiseConnectionTimeout = ((BaseUnityPlugin)this).Config.Bind("2 - Disconnect fixes", "RaiseConnectionTimeout", true, "Gives a stuttering player longer before Steam decides they are gone. Steam drops a connection after about 10 seconds of silence, and a machine at 2 fps can be silent that long without having crashed."); _connectionTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("2 - Disconnect fixes", "ConnectionTimeoutSeconds", 60, new ConfigDescription("How many seconds of silence to allow before dropping somebody. 60 is forgiving without being silly. Set it back to 10 for the game's normal behaviour.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 600), Array.Empty())); _raiseSceneLoadTimeout = ((BaseUnityPlugin)this).Config.Bind("2 - Disconnect fixes", "RaiseSceneLoadTimeout", true, "Gives slow machines longer to finish generating the level before the netcode gives up on them. Helps when landing takes a long time because one player is still loading."); _sceneLoadTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("2 - Disconnect fixes", "SceneLoadTimeoutSeconds", 300, new ConfigDescription("Seconds a player may take to load a level. The game's own default is much shorter than a heavily modded pack needs on weak hardware.", (AcceptableValueBase)(object)new AcceptableValueRange(30, 1200), Array.Empty())); _fasterDungeonGeneration = ((BaseUnityPlugin)this).Config.Bind("5 - Loading", "FasterDungeonGeneration", true, "Speeds up building the interior when you land. The game tells the dungeon builder it may only work for 1 millisecond per frame, so the time it takes is roughly (number of rooms) x (your frame time) - which is why one slow machine makes EVERYONE wait, since nobody's doors open until the last player finishes. Raising that budget spends more of each frame on building. You are staring at a loading screen anyway, so there is nothing to lose."); _dungeonFrameBudgetMs = ((BaseUnityPlugin)this).Config.Bind("5 - Loading", "DungeonFrameBudgetMs", 8f, new ConfigDescription("Milliseconds per frame the interior builder may use. The game sets 1; the builder's own default is 10. 8 is a large speed-up while still letting the loading screen animate. This only affects the machine it is installed on, so the slow player needs it too.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 33f), Array.Empty())); _autoThrottleMaskedAI = ((BaseUnityPlugin)this).Config.Bind("3 - Auto throttle", "AutoThrottleMaskedAI", true, "If your frame rate falls through the floor, switch off LethalIntelligence's added masked AI on THIS machine until it recovers. Masked go back to their normal game behaviour for you; everyone else is unaffected, and you can still join because the mod is still installed."); _throttleBelowFps = ((BaseUnityPlugin)this).Config.Bind("3 - Auto throttle", "ThrottleBelowFps", 15, new ConfigDescription("Switch the added masked AI off when the frame rate drops below this.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 60), Array.Empty())); _restoreAboveFps = ((BaseUnityPlugin)this).Config.Bind("3 - Auto throttle", "RestoreAboveFps", 25, new ConfigDescription("Switch it back on once the frame rate climbs above this. Keep it comfortably higher than ThrottleBelowFps so it does not flicker on and off.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 120), Array.Empty())); _fpsCheckSeconds = ((BaseUnityPlugin)this).Config.Bind("3 - Auto throttle", "FpsCheckSeconds", 5f, new ConfigDescription("How often to measure the frame rate, in seconds. Longer means slower to react but steadier.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 60f), Array.Empty())); DungeonFrameBudget = _dungeonFrameBudgetMs; FasterDungeon = _fasterDungeonGeneration; _harmony = new Harmony("local.performanceguard"); if (_fasterDungeonGeneration.Value) { try { _harmony.PatchAll(typeof(DungeonSpeedPatch)); Log.LogInfo((object)string.Format("[{0}] Interior building may use {1:F0}ms per frame (the game asks for 1).", "PerformanceGuard", _dungeonFrameBudgetMs.Value)); } catch (Exception ex) { Log.LogWarning((object)("[PerformanceGuard] Could not speed up interior building: " + ex.Message)); } } if (FixEmptyAINodeCrash.Value) { _harmony.PatchAll(typeof(EmptyAINodePatch)); Log.LogInfo((object)"[PerformanceGuard] Empty AI node crash fix is active."); } Log.LogInfo((object)"PerformanceGuard 1.2.0 loaded."); } private void Update() { ApplyTimeoutsOnce(); TrackFrameRate(); ReportSuppressedCrashes(); } private void ApplyTimeoutsOnce() { if (_timeoutsApplied || (Object)(object)NetworkManager.Singleton == (Object)null || !NetworkManager.Singleton.IsListening) { return; } _timeoutsApplied = true; if (_raiseSceneLoadTimeout.Value) { try { NetworkManager.Singleton.NetworkConfig.LoadSceneTimeOut = _sceneLoadTimeoutSeconds.Value; Log.LogInfo((object)string.Format("[{0}] Level loading may now take up to {1}s before a player is given up on.", "PerformanceGuard", _sceneLoadTimeoutSeconds.Value)); } catch (Exception ex) { Log.LogWarning((object)("[PerformanceGuard] Could not raise the scene load timeout: " + ex.Message)); } } if (!_raiseConnectionTimeout.Value) { return; } try { int num = _connectionTimeoutSeconds.Value * 1000; if (SetSteamTimeout("TimeoutConnected", num) && SetSteamTimeout("TimeoutInitial", Math.Max(num / 2, 10000))) { Log.LogInfo((object)string.Format("[{0}] A player may now stall for up to {1}s before being dropped.", "PerformanceGuard", _connectionTimeoutSeconds.Value)); } else { Log.LogWarning((object)"[PerformanceGuard] Could not reach Steam's timeout settings; the scene load timeout above still applies."); } } catch (Exception ex2) { Log.LogWarning((object)("[PerformanceGuard] Could not raise the connection timeout: " + ex2.Message)); } } private static bool SetSteamTimeout(string settingName, int milliseconds) { try { Type type = AccessTools.TypeByName("Steamworks.SteamNetworkingUtils"); Type type2 = AccessTools.TypeByName("Steamworks.NetConfig"); if (type == null || type2 == null) { return false; } MethodInfo methodInfo = AccessTools.Method(type, "SetConfigInt", new Type[2] { type2, typeof(int) }, (Type[])null); if (methodInfo == null) { return false; } if (!Enum.IsDefined(type2, settingName)) { return false; } object obj = Enum.Parse(type2, settingName); methodInfo.Invoke(null, new object[2] { obj, milliseconds }); return true; } catch (Exception ex) { Log.LogWarning((object)("[PerformanceGuard] Steam timeout '" + settingName + "' could not be set: " + ex.Message)); return false; } } private void TrackFrameRate() { if (!_autoThrottleMaskedAI.Value) { return; } _fpsAccumulator += Time.unscaledDeltaTime; _fpsFrames++; if (Time.unscaledTime < _nextFpsCheck) { return; } _nextFpsCheck = Time.unscaledTime + _fpsCheckSeconds.Value; if (_fpsFrames == 0 || _fpsAccumulator <= 0f) { return; } float num = (float)_fpsFrames / _fpsAccumulator; _fpsAccumulator = 0f; _fpsFrames = 0; if (!_maskedAiThrottled && num < (float)_throttleBelowFps.Value) { if (SetMaskedAiEnabled(enabled: false, out var count) && count > 0) { _maskedAiThrottled = true; Log.LogWarning((object)string.Format("[{0}] Frame rate is {1:F0}. Switched off the added masked AI on {2} enemies to recover.", "PerformanceGuard", num, count)); } } else if (_maskedAiThrottled && num < (float)_restoreAboveFps.Value) { SetMaskedAiEnabled(enabled: false, out var _); } else if (_maskedAiThrottled && num > (float)_restoreAboveFps.Value) { SetMaskedAiEnabled(enabled: true, out var count3); _maskedAiThrottled = false; Log.LogInfo((object)string.Format("[{0}] Frame rate recovered to {1:F0}. Added masked AI switched back on ({2} enemies).", "PerformanceGuard", num, count3)); } } private bool SetMaskedAiEnabled(bool enabled, out int count) { count = 0; if (!_lookedForMaskedAiType) { _lookedForMaskedAiType = true; _maskedAiType = AccessTools.TypeByName("LethalIntelligence.Patches.MaskedAIRevamp"); if (_maskedAiType == null) { Log.LogInfo((object)"[PerformanceGuard] LethalIntelligence is not installed, so there is no masked AI to throttle."); } } if (_maskedAiType == null) { return false; } try { Object[] array = Object.FindObjectsOfType(_maskedAiType); foreach (Object obj in array) { Behaviour val = (Behaviour)(object)((obj is Behaviour) ? obj : null); if (!((Object)(object)val == (Object)null)) { val.enabled = enabled; count++; } } return true; } catch (Exception ex) { Log.LogWarning((object)("[PerformanceGuard] Could not change the masked AI: " + ex.Message)); return false; } } private void ReportSuppressedCrashes() { if (LogSuppressedCrashes.Value && !(Time.unscaledTime < _nextCrashReport)) { _nextCrashReport = Time.unscaledTime + 30f; if (SuppressedCrashCount != _lastReportedCount) { int num = SuppressedCrashCount - _lastReportedCount; _lastReportedCount = SuppressedCrashCount; Log.LogInfo((object)(string.Format("[{0}] Prevented {1} crashes in the last 30s ({2} total this session). ", "PerformanceGuard", num, SuppressedCrashCount) + "Each one would have written a stack trace to disk.")); } } } } [HarmonyPatch(typeof(RoundManager), "GenerateNewLevelClientRpc")] internal static class DungeonSpeedPatch { private static void Postfix(RoundManager __instance) { if (!PerformanceGuardPlugin.FasterDungeon.Value) { return; } try { if (!((Object)(object)__instance.dungeonGenerator == (Object)null) && __instance.dungeonGenerator.Generator != null) { __instance.dungeonGenerator.Generator.MaxAsyncFrameMilliseconds = PerformanceGuardPlugin.DungeonFrameBudget.Value; } } catch (Exception ex) { PerformanceGuardPlugin.Log.LogWarning((object)("[PerformanceGuard] Left interior building alone: " + ex.Message)); } } } [HarmonyPatch(typeof(EnemyAI), "ChooseFarthestNodeFromPosition")] internal static class EmptyAINodePatch { private static bool Prefix(EnemyAI __instance, ref Transform __result) { GameObject[] allAINodes = __instance.allAINodes; if (allAINodes != null && allAINodes.Length != 0) { return true; } PerformanceGuardPlugin.SuppressedCrashCount++; __instance.gotFarthestNodeAsync = true; __result = ((Component)__instance).transform; return false; } } }