Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LoadTimeProfiler v1.3.2
patchers/LoadTimeProfiler.dll
Decompiled 4 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using Mono.Cecil.Cil; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("LoadTimeProfiler")] [assembly: AssemblyDescription("Valheim startup/connection profiler, accelerator, and join-stability patcher")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("LoadTimeProfiler")] [assembly: AssemblyCopyright("Copyright 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("1E310F27-0F47-41D7-94D7-4A3FE40B8539")] [assembly: AssemblyFileVersion("1.3.2")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.3.2.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 LoadTimeProfiler { public static class LoadTimeProfilerPatcher { internal const string ModName = "LoadTimeProfiler"; internal const string ModVersion = "1.3.2"; internal const string Author = "sighsorry"; internal const string ModGUID = "sighsorry.LoadTimeProfiler"; internal const string ConfigFileName = "sighsorry.LoadTimeProfiler.cfg"; private static int _bootstrapped; private static bool _initialized; private static ModConfiguration? _configuration; public static IEnumerable<string> TargetDLLs { get; } = new string[1] { "UnityEngine.CoreModule.dll" }; internal static ModConfiguration? Configuration => Volatile.Read(in _configuration); internal static ManualLogSource? Log { get; private set; } internal static bool ProfilingEnabled { get; private set; } = true; internal static bool LocalizationCacheEnabled { get; private set; } = true; internal static bool ConfigWriteCoalescingEnabled { get; private set; } = true; internal static bool ConfigAutoReloadEnabled { get; private set; } internal static float ConnectionTimeoutSeconds { get; private set; } = 120f; internal static bool TimeoutProtectionEnabled => ConnectionTimeoutSeconds > 0f; internal static bool StartupAccelerationEnabled { get { if (!LocalizationCacheEnabled) { return ConfigWriteCoalescingEnabled; } return true; } } internal static bool RuntimeInstrumentationNeeded { get { if (!ProfilingEnabled) { return LocalizationCacheEnabled; } return true; } } internal static bool AnyStartupFeatureEnabled { get { if (!ProfilingEnabled && !LocalizationCacheEnabled && !ConfigWriteCoalescingEnabled) { return TimeoutProtectionEnabled; } return true; } } internal static bool IsDedicatedServer { get; private set; } public static void Initialize() { if (Interlocked.Exchange(ref _bootstrapped, 1) != 0) { return; } AttachBepInExLogger(); try { ModConfiguration modConfiguration = new ModConfiguration(Path.Combine(Paths.ConfigPath, "sighsorry.LoadTimeProfiler.cfg"), LogWarning, LogInfo); Volatile.Write(ref _configuration, modConfiguration); ModSettings startupSettings = modConfiguration.StartupSettings; ProfilingEnabled = startupSettings.ProfilingEnabled; LocalizationCacheEnabled = startupSettings.LocalizationCacheEnabled; ConfigWriteCoalescingEnabled = startupSettings.ConfigWriteCoalescingEnabled; ConfigAutoReloadEnabled = startupSettings.ConfigAutoReloadEnabled; ConnectionTimeoutSeconds = startupSettings.TimeoutProtectionSeconds; } catch (Exception ex) { LogError("Could not initialize configuration; startup defaults retained and logging filter disabled: " + ex); } ModConfiguration configuration = Configuration; if (configuration != null) { try { LogFiltering.Install(configuration); } catch (Exception ex2) { LogFiltering.Dispose(); LogError("Could not install logging filters; other features remain active: " + ex2); } } if (ConfigAutoReloadEnabled) { try { ConfigAutoReload.Install(); } catch (Exception ex3) { LogWarning("Config auto reload initialization failed: " + ex3.Message); } } AppDomain.CurrentDomain.ProcessExit += Shutdown; AppDomain.CurrentDomain.DomainUnload += Shutdown; } public static void Patch(AssemblyDefinition assembly) { TimelineProfiler.CapturePatcherStart(); try { InjectRuntimeEntrypoint(assembly); } catch (Exception ex) { LogError("Could not inject runtime entrypoint: " + ex); } } public static void Finish() { LogFiltering.CheckForConflicts(); } private static void InjectRuntimeEntrypoint(AssemblyDefinition assembly) { //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) MethodDefinition obj = ((IEnumerable<MethodDefinition>)((IEnumerable<TypeDefinition>)assembly.MainModule.Types).First((TypeDefinition type) => ((TypeReference)type).Namespace == "UnityEngine" && ((MemberReference)type).Name == "GameObject").Methods).First((MethodDefinition val2) => val2.IsConstructor && val2.IsStatic); Instruction val = ((IEnumerable<Instruction>)obj.Body.Instructions).First(delegate(Instruction instruction) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (instruction.OpCode == OpCodes.Call) { object operand = instruction.Operand; MethodReference val2 = (MethodReference)((operand is MethodReference) ? operand : null); if (val2 != null && ((MemberReference)((MemberReference)val2).DeclaringType).FullName == "BepInEx.Bootstrap.Chainloader") { return ((MemberReference)val2).Name == "Start"; } } return false; }); MethodInfo method = typeof(RuntimeEntrypoint).GetMethod("BeforeChainloaderStart", BindingFlags.Static | BindingFlags.Public); MethodInfo method2 = typeof(RuntimeEntrypoint).GetMethod("AfterChainloaderStart", BindingFlags.Static | BindingFlags.Public); ILProcessor iLProcessor = obj.Body.GetILProcessor(); iLProcessor.InsertBefore(val, iLProcessor.Create(OpCodes.Call, assembly.MainModule.ImportReference((MethodBase)method))); iLProcessor.InsertAfter(val, iLProcessor.Create(OpCodes.Call, assembly.MainModule.ImportReference((MethodBase)method2))); } internal static void InitializeProfiler() { Initialize(); if (!_initialized) { _initialized = true; IsDedicatedServer = (Paths.ProcessName ?? string.Empty).IndexOf("valheim_server", StringComparison.OrdinalIgnoreCase) >= 0; if (ProfilingEnabled) { ProfilerLog.Initialize(); TimelineProfiler.BeginStartup(IsDedicatedServer); LogInfo("Writing " + (IsDedicatedServer ? "dedicated server startup" : "startup and connection") + " profiles to " + ProfilerLog.FilePath + "."); } else { LogInfo("Profiling is disabled; independently enabled acceleration, timeout and logging features remain active."); } } } internal static void AttachBepInExLogger() { if (Log != null) { return; } try { Log = Logger.CreateLogSource("LoadTimeProfiler"); } catch (Exception ex) { WriteConsole("Could not create log source: " + ex.Message); } } internal static void LogInfo(string message) { WriteLog((LogLevel)16, message); } internal static void LogWarning(string message) { WriteLog((LogLevel)4, message); } internal static void LogError(string message) { WriteLog((LogLevel)2, message); } private static void WriteLog(LogLevel level, string message) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) try { if (Log != null) { Log.Log(level, (object)message); } else { WriteConsole(message); } } catch { WriteConsole(message); } } private static void WriteConsole(string message) { try { Console.Error.WriteLine("[LoadTimeProfiler] " + message); } catch { } } private static void Shutdown(object? sender, EventArgs args) { ConfigAutoReload.Dispose(); ConfigManagerIntegration.Dispose(); LogFiltering.Disable(); Interlocked.Exchange(ref _configuration, null)?.Dispose(); } } [Flags] public enum LogGroups { None = 0, [Description("Fatal + Error")] Errors = 1, [Description("Warning")] Warnings = 2, [Description("Message + Info")] Information = 4, [Description("Debug")] Debug = 8 } internal sealed class LogPolicy { internal const LogGroups DefaultGroups = LogGroups.Errors | LogGroups.Warnings; internal const LogGroups EveryGroup = LogGroups.Errors | LogGroups.Warnings | LogGroups.Information | LogGroups.Debug; private static readonly LogLevel[] SeverityMasks = CreateSeverityMasks(); private readonly Dictionary<string, LogGroups> _mods; private readonly bool _failOpen; internal string Description { get; } internal LogPolicy(Dictionary<string, LogGroups> mods, bool failOpen = false) { _failOpen = failOpen; _mods = new Dictionary<string, LogGroups>(mods, StringComparer.Ordinal); StringBuilder stringBuilder = new StringBuilder(); if (failOpen) { stringBuilder.Append("configuration unavailable: all logs pass; "); } stringBuilder.Append("default groups=").Append(LogGroups.Errors | LogGroups.Warnings).Append("; Unity/unmapped=pass; mods=["); List<string> list = new List<string>(_mods.Keys); list.Sort(StringComparer.Ordinal); for (int i = 0; i < list.Count; i++) { if (i > 0) { stringBuilder.Append("; "); } stringBuilder.Append(list[i]).Append('=').Append(_mods[list[i]]); } Description = stringBuilder.Append(']').ToString(); } internal LogGroups GetModGroups(string guid) { if (!_failOpen) { if (!_mods.TryGetValue(guid, out var value)) { return LogGroups.Errors | LogGroups.Warnings; } return value; } return LogGroups.Errors | LogGroups.Warnings | LogGroups.Information | LogGroups.Debug; } internal bool Allows(string? guid, LogLevel level) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if (_failOpen || guid == null) { return true; } LogGroups modGroups = GetModGroups(guid); if (modGroups == (LogGroups.Errors | LogGroups.Warnings | LogGroups.Information | LogGroups.Debug)) { return true; } return (level & SeverityMasks[(int)modGroups]) > 0; } private static LogLevel[] CreateSeverityMasks() { LogLevel[] array = (LogLevel[])(object)new LogLevel[16]; for (int i = 0; i < array.Length; i++) { int num = i; if ((num & 1) != 0) { ref LogLevel reference = ref array[i]; reference = (LogLevel)((uint)reference | 3u); } if ((num & 2) != 0) { ref LogLevel reference2 = ref array[i]; reference2 = (LogLevel)((uint)reference2 | 4u); } if ((num & 4) != 0) { ref LogLevel reference3 = ref array[i]; reference3 = (LogLevel)((uint)reference3 | 0x18u); } if ((num & 8) != 0) { ref LogLevel reference4 = ref array[i]; reference4 = (LogLevel)((uint)reference4 | 0x20u); } } return array; } internal bool HasSameValues(LogPolicy other) { if (_failOpen != other._failOpen || _mods.Count != other._mods.Count) { return false; } foreach (KeyValuePair<string, LogGroups> mod in _mods) { if (!other._mods.TryGetValue(mod.Key, out var value) || value != mod.Value) { return false; } } return true; } } internal sealed class ModSettings { internal bool ProfilingEnabled { get; } internal bool LocalizationCacheEnabled { get; } internal bool ConfigWriteCoalescingEnabled { get; } internal bool ConfigAutoReloadEnabled { get; } internal float TimeoutProtectionSeconds { get; } internal LogPolicy Logging { get; } internal long Revision { get; } internal ModSettings(bool profilingEnabled, bool localizationCacheEnabled, bool configWriteCoalescingEnabled, float timeoutProtectionSeconds, LogPolicy logging, long revision = 0L, bool configAutoReloadEnabled = false) { ProfilingEnabled = profilingEnabled; LocalizationCacheEnabled = localizationCacheEnabled; ConfigWriteCoalescingEnabled = configWriteCoalescingEnabled; ConfigAutoReloadEnabled = configAutoReloadEnabled; TimeoutProtectionSeconds = timeoutProtectionSeconds; Logging = logging; Revision = revision; } internal bool HasSameStartupValues(ModSettings other) { if (ProfilingEnabled == other.ProfilingEnabled && LocalizationCacheEnabled == other.LocalizationCacheEnabled && ConfigWriteCoalescingEnabled == other.ConfigWriteCoalescingEnabled && ConfigAutoReloadEnabled == other.ConfigAutoReloadEnabled) { return TimeoutProtectionSeconds == other.TimeoutProtectionSeconds; } return false; } internal bool HasSameValues(ModSettings other) { if (HasSameStartupValues(other)) { return Logging.HasSameValues(other.Logging); } return false; } internal ModSettings WithRevision(long revision) { return new ModSettings(ProfilingEnabled, LocalizationCacheEnabled, ConfigWriteCoalescingEnabled, TimeoutProtectionSeconds, Logging, revision, ConfigAutoReloadEnabled); } } internal sealed class ModConfiguration : IDisposable { internal const int MaximumFileBytes = 262144; private const int PollMilliseconds = 500; private static readonly Encoding ConfigEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private readonly string _configPath; private readonly Action<string> _warning; private readonly Action<string> _notice; private readonly object _publicationLock = new object(); private readonly Timer _timer; private ModSettings _snapshot = CreateFallback(); private int _disposed; private int _reloadBusy; private bool _startupCaptured; private string? _loadedText; private string? _candidateText; private string? _lastWarning; internal ModSettings Snapshot => Volatile.Read(in _snapshot); internal ModSettings StartupSettings { get; } internal ModConfiguration(string configPath, Action<string> warning, Action<string> notice) { _configPath = Path.GetFullPath(configPath ?? throw new ArgumentNullException("configPath")); _warning = warning ?? throw new ArgumentNullException("warning"); _notice = notice ?? throw new ArgumentNullException("notice"); CreateDefaultFileIfMissing(); ReloadNow(); StartupSettings = Snapshot; _startupCaptured = true; _timer = new Timer(Poll, null, 500, 500); } internal bool ReloadNow() { return TryReload(immediate: true); } internal object GetSettingValue(string section, string key) { if (section == null) { throw new ArgumentNullException("section"); } if (key == null) { throw new ArgumentNullException("key"); } ModSettings snapshot = Snapshot; if (section == "General") { switch (key) { case "ProfilingEnabled": return snapshot.ProfilingEnabled; case "LocalizationCacheEnabled": return snapshot.LocalizationCacheEnabled; case "ConfigWriteCoalescingEnabled": return snapshot.ConfigWriteCoalescingEnabled; case "ConfigAutoReloadEnabled": return snapshot.ConfigAutoReloadEnabled; case "TimeoutProtectionSeconds": return snapshot.TimeoutProtectionSeconds; } } throw new ArgumentException("Unknown setting: " + section + "." + key + "."); } internal void SetSettingValue(string section, string key, object value) { object settingValue = GetSettingValue(section, key); if (value == null || value.GetType() != settingValue.GetType()) { throw new ArgumentException(section + "." + key + " requires a " + settingValue.GetType().Name + " value.", "value"); } string formatted; if (value is bool flag) { formatted = (flag ? "true" : "false"); } else { float num = (float)value; if (float.IsNaN(num) || float.IsInfinity(num) || num < 0f) { throw new ArgumentOutOfRangeException("value", "Timeout must be a finite, non-negative number."); } formatted = num.ToString("R", CultureInfo.InvariantCulture); } SaveEdit((string text) => EditSetting(text, section, key, formatted)); } internal LogGroups GetModLogGroups(string guid) { if (guid == null) { throw new ArgumentNullException("guid"); } return Snapshot.Logging.GetModGroups(guid); } internal void SetModLogGroups(string guid, LogGroups groups) { if (!IsValidGuid(guid)) { throw new ArgumentException("A mod GUID must be a non-empty, unambiguous configuration key.", "guid"); } if ((groups & ~(LogGroups.Errors | LogGroups.Warnings | LogGroups.Information | LogGroups.Debug)) != LogGroups.None) { throw new ArgumentOutOfRangeException("groups", "Unsupported log group flags."); } SaveEdit((string document) => EditSetting(document, "Logging.Mods", guid, groups.ToString())); } private static bool IsValidGuid(string? guid) { if (string.IsNullOrEmpty(guid) || char.IsWhiteSpace(guid[0]) || char.IsWhiteSpace(guid[guid.Length - 1]) || guid[0] == '#' || guid[0] == ';') { return false; } foreach (char c in guid) { if (char.IsControl(c) || c == '[' || c == ']' || c == '=' || c == '\ufeff') { return false; } } return true; } private void SaveEdit(Func<string, string> edit) { if (Volatile.Read(in _disposed) != 0) { throw new ObjectDisposedException("ModConfiguration"); } if (Interlocked.CompareExchange(ref _reloadBusy, 1, 0) != 0) { throw new InvalidOperationException("Another configuration reload or save is in progress; retry the change."); } string text = null; bool restartRequired = false; try { string text2 = ReadConfigText(); string text3 = edit(text2); string warning; ModSettings parsed = Parse(text3, (_loadedText == null) ? null : Snapshot, out warning); byte[] bytes = ConfigEncoding.GetBytes(text3); if (bytes.Length > 262144) { throw new InvalidDataException("The edited configuration exceeds the 256 KiB limit."); } text = _configPath + "." + Guid.NewGuid().ToString("N") + ".tmp"; using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } lock (_publicationLock) { if (_disposed != 0) { throw new ObjectDisposedException("ModConfiguration"); } using FileStream stream = OpenConfigRead(); if (!string.Equals(text2, ReadConfigText(stream), StringComparison.Ordinal)) { throw new IOException("Configuration changed on disk during the save; review the latest values and retry."); } ModSettings settings = PreparePublication(parsed, out restartRequired); File.Replace(text, _configPath, null); text = null; Publish(text3, settings); } NotifyRestart(restartRequired); if (warning.Length > 0) { WarnOnce(warning); } else { _lastWarning = null; } } finally { if (text != null) { try { File.Delete(text); } catch (Exception ex) { Notify(_warning, "Could not remove temporary configuration file: " + ex.Message); } } Volatile.Write(ref _reloadBusy, 0); } } private static string EditSetting(string text, string section, string key, string value) { string text2 = string.Empty; int num = -1; int num2 = -1; int startIndex = -1; int num3 = 0; while (num3 < text.Length) { int next; int lineEnd = GetLineEnd(text, num3, out next); string text3 = text.Substring(num3, lineEnd - num3); string text4 = ((num3 == 0) ? text3.TrimStart(new char[1] { '\ufeff' }) : text3).Trim(); if (text4.StartsWith("[", StringComparison.Ordinal)) { text2 = (text4.EndsWith("]", StringComparison.Ordinal) ? text4.Substring(1, text4.Length - 2) : string.Empty); if (text2 == section) { num = next; } } else if (text2 == section && text4.Length > 0 && text4[0] != '#' && text4[0] != ';') { int num4 = text3.IndexOf('='); if (num4 >= 0 && string.Equals(text3.Substring(0, num4).Trim(), key, StringComparison.Ordinal)) { int i = num4 + 1; int num5; for (num5 = text3.Length; i < num5 && char.IsWhiteSpace(text3[i]); i++) { } while (num5 > i && char.IsWhiteSpace(text3[num5 - 1])) { num5--; } num2 = num3 + i; startIndex = num3 + num5; } } num3 = next; } if (num2 >= 0) { return text.Substring(0, num2) + value + text.Substring(startIndex); } string text5 = (text.Contains("\r\n") ? "\r\n" : (text.Contains("\n") ? "\n" : (text.Contains("\r") ? "\r" : Environment.NewLine))); string text6 = key + " = " + value + text5; if (num < 0) { return text + ((text.Length > 0 && text[text.Length - 1] != '\r' && text[text.Length - 1] != '\n') ? text5 : string.Empty) + "[" + section + "]" + text5 + text6; } return text.Insert(num, ((num > 0 && text[num - 1] != '\r' && text[num - 1] != '\n') ? text5 : string.Empty) + text6); } private static int GetLineEnd(string text, int start, out int next) { int i; for (i = start; i < text.Length && text[i] != '\r' && text[i] != '\n'; i++) { } next = i; if (next < text.Length && text[next++] == '\r' && next < text.Length && text[next] == '\n') { next++; } return i; } public void Dispose() { lock (_publicationLock) { if (_disposed == 0) { Volatile.Write(ref _disposed, 1); _timer.Dispose(); } } } private static ModSettings CreateFallback(bool failOpen = true) { return new ModSettings(profilingEnabled: true, localizationCacheEnabled: true, configWriteCoalescingEnabled: true, 120f, new LogPolicy(new Dictionary<string, LogGroups>(StringComparer.Ordinal), failOpen), 0L); } private void Poll(object? state) { TryReload(immediate: false); } private bool TryReload(bool immediate) { if (Volatile.Read(in _disposed) != 0 || Interlocked.CompareExchange(ref _reloadBusy, 1, 0) != 0) { return false; } try { string text; try { text = ReadConfigText(); } catch (Exception ex) { _candidateText = null; WarnOnce("Could not read configuration; retaining current settings: " + ex.Message); return false; } if (string.Equals(text, _loadedText, StringComparison.Ordinal)) { _candidateText = null; return true; } if (!immediate && !string.Equals(text, _candidateText, StringComparison.Ordinal)) { _candidateText = text; return false; } _candidateText = text; string warning; ModSettings parsed = Parse(text, (_loadedText == null) ? null : Snapshot, out warning); bool restartRequired = false; lock (_publicationLock) { if (_disposed != 0) { return false; } ModSettings settings = PreparePublication(parsed, out restartRequired); Publish(text, settings); } NotifyRestart(restartRequired); if (warning.Length > 0) { WarnOnce(warning); } else { _lastWarning = null; } return true; } catch (Exception ex2) { WarnOnce("Configuration reload failed; retaining current settings: " + ex2.Message); return false; } finally { Volatile.Write(ref _reloadBusy, 0); } } private string ReadConfigText() { using FileStream stream = OpenConfigRead(); return ReadConfigText(stream); } private FileStream OpenConfigRead() { return new FileStream(_configPath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); } private static string ReadConfigText(FileStream stream) { if (stream.Length > 262144) { throw new InvalidDataException("Configuration exceeds the 256 KiB limit."); } byte[] array = new byte[(int)stream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = stream.Read(array, i, array.Length - i); if (num == 0) { throw new EndOfStreamException("Configuration changed while being read."); } } return ConfigEncoding.GetString(array); } private ModSettings PreparePublication(ModSettings parsed, out bool restartRequired) { ModSettings snapshot = _snapshot; restartRequired = false; if (snapshot.HasSameValues(parsed)) { return snapshot; } ModSettings modSettings = parsed.WithRevision(snapshot.Revision + 1); restartRequired = _startupCaptured && !snapshot.HasSameStartupValues(modSettings) && !StartupSettings.HasSameStartupValues(modSettings); return modSettings; } private void Publish(string text, ModSettings settings) { Volatile.Write(ref _snapshot, settings); _loadedText = text; _candidateText = null; } private void NotifyRestart(bool restartRequired) { if (restartRequired && Volatile.Read(in _disposed) == 0) { Notify(_notice, "General settings changed; restart required. Logging settings are reloaded immediately."); } } private void CreateDefaultFileIfMissing() { if (File.Exists(_configPath)) { return; } try { string directoryName = Path.GetDirectoryName(_configPath); if (directoryName != null) { Directory.CreateDirectory(directoryName); } using FileStream stream = new FileStream(_configPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read); using StreamWriter streamWriter = new StreamWriter(stream, ConfigEncoding); streamWriter.WriteLine("# LoadTimeProfiler configuration. Missing settings use their current or default values."); streamWriter.WriteLine("# Logging changes reload automatically. Other changes require a restart."); streamWriter.WriteLine("# Invalid values keep that setting's current or default value; unknown entries are ignored."); streamWriter.WriteLine("# Reload never writes this file; UI edits preserve unrelated lines."); streamWriter.WriteLine(); streamWriter.WriteLine("[General]"); streamWriter.WriteLine("ProfilingEnabled = true"); streamWriter.WriteLine("LocalizationCacheEnabled = true"); streamWriter.WriteLine("ConfigWriteCoalescingEnabled = true"); streamWriter.WriteLine("# Reload other mods' BepInEx cfg files after local file edits. Restart required."); streamWriter.WriteLine("# Each mod determines whether its changed values take effect during play."); streamWriter.WriteLine("ConfigAutoReloadEnabled = false"); streamWriter.WriteLine("# Set to 0 to disable the timeout floor. Longer original timeouts are retained."); streamWriter.WriteLine("TimeoutProtectionSeconds = 120"); streamWriter.WriteLine(); streamWriter.WriteLine("[Logging.Mods]"); streamWriter.WriteLine("# Exact, case-sensitive plugin GUIDs. Unlisted plugins use Errors, Warnings."); streamWriter.WriteLine("# Groups: Errors (Fatal + Error), Warnings, Information (Message + Info), Debug."); streamWriter.WriteLine("# Use None to disable a mod's default logger; combine group names with commas."); streamWriter.WriteLine("# Unity and unattributed/custom loggers pass through unchanged."); streamWriter.WriteLine("# author.examplemod = Errors, Warnings"); } catch (IOException) when (File.Exists(_configPath)) { } catch (Exception ex2) { WarnOnce("Could not create default configuration: " + ex2.Message); } } private static ModSettings Parse(string text, ModSettings? previous, out string warning) { if (previous == null) { previous = CreateFallback(failOpen: false); } bool profilingEnabled = previous.ProfilingEnabled; bool localizationCacheEnabled = previous.LocalizationCacheEnabled; bool configWriteCoalescingEnabled = previous.ConfigWriteCoalescingEnabled; bool configAutoReloadEnabled = previous.ConfigAutoReloadEnabled; float timeoutProtectionSeconds = previous.TimeoutProtectionSeconds; Dictionary<string, LogGroups> dictionary = new Dictionary<string, LogGroups>(StringComparer.Ordinal); int invalidCount = 0; int firstInvalidLine = 0; string text2 = string.Empty; if (text.Length > 0 && text[0] == '\ufeff') { text = text.Substring(1); } using (StringReader stringReader = new StringReader(text)) { int num = 0; string text3; while ((text3 = stringReader.ReadLine()) != null) { num++; string text4 = text3.Trim(); if (text4.Length == 0 || text4[0] == '#' || text4[0] == ';') { continue; } if (text4[0] == '[') { text2 = ((text4[text4.Length - 1] == ']') ? text4.Substring(1, text4.Length - 2) : string.Empty); if (text2.Length == 0) { Invalid(num); } } else { if (text2 != "General" && text2 != "Logging.Mods") { continue; } int num2 = text4.IndexOf('='); if (num2 <= 0) { Invalid(num); continue; } string text5 = text4.Substring(0, num2).Trim(); string text6 = text4.Substring(num2 + 1).Trim(); if (text5.Length == 0) { Invalid(num); } else if (text2 == "Logging.Mods") { if (!IsValidGuid(text5)) { Invalid(num); continue; } if (TryParseGroups(text6, out var groups)) { dictionary[text5] = groups; continue; } Invalid(num); if (!dictionary.ContainsKey(text5)) { dictionary[text5] = previous.Logging.GetModGroups(text5); } } else if (text5 == "TimeoutProtectionSeconds") { if (float.TryParse(text6, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && !float.IsNaN(result) && !float.IsInfinity(result) && result >= 0f) { timeoutProtectionSeconds = result; } else { Invalid(num); } } else { if (text5 != "ProfilingEnabled" && text5 != "LocalizationCacheEnabled" && text5 != "ConfigWriteCoalescingEnabled" && text5 != "ConfigAutoReloadEnabled") { continue; } if (!bool.TryParse(text6, out var result2)) { Invalid(num); continue; } switch (text5) { case "ProfilingEnabled": profilingEnabled = result2; break; case "LocalizationCacheEnabled": localizationCacheEnabled = result2; break; case "ConfigWriteCoalescingEnabled": configWriteCoalescingEnabled = result2; break; default: configAutoReloadEnabled = result2; break; } } } } warning = ((invalidCount == 0) ? string.Empty : ("Ignored " + invalidCount + " invalid configuration entr" + ((invalidCount == 1) ? "y" : "ies") + " (first on line " + firstInvalidLine + "). Valid entries were applied; affected settings retain their current or default values.")); return new ModSettings(profilingEnabled, localizationCacheEnabled, configWriteCoalescingEnabled, timeoutProtectionSeconds, new LogPolicy(dictionary), 0L, configAutoReloadEnabled); } void Invalid(int line) { if (invalidCount++ == 0) { firstInvalidLine = line; } } } private static bool TryParseGroups(string value, out LogGroups groups) { groups = LogGroups.None; if (string.Equals(value, "None", StringComparison.OrdinalIgnoreCase)) { return true; } string[] array = value.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string a = array[i].Trim(); if (string.Equals(a, "Errors", StringComparison.OrdinalIgnoreCase)) { groups |= LogGroups.Errors; continue; } if (string.Equals(a, "Warnings", StringComparison.OrdinalIgnoreCase)) { groups |= LogGroups.Warnings; continue; } if (string.Equals(a, "Information", StringComparison.OrdinalIgnoreCase)) { groups |= LogGroups.Information; continue; } if (string.Equals(a, "Debug", StringComparison.OrdinalIgnoreCase)) { groups |= LogGroups.Debug; continue; } return false; } return true; } private void WarnOnce(string message) { if (Volatile.Read(in _disposed) == 0 && !string.Equals(_lastWarning, message, StringComparison.Ordinal)) { _lastWarning = message; Notify(_warning, message); } } private static void Notify(Action<string> callback, string message) { try { callback(message); } catch { } } } internal static class ConfigAutoReload { private static class Runtime { internal static void Dispatch(Action action) { ThreadingHelper.Instance.StartSyncInvoke(action); } internal static void Start() { WatchService watchService = Volatile.Read(in _service); if (watchService == null) { return; } if ((Object)(object)ThreadingHelper.Instance == (Object)null) { throw new InvalidOperationException("BepInEx main-thread dispatcher is unavailable."); } foreach (PluginInfo value in Chainloader.PluginInfos.Values) { if ((Object)(object)value.Instance != (Object)null) { watchService.Track(value.Instance.Config); } } watchService.Start(); } } internal sealed class WatchService : IDisposable { private sealed class WatchedFile { internal readonly string Path; internal readonly string Directory; internal readonly List<WatchedConfig> Configs = new List<WatchedConfig>(); internal byte[]? Applied; internal byte[]? Candidate; internal string? LastError; internal long Generation; internal long Due; internal bool Queued; internal WatchedFile(string path) { Path = path; Directory = System.IO.Path.GetDirectoryName(path); } } private sealed class WatchedConfig { internal readonly WeakReference<ConfigFile> Reference; internal byte[]? Applied; internal WatchedConfig(ConfigFile config, byte[]? applied) { Reference = new WeakReference<ConfigFile>(config); Applied = applied; } } private const int PollMilliseconds = 250; private const int SettleMilliseconds = 500; private static readonly StringComparer PathComparer = ((Path.DirectorySeparatorChar == '\\') ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); private readonly object _gate = new object(); private readonly Dictionary<string, WatchedFile> _files = new Dictionary<string, WatchedFile>(PathComparer); private readonly Dictionary<string, FileSystemWatcher> _watchers = new Dictionary<string, FileSystemWatcher>(PathComparer); private readonly HashSet<WatchedFile> _pending = new HashSet<WatchedFile>(); private readonly Action<Action> _dispatch; private readonly Action<string> _warning; private readonly string _excludedPath; private readonly Timer _timer; private bool _started; private bool _disposed; private bool _ticking; private int _pollBusy; private static long Now => (long)((double)Stopwatch.GetTimestamp() * (1000.0 / (double)Stopwatch.Frequency)); internal WatchService(Action<Action> dispatch, Action<string> warning, string excludedPath) { _dispatch = dispatch ?? throw new ArgumentNullException("dispatch"); _warning = warning ?? throw new ArgumentNullException("warning"); _excludedPath = Path.GetFullPath(excludedPath); _timer = new Timer(Poll, null, -1, -1); } internal void Track(ConfigFile config) { string fullPath = Path.GetFullPath(config.ConfigFilePath); if (PathComparer.Equals(fullPath, _excludedPath) || !string.Equals(Path.GetExtension(fullPath), ".cfg", StringComparison.OrdinalIgnoreCase)) { return; } lock (_gate) { if (_disposed) { return; } if (!_files.TryGetValue(fullPath, out WatchedFile value)) { value = new WatchedFile(fullPath); _files.Add(fullPath, value); if (_started) { CaptureBaseline(value); BeginWatching(value); } } for (int num = value.Configs.Count - 1; num >= 0; num--) { if (!value.Configs[num].Reference.TryGetTarget(out ConfigFile target)) { value.Configs.RemoveAt(num); } else if (target == config) { return; } } value.Configs.Add(new WatchedConfig(config, value.Applied)); } } internal void Start() { lock (_gate) { if (_disposed || _started) { return; } _started = true; foreach (WatchedFile value in _files.Values) { CaptureBaseline(value); } foreach (WatchedFile value2 in _files.Values) { BeginWatching(value2); } } } private void CaptureBaseline(WatchedFile file) { try { file.Applied = Fingerprint(file.Path); } catch (FileNotFoundException) { file.Applied = null; } catch (DirectoryNotFoundException) { file.Applied = null; } catch (Exception exception) { Warn(file, exception); } foreach (WatchedConfig config in file.Configs) { config.Applied = file.Applied; } } private void BeginWatching(WatchedFile file) { try { EnsureWatcher(file.Directory); } catch (Exception exception) { Warn(file, exception); } Dirty(file); } private void EnsureWatcher(string directory) { if (_watchers.ContainsKey(directory) || !Directory.Exists(directory)) { return; } FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(directory) { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite) }; fileSystemWatcher.Changed += FileChanged; fileSystemWatcher.Created += FileChanged; fileSystemWatcher.Deleted += FileChanged; fileSystemWatcher.Renamed += FileRenamed; fileSystemWatcher.Error += WatcherError; try { fileSystemWatcher.EnableRaisingEvents = true; _watchers.Add(directory, fileSystemWatcher); } catch { ReleaseWatcher(fileSystemWatcher); throw; } } private void FileChanged(object sender, FileSystemEventArgs args) { lock (_gate) { if (!_disposed && _files.TryGetValue(args.FullPath, out WatchedFile value)) { Dirty(value); } } } private void FileRenamed(object sender, RenamedEventArgs args) { lock (_gate) { if (!_disposed) { if (_files.TryGetValue(args.OldFullPath, out WatchedFile value)) { Dirty(value); } if (_files.TryGetValue(args.FullPath, out WatchedFile value2)) { Dirty(value2); } } } } private void WatcherError(object sender, ErrorEventArgs args) { lock (_gate) { if (_disposed) { return; } string path = ((FileSystemWatcher)sender).Path; if (!_watchers.TryGetValue(path, out FileSystemWatcher value) || value != sender) { return; } Report("Config watcher will recheck registered files after a filesystem error: " + args.GetException().Message); _watchers.Remove(path); ReleaseWatcher((FileSystemWatcher)sender); foreach (WatchedFile value2 in _files.Values) { if (PathComparer.Equals(value2.Directory, path)) { Dirty(value2); } } } } private void Dirty(WatchedFile file) { file.Generation++; file.Candidate = null; file.Due = Now + 500; _pending.Add(file); if (!_ticking) { _ticking = true; _timer.Change(250, 250); } } private void Poll(object? state) { if (Interlocked.Exchange(ref _pollBusy, 1) != 0) { return; } try { WatchedFile[] array; lock (_gate) { if (_disposed) { return; } array = new WatchedFile[_pending.Count]; _pending.CopyTo(array); } WatchedFile[] array2 = array; foreach (WatchedFile file in array2) { CheckFile(file); } } catch (Exception ex) { Report("Config watcher check failed: " + ex.Message); } finally { Volatile.Write(ref _pollBusy, 0); } } private void CheckFile(WatchedFile file) { long generation; lock (_gate) { if (_disposed || !_pending.Contains(file) || file.Queued || file.Due > Now) { return; } if (!Prune(file)) { RemoveFile(file); return; } generation = file.Generation; } try { lock (_gate) { if (_disposed) { return; } EnsureWatcher(file.Directory); } byte[] fingerprint = Fingerprint(file.Path); lock (_gate) { if (_disposed || file.Generation != generation) { return; } if (AllApplied(file, fingerprint)) { Complete(file, fingerprint); return; } if (!Equal(fingerprint, file.Candidate)) { file.Candidate = fingerprint; file.Due = Now + 250; return; } file.Queued = true; } _dispatch(delegate { Reload(file, generation, fingerprint); }); } catch (FileNotFoundException) { Missing(file, generation); } catch (DirectoryNotFoundException) { Missing(file, generation); } catch (Exception exception) { Retry(file, exception); } } private void Reload(WatchedFile file, long generation, byte[] expected) { List<KeyValuePair<ConfigFile, WatchedConfig>> list = new List<KeyValuePair<ConfigFile, WatchedConfig>>(); lock (_gate) { file.Queued = false; if (_disposed || file.Generation != generation || !_pending.Contains(file)) { return; } foreach (WatchedConfig config in file.Configs) { if (config.Reference.TryGetTarget(out ConfigFile target) && !Equal(config.Applied, expected)) { list.Add(new KeyValuePair<ConfigFile, WatchedConfig>(target, config)); } } } try { if (!Equal(expected, Fingerprint(file.Path))) { lock (_gate) { if (!_disposed) { Dirty(file); } return; } } Exception ex = null; foreach (KeyValuePair<ConfigFile, WatchedConfig> item in list) { lock (_gate) { if (_disposed) { return; } } ConfigFile key = item.Key; bool saveOnConfigSet = key.SaveOnConfigSet; try { key.SaveOnConfigSet = false; key.Reload(); lock (_gate) { item.Value.Applied = expected; } } catch (Exception ex2) { lock (_gate) { item.Value.Applied = null; } ex = ex2; } finally { key.SaveOnConfigSet = saveOnConfigSet; } } if (ex != null) { Retry(file, ex); return; } lock (_gate) { if (!_disposed && file.Generation == generation) { Complete(file, expected); } } } catch (FileNotFoundException) { Missing(file, generation); } catch (DirectoryNotFoundException) { Missing(file, generation); } catch (Exception exception) { Retry(file, exception); } } private void Missing(WatchedFile file, long generation) { lock (_gate) { if (_disposed || file.Generation != generation) { return; } file.Queued = false; file.Applied = null; foreach (WatchedConfig config in file.Configs) { config.Applied = null; } if (!_watchers.ContainsKey(file.Directory)) { file.Due = Now + 5000; return; } _pending.Remove(file); StopIdleTimer(); } } private void Retry(WatchedFile file, Exception exception) { lock (_gate) { if (!_disposed) { file.Queued = false; file.Candidate = null; file.Due = Now + 1000; Warn(file, exception); } } } private void Complete(WatchedFile file, byte[] fingerprint) { file.Applied = fingerprint; file.LastError = null; file.Candidate = null; file.Queued = false; _pending.Remove(file); StopIdleTimer(); } private static bool Prune(WatchedFile file) { for (int num = file.Configs.Count - 1; num >= 0; num--) { if (!file.Configs[num].Reference.TryGetTarget(out ConfigFile _)) { file.Configs.RemoveAt(num); } } return file.Configs.Count != 0; } private static bool AllApplied(WatchedFile file, byte[] fingerprint) { foreach (WatchedConfig config in file.Configs) { if (config.Reference.TryGetTarget(out ConfigFile _) && !Equal(config.Applied, fingerprint)) { return false; } } return true; } private void RemoveFile(WatchedFile file) { _files.Remove(file.Path); _pending.Remove(file); bool flag = false; foreach (WatchedFile value2 in _files.Values) { if (PathComparer.Equals(value2.Directory, file.Directory)) { flag = true; break; } } if (!flag && _watchers.TryGetValue(file.Directory, out FileSystemWatcher value)) { _watchers.Remove(file.Directory); ReleaseWatcher(value); } StopIdleTimer(); } private void StopIdleTimer() { if (_pending.Count == 0 && _ticking) { _ticking = false; _timer.Change(-1, -1); } } private static byte[] Fingerprint(string path) { using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(inputStream); } private static bool Equal(byte[]? left, byte[]? right) { if (left == null || right == null || left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (left[i] != right[i]) { return false; } } return true; } private void Warn(WatchedFile file, Exception exception) { string message = exception.Message; if (!(file.LastError == message)) { file.LastError = message; Report("Could not reload " + file.Path + "; will retry: " + message); } } private void Report(string message) { try { _warning(message); } catch { } } private void ReleaseWatcher(FileSystemWatcher watcher) { watcher.Changed -= FileChanged; watcher.Created -= FileChanged; watcher.Deleted -= FileChanged; watcher.Renamed -= FileRenamed; watcher.Error -= WatcherError; watcher.Dispose(); } public void Dispose() { lock (_gate) { if (_disposed) { return; } _disposed = true; _timer.Dispose(); foreach (FileSystemWatcher value in _watchers.Values) { ReleaseWatcher(value); } _watchers.Clear(); _files.Clear(); _pending.Clear(); } } } private const string Owner = "sighsorry.LoadTimeProfiler.config-auto-reload"; private static readonly object LifecycleLock = new object(); private static WatchService? _service; private static Harmony? _harmony; internal static void Install() { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown lock (LifecycleLock) { if (_service != null || HasStandaloneWatcher()) { return; } WatchService watchService = new WatchService(Runtime.Dispatch, LoadTimeProfilerPatcher.LogWarning, Path.Combine(Paths.ConfigPath, "sighsorry.LoadTimeProfiler.cfg")); Harmony val = new Harmony("sighsorry.LoadTimeProfiler.config-auto-reload"); try { _service = watchService; ConstructorInfo constructorInfo = AccessTools.Constructor(typeof(ConfigFile), new Type[3] { typeof(string), typeof(bool), typeof(BepInPlugin) }, false) ?? throw new MissingMethodException("ConfigFile constructor"); val.Patch((MethodBase)constructorInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(ConfigAutoReload), "Constructed", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony = val; } catch { _service = null; watchService.Dispose(); val.UnpatchSelf(); throw; } } } private static void Constructed(ConfigFile __instance) { try { Volatile.Read(in _service)?.Track(__instance); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Could not track config for reload: " + ex.Message); } } [MethodImpl(MethodImplOptions.NoInlining)] internal static void Start() { if (Volatile.Read(in _service) == null) { return; } if (HasStandaloneWatcher()) { Dispose(); return; } try { Runtime.Start(); } catch (Exception ex) { Dispose(); LoadTimeProfilerPatcher.LogWarning("Config auto reload could not start: " + ex.Message); } } private static bool HasStandaloneWatcher() { bool flag = Harmony.HasAnyPatches("org.bepinex.patchers.configwatcher"); if (!flag) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { if (string.Equals(assemblies[i].GetName().Name, "ConfigWatcher", StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } } if (flag) { LoadTimeProfilerPatcher.LogWarning("Config auto reload is inactive because standalone ConfigWatcher is loaded. Remove it and restart to use LoadTimeProfiler's watcher. Other features remain active."); } return flag; } internal static void Dispose() { lock (LifecycleLock) { Interlocked.Exchange(ref _service, null)?.Dispose(); Harmony harmony = _harmony; _harmony = null; if (harmony != null) { try { harmony.UnpatchSelf(); return; } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Config tracking cleanup failed: " + ex.Message); return; } } } } } internal static class ConfigManagerIntegration { private const string ManagerGuid = "sighsorry.ConfigManager"; private static readonly object[] GroupValues = CreateGroupValues(); private static IDisposable? _registration; private static int _attempted; private static int _stopped; private static object[] CreateGroupValues() { object[] array = new object[16]; for (int i = 0; i < array.Length; i++) { array[i] = (LogGroups)i; } return array; } [MethodImpl(MethodImplOptions.NoInlining)] internal static void TryRegister() { if (Volatile.Read(in _stopped) != 0 || Interlocked.Exchange(ref _attempted, 1) != 0 || LoadTimeProfilerPatcher.IsDedicatedServer) { return; } ModConfiguration configuration = LoadTimeProfilerPatcher.Configuration; if (configuration == null || !Chainloader.PluginInfos.TryGetValue("sighsorry.ConfigManager", out var value) || (Object)(object)value.Instance == (Object)null) { return; } ConstructorInfo constructor; List<object> settings; try { Type type = ((object)value.Instance).GetType(); Type type2 = type.Assembly.GetType("ConfigurationManager.ExternalSetting"); constructor = type2?.GetConstructor(new Type[7] { typeof(string), typeof(string), typeof(string), typeof(Type), typeof(object), typeof(Func<object>), typeof(Action<object>) }); Type type3 = ((type2 == null) ? null : typeof(IEnumerable<>).MakeGenericType(type2)); MethodInfo methodInfo = ((type3 == null) ? null : type.GetMethod("RegisterExternalSettings", BindingFlags.Static | BindingFlags.Public, null, new Type[4] { typeof(string), typeof(string), typeof(string), type3 }, null)); if (constructor == null || methodInfo == null || !typeof(IDisposable).IsAssignableFrom(methodInfo.ReturnType)) { LoadTimeProfilerPatcher.LogInfo("This ConfigManager build does not support external settings. Use its cfg text editor or install a build with the external settings API."); return; } settings = new List<object>(); AddSetting("General", "ProfilingEnabled", "Measure startup and world-join times and write timing reports. Restart required.", typeof(bool), true); AddSetting("General", "LocalizationCacheEnabled", "Cache supported localization work. Restart required.", typeof(bool), true); AddSetting("General", "ConfigWriteCoalescingEnabled", "Coalesce automatic config writes during startup. Restart required.", typeof(bool), true); AddSetting("General", "ConfigAutoReloadEnabled", "Automatically reload other mods' BepInEx cfg files after local file edits. Each mod determines whether changed values take effect during play and whether they synchronize to clients. Does not control LoadTimeProfiler's live logging settings. Restart required.", typeof(bool), false); AddSetting("General", "TimeoutProtectionSeconds", "Minimum supported connection timeout in seconds. Applies locally on clients, hosts and dedicated servers; not synchronized. Use 0 to disable this protection. Longer original timeouts remain. Restart required.", typeof(float), 120f); string conflictName = LogFiltering.ConflictName; string text = ((conflictName == null) ? null : ("LoadTimeProfiler's logging filter is disabled because " + conflictName + " is loaded. These checkboxes show saved LTP rules, not the other patcher's settings, and cannot be edited here. Remove the overlapping patcher and restart to use LTP filtering. Other LTP features remain active.")); if (conflictName != null) { string status = "Disabled: " + conflictName + " is loaded."; Add("Logging - Mods", "Status", text, typeof(string), status, () => status, null); } List<(string, string)> list = new List<(string, string)>(); Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); foreach (PluginInfo value4 in Chainloader.PluginInfos.Values) { if (!((Object)(object)value4.Instance == (Object)null) && !(value4.Metadata.GUID == "sighsorry.LoadTimeProfiler")) { string name = value4.Metadata.Name; list.Add((value4.Metadata.GUID, name)); dictionary.TryGetValue(name, out var value2); dictionary[name] = value2 + 1; } } list.Sort(((string Guid, string Name) left, (string Guid, string Name) right) => StringComparer.Ordinal.Compare(left.Guid, right.Guid)); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); if (conflictName != null) { hashSet.Add("Status"); } foreach (var item in list) { string guid = item.Item1; string text2 = ((dictionary[item.Item2] > 1) ? (item.Item2 + " [" + guid + "]") : item.Item2); while (!hashSet.Add(text2)) { text2 = text2 + " [" + guid + "]"; } string description = "Mod GUID: " + guid + ". " + (text ?? "Controls this mod's default BepInEx logger, including constructor and Awake logs on the next launch. Select the groups to allow; select all for every log, or clear all to silence this logger. Changes apply immediately. Unity logs and unowned custom/shared Manual loggers are always allowed by LoadTimeProfiler."); Add("Logging - Mods", text2, description, typeof(LogGroups), LogGroups.Errors | LogGroups.Warnings, () => GroupValues[(int)configuration.GetModLogGroups(guid)], (conflictName == null) ? ((Action<object>)delegate(object obj) { configuration.SetModLogGroups(guid, (LogGroups)obj); }) : null); } Array array = Array.CreateInstance(type2, settings.Count); for (int num = 0; num < settings.Count; num++) { array.SetValue(settings[num], num); } IDisposable value3 = (IDisposable)(methodInfo.Invoke(null, new object[4] { "sighsorry.LoadTimeProfiler", "LoadTimeProfiler", "1.3.2", array }) ?? throw new InvalidOperationException("ConfigManager returned no registration handle.")); Interlocked.Exchange(ref _registration, value3)?.Dispose(); if (Volatile.Read(in _stopped) != 0) { Dispose(); } void Add(string section, string key, string text3, Type type4, object defaultValue, Func<object> get, Action<object>? set) { settings.Add(constructor.Invoke(new object[7] { section, key, text3, type4, defaultValue, get, set })); } } catch (Exception ex) { Exception ex2 = ((ex is TargetInvocationException { InnerException: not null } ex3) ? ex3.InnerException : ex); LoadTimeProfilerPatcher.LogWarning("ConfigManager integration failed; cfg reload remains available: " + ex2.Message); } void AddSetting(string section, string key, string description2, Type type4, object defaultValue) { Add(section, key, description2, type4, defaultValue, () => configuration.GetSettingValue(section, key), delegate(object value4) { configuration.SetSettingValue(section, key, value4); }); } } internal static void Dispose() { Volatile.Write(ref _stopped, 1); try { Interlocked.Exchange(ref _registration, null)?.Dispose(); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("ConfigManager registration cleanup failed: " + ex.Message); } } } internal static class LogFiltering { private static class PluginOwnership { private static Func<BaseUnityPlugin, ManualLogSource>? _getLogger; internal static void Install() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Expected O, but got Unknown lock (LifecycleLock) { if (_ownershipHarmony != null || Volatile.Read(in _config) == null) { return; } Harmony val = new Harmony("sighsorry.LoadTimeProfiler.logging.ownership"); try { MethodInfo method = AccessTools.PropertyGetter(typeof(BaseUnityPlugin), "Logger") ?? throw new MissingMethodException("BaseUnityPlugin.Logger"); _getLogger = (Func<BaseUnityPlugin, ManualLogSource>)Delegate.CreateDelegate(typeof(Func<BaseUnityPlugin, ManualLogSource>), method); ConstructorInfo constructorInfo = AccessTools.Constructor(typeof(BaseUnityPlugin), Type.EmptyTypes, false) ?? throw new MissingMethodException("BaseUnityPlugin constructor"); val.Patch((MethodBase)constructorInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(PluginOwnership), "Constructed", (Type[])null) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _ownershipHarmony = val; } catch (Exception ex) { RemovePatches(val); _getLogger = null; Report("Mod logger ownership unavailable; unowned Manual logs remain allowed: " + ex.Message); } } } private static void Constructed(BaseUnityPlugin __instance) { Func<BaseUnityPlugin, ManualLogSource> getLogger = _getLogger; if (getLogger != null && Volatile.Read(in _config) != null) { BindPluginLogger(getLogger(__instance), __instance.Info.Metadata.GUID); } } } private sealed class LoggerIdentityComparer : IEqualityComparer<ManualLogSource> { public bool Equals(ManualLogSource? x, ManualLogSource? y) { return x == y; } public int GetHashCode(ManualLogSource obj) { return RuntimeHelpers.GetHashCode(obj); } } private const string Owner = "sighsorry.LoadTimeProfiler.logging"; private static readonly object LifecycleLock = new object(); private static ModConfiguration? _config; private static Harmony? _manualHarmony; private static Harmony? _ownershipHarmony; private static readonly ConcurrentDictionary<ManualLogSource, string> PluginOwners = new ConcurrentDictionary<ManualLogSource, string>(new LoggerIdentityComparer()); private static string? _conflict; internal static string? ConflictName => Volatile.Read(in _conflict); internal static void Install(ModConfiguration configuration) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown lock (LifecycleLock) { if (_manualHarmony != null) { return; } CheckForConflicts(); if (_conflict != null) { return; } Volatile.Write(ref _config, configuration); _manualHarmony = new Harmony("sighsorry.LoadTimeProfiler.logging.manual"); try { _manualHarmony.Patch((MethodBase)AccessTools.Method(typeof(ManualLogSource), "Log", new Type[2] { typeof(LogLevel), typeof(object) }, (Type[])null), new HarmonyMethod(typeof(LogFiltering), "ManualLogPrefix", (Type[])null) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch { Dispose(); throw; } } } private static bool ManualLogPrefix(ManualLogSource __instance, LogLevel level) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return Allows(__instance, level); } internal static bool Allows(ManualLogSource source, LogLevel level) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) if (source == LoadTimeProfilerPatcher.Log) { return true; } ModConfiguration modConfiguration = Volatile.Read(in _config); if (modConfiguration == null) { return true; } if (!PluginOwners.TryGetValue(source, out string value)) { return true; } return modConfiguration.Snapshot.Logging.Allows(value, level); } [MethodImpl(MethodImplOptions.NoInlining)] internal static void InstallPluginOwnership() { PluginOwnership.Install(); } internal static void BindPluginLogger(ManualLogSource source, string guid) { if (source == null) { throw new ArgumentNullException("source"); } if (string.IsNullOrEmpty(guid)) { throw new ArgumentException("A mod GUID is required.", "guid"); } PluginOwners.TryAdd(source, guid); } internal static void CheckForConflicts() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { string name = assemblies[i].GetName().Name; if (string.Equals(name, "ShutUp", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "QuietLogs", StringComparison.OrdinalIgnoreCase)) { if (Interlocked.CompareExchange(ref _conflict, name, null) == null) { Disable(); Report("Logging filter disabled because " + name + " is also loaded. Remove that standalone patcher and restart to use integrated filtering. Other LoadTimeProfiler features remain active."); } break; } } } internal static string DescribeHooks() { string text = Volatile.Read(in _conflict); if (text != null) { return "disabled (conflict: " + text + ")"; } return "Manual=" + (Volatile.Read(in _config) != null) + "; mod GUID=" + (_ownershipHarmony != null); } internal static void Report(string message) { LoadTimeProfilerPatcher.LogWarning(message); } internal static void RemovePatches(Harmony harmony) { try { harmony.UnpatchSelf(); } catch (Exception ex) { Report("Could not remove patches for " + harmony.Id + ": " + ex.Message); } } internal static void Disable() { Volatile.Write(ref _config, null); } internal static void Dispose() { lock (LifecycleLock) { Disable(); Harmony manualHarmony = _manualHarmony; Harmony ownershipHarmony = _ownershipHarmony; _manualHarmony = null; _ownershipHarmony = null; if (manualHarmony != null) { RemovePatches(manualHarmony); } if (ownershipHarmony != null) { RemovePatches(ownershipHarmony); } PluginOwners.Clear(); } } } internal static class ProfilerLog { private const int MaximumRetainedLogs = 20; private static readonly object Lock = new object(); private static StreamWriter? _writer; internal static string FilePath { get; private set; } = Path.Combine(Paths.ConfigPath, "LoadTimeProfiler", "pending.log"); internal static void Initialize() { lock (Lock) { DisposeWriter(); DateTime now = DateTime.Now; string text = Path.Combine(Paths.ConfigPath, "LoadTimeProfiler"); try { Directory.CreateDirectory(text); RetainNewestLogs(text, 19); FilePath = CreateUniqueLogPath(text, now); _writer = new StreamWriter(new FileStream(FilePath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; _writer.WriteLine("LoadTimeProfiler 1.3.2"); _writer.WriteLine($"Session: {now:yyyy-MM-dd HH:mm:ss zzz}"); _writer.WriteLine("Process: " + Paths.ProcessName); _writer.WriteLine("Mode: " + (LoadTimeProfilerPatcher.IsDedicatedServer ? "Dedicated server" : "Client")); _writer.WriteLine($"BepInEx: {typeof(BaseUnityPlugin).Assembly.GetName().Version}"); _writer.WriteLine(); } catch (Exception ex) { DisposeWriter(); LoadTimeProfilerPatcher.LogError("Could not create profiler log '" + FilePath + "': " + ex.Message); } } } private static string CreateUniqueLogPath(string directory, DateTime timestamp) { string text = timestamp.ToString("yyyy-MM-dd_HH-mm-ss"); string text2 = Path.Combine(directory, text + ".log"); int num = 2; while (File.Exists(text2)) { text2 = Path.Combine(directory, text + "_" + num + ".log"); num++; } return text2; } private static void RetainNewestLogs(string directory, int maximumExistingLogs) { FileInfo[] array = (from file in new DirectoryInfo(directory).GetFiles("*.log", SearchOption.TopDirectoryOnly) orderby file.LastWriteTimeUtc select file).ThenBy<FileInfo, string>((FileInfo file) => file.Name, StringComparer.Ordinal).ToArray(); int num = Math.Max(0, array.Length - maximumExistingLogs); for (int num2 = 0; num2 < num; num2++) { try { array[num2].Delete(); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } internal static void WriteLine(string text) { lock (Lock) { if (_writer == null) { return; } try { _writer.WriteLine(text); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Could not write profiler log: " + ex.Message); DisposeWriter(); } } } internal static void WriteWarning(string text) { WriteLine(text); if (!LoadTimeProfilerPatcher.ProfilingEnabled) { LoadTimeProfilerPatcher.LogWarning(text); } } internal static void WriteBlock(string text) { lock (Lock) { if (_writer == null) { return; } try { _writer.WriteLine(text.TrimEnd(Array.Empty<char>())); _writer.WriteLine(); } catch (Exception ex) { LoadTimeProfilerPatcher.LogWarning("Could not write profiler log: " + ex.Message); DisposeWriter(); } } } private static void DisposeWriter() { try { _writer?.Dispose(); } catch { } _writer = null; } } internal enum ProfileSession { Startup, Connection } [Flags] internal enum ProfileSessionMask { None = 0, Startup = 1, Connection = 2 } internal static class TimelineProfiler { private sealed class SessionState { private readonly List<Milestone> _milestones = new List<Milestone>(); private readonly HashSet<string> _seenMilestones = new HashSet<string>(StringComparer.Ordinal); private double? _failureDecisionMilliseconds; private string? _failureReason; private double? _logoutCandidateMilliseconds; private string? _logoutCandidateObservation; private ModSettings? _startSettings; private string _startHooks = string.Empty; internal ProfileSession Session { get; } internal string Name { get; private set; } internal bool Active { get; private set; } internal bool HasFailureDecision => _failureDecisionMilliseconds.HasValue; internal int Generation { get; private set; } private double StartMilliseconds { get; set; } internal SessionState(ProfileSession session, string name) { Session = session; Name = name; } internal void Begin(double startMilliseconds, string? name = null, ModSettings? startSettings = null, string? startHooks = null) { if (!string.IsNullOrEmpty(name)) { Name = name; } Generation++; Active = true; StartMilliseconds = startMilliseconds; _milestones.Clear(); _seenMilestones.Clear(); _failureDecisionMilliseconds = null; _failureReason = null; _logoutCandidateMilliseconds = null; _logoutCandidateObservation = null; _startSettings = startSettings ?? LoadTimeProfilerPatcher.Configuration?.Snapshot; _startHooks = startHooks ?? LogFiltering.DescribeHooks(); } internal void AddMilestoneOnce(string label, double absoluteMilliseconds) { if (_seenMilestones.Add(label)) { AddMilestone(label, absoluteMilliseconds); } } internal void AddMilestone(string label, double absoluteMilliseconds) { _seenMilestones.Add(label); _milestones.Add(new Milestone(label, Math.Max(0.0, absoluteMilliseconds - StartMilliseconds))); } internal void MarkFailureDecision(double absoluteMilliseconds, string reason) { if (!_failureDecisionMilliseconds.HasValue) { _failureDecisionMilliseconds = Math.Max(0.0, absoluteMilliseconds - StartMilliseconds); _failureReason = reason; AddMilestone("Connection failure decided: " + reason, absoluteMilliseconds); } } internal void MarkLogoutCandidate(double absoluteMilliseconds, string observation) { if (!_logoutCandidateMilliseconds.HasValue) { _logoutCandidateMilliseconds = Math.Max(0.0, absoluteMilliseconds - StartMilliseconds); _logoutCandidateObservation = observation; } } internal void ConfirmFailureDecision(double absoluteMilliseconds, string reason) { if (!_failureDecisionMilliseconds.HasValue) { _failureDecisionMilliseconds = _logoutCandidateMilliseconds ?? Math.Max(0.0, absoluteMilliseconds - StartMilliseconds); _failureReason = ((_logoutCandidateObservation == null) ? reason : (reason + " (logout first observed as " + _logoutCandidateObservation + ")")); AddMilestone("Connection failure confirmed: " + reason, absoluteMilliseconds); } } internal SessionSnapshot Complete(double absoluteMilliseconds, string result) { Active = false; double totalMilliseconds = Math.Max(0.0, absoluteMilliseconds - StartMilliseconds); return new SessionSnapshot(Session, Name, result, totalMilliseconds, Generation, _milestones.ToArray(), _failureDecisionMilliseconds, _failureReason, _startSettings, LoadTimeProfilerPatcher.Configuration?.Snapshot, _startHooks, LogFiltering.DescribeHooks()); } } private readonly struct SessionSnapshot { internal ProfileSession Session { get; } internal string Name { get; } internal string Result { get; } internal double TotalMilliseconds { get; } internal int Generation { get; } internal Milestone[] Milestones { get; } internal double? FailureDecisionMilliseconds { get; } internal string? FailureReason { get; } internal ModSettings? StartSettings { get; } internal ModSettings? EndSettings { get; } internal string StartHooks { get; } internal string EndHooks { get; } internal SessionSnapshot(ProfileSession session, string name, string result, double totalMilliseconds, int generation, Milestone[] milestones, double? failureDecisionMilliseconds, string? failureReason, ModSettings? startSettings, ModSettings? endSettings, string startHooks, string endHooks) { Session = session; Name = name; Result = result; TotalMilliseconds = totalMilliseconds; Generation = generation; Milestones = milestones; FailureDecisionMilliseconds = failureDecisionMilliseconds; FailureReason = failureReason; StartSettings = startSettings; EndSettings = endSettings; StartHooks = startHooks; EndHooks = endHooks; } } private readonly struct Milestone { internal string Label { get; } internal double ElapsedMilliseconds { get; } internal Milestone(string label, double elapsedMilliseconds) { Label = label; ElapsedMilliseconds = elapsedMilliseconds; } } private static readonly object Lock = new object(); private static readonly SessionState Startup = new SessionState(ProfileSession.Startup, "Start To Lobby"); private static readonly SessionState Connection = new SessionState(ProfileSession.Connection, "Lobby To World"); private static double _patcherStartMilliseconds; private static ModSettings? _patcherStartSettings; private static string? _patcherStartHooks; internal static void CapturePatcherStart() { lock (Lock) { if (_patcherStartMilliseconds <= 0.0) { _patcherStartMilliseconds = NowMilliseconds(); _patcherStartSettings = LoadTimeProfilerPatcher.Configuration?.Snapshot; _patcherStartHooks = LogFiltering.DescribeHooks(); } } } internal static void BeginStartup(bool dedicatedServer) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } LifecyclePhaseProfiler.ResetSession(ProfileSession.Startup); ChainloaderProfiler.ResetSession(); if (dedicatedServer) { DeepLobbyAttributionProfiler.ResetSession(); } lock (Lock) { double num = NowMilliseconds(); double num2 = ((_patcherStartMilliseconds > 0.0) ? _patcherStartMilliseconds : num); Startup.Begin(num2, dedicatedServer ? "Server Startup" : "Start To Lobby", _patcherStartSettings, _patcherStartHooks); Startup.AddMilestone("LoadTimeProfiler.Patcher.Patch", num2); Startup.AddMilestone("LoadTimeProfiler.Patcher initialized", num); } } internal static void BeginConnection(string label, bool restartActive) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } if (restartActive) { CancelConnection("superseded by " + label); } lock (Lock) { if (Connection.Active) { Connection.AddMilestoneOnce(label, NowMilliseconds()); return; } } LifecyclePhaseProfiler.ResetSession(ProfileSession.Connection); DeepLobbyAttributionProfiler.ResetSession(); lock (Lock) { double num = NowMilliseconds(); if (Connection.Active) { Connection.AddMilestoneOnce(label, num); return; } Connection.Begin(num); Connection.AddMilestone(label, num); } } internal static void MarkStartup(string label) { Mark(Startup, label); } internal static void MarkConnection(string label) { Mark(Connection, label); } internal static bool IsActive(ProfileSession session) { lock (Lock) { return GetState(session).Active; } } internal static ProfileSessionMask GetActiveSessionMask() { lock (Lock) { ProfileSessionMask profileSessionMask = ProfileSessionMask.None; if (Startup.Active) { profileSessionMask |= ProfileSessionMask.Startup; } if (Connection.Active) { profileSessionMask |= ProfileSessionMask.Connection; } return profileSessionMask; } } internal static double GetElapsedSincePatcherStart() { lock (Lock) { return (_patcherStartMilliseconds <= 0.0) ? 0.0 : Math.Max(0.0, NowMilliseconds() - _patcherStartMilliseconds); } } internal static void CompleteStartup(string label) { Finish(Startup, label, "completed"); } internal static void AbortStartup(string label) { Finish(Startup, label, "aborted"); } internal static void CompleteConnection(string label) { Finish(Connection, label, "completed", deferReport: true); } internal static void AbortConnection(string label) { MarkConnectionFailureDecision(label); Finish(Connection, label, "failed"); } internal static void CancelConnection(string label) { bool hasFailureDecision; lock (Lock) { hasFailureDecision = Connection.HasFailureDecision; } Finish(Connection, label, hasFailureDecision ? "failed" : "cancelled"); } internal static void MarkConnectionFailureDecision(string reason) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } lock (Lock) { if (Connection.Active) { Connection.MarkFailureDecision(NowMilliseconds(), reason); } } } internal static void MarkConnectionLogoutCandidate(string observation) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } lock (Lock) { if (Connection.Active) { Connection.MarkLogoutCandidate(NowMilliseconds(), observation); } } } internal static void ConfirmConnectionFailureDecision(string reason) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } lock (Lock) { if (Connection.Active) { Connection.ConfirmFailureDecision(NowMilliseconds(), reason); } } } private static void Mark(SessionState state, string label) { if (!LoadTimeProfilerPatcher.ProfilingEnabled) { return; } lock (Lock) { if (state.Active) { state.AddMilestoneOnce(label, NowMilliseconds()); } } } private static void Finish(SessionState state, string label, string result, bool deferReport = false) { SessionSnapshot snapshot; lock (Lock) { if (!state.Active) { return; } double absoluteMilliseconds = NowMilliseconds(); state.AddMilestone(label, absoluteMilliseconds); snapshot = state.Complete(absoluteMilliseconds, result); } if (!deferReport || !TryDeferReport(snapshot)) { WriteReportSafely(snapshot); } } private static bool TryDeferReport(SessionSnapshot snapshot) { try { ThreadingHelper instance = ThreadingHelper.Instance; if ((Object)(object)instance == (Object)null) { return false; } instance.StartSyncInvoke((Action)delegate { QueueDeferredReportSecondStage(snapshot); }); return true; } catch (Exception ex) { ProfilerLog.WriteLine("Could not defer the completed connection report: " + ex.Message); return false; } } private static void QueueDeferredReportSecondStage(SessionSnapshot snapshot) { try { ThreadingHelper instance = ThreadingHelper.Instance; if ((Object)(object)instance == (Object)null) { WriteReportSafely(snapshot); return; } instance.StartSyncInvoke((Action)delegate { WriteReportSafely(snapshot); }); } catch (Exception ex) { ProfilerLog.WriteLine("Could not queue the second deferred report stage: " + ex.Message); WriteReportSafely(snapshot); } } private static void WriteReportSafely(SessionSnapshot snapshot) { try { WriteReport(snapshot); } catch (Exception ex) { ProfilerLog.WriteLine("Could not assemble " + snapshot.Name + " report: " + ex); LoadTimeProfilerPatcher.LogError("Could not assemble " + snapshot.Name + " report: " + ex.Message); } } private static void WriteReport(SessionSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== " + snapshot.Name + " ==="); stringBuilder.AppendLine("Result: " + snapshot.Result); stringBuilder.Append("Total: ").AppendLine(FormatDuration(snapshot.TotalMilliseconds)); if (snapshot.Session == ProfileSession.Startup && snapshot.Name == "Start To Lobby") { stringBuilder.AppendLine("Scope: Patcher.Patch to FejdStartup.Start completion; excludes later intro playback and menu interaction readiness."); } stringBuilder.Append("Logging at start: ").AppendLine(snapshot.StartSettings?.Logging.Description ?? "unavailable"); stringBuilder.Append("Logging at end: ").AppendLine(snapshot.EndSettings?.Logging.Description ?? "unavailable"); stringBuilder.Append("Logging hooks at start: ").AppendLine(snapshot.StartHooks); stringBuilder.Append("Logging hooks at end: ").AppendLine(snapshot.EndHooks); stringBuilder.Append("Config revision: ").Append(snapshot.StartSettings?.Revision ?? 0).Append(" -> ") .Append(snapshot.EndSettings?.Revision ?? 0) .AppendLine(); if (snapshot.StartSettings?.Revision != snapshot.EndSettings?.Revision) { stringBuilder.AppendLine("Configuration changed during this measurement; startup settings remained fixed. Compare runs with matching logging policies."); } if (snapshot.Session == ProfileSession.Connection) { AppendConnectionOutcome(stringBuilder, snapshot); } bool num = IsReportDataCurrent(snapshot); AppendMilestoneIntervals(lifecycleExecutionTimes: num ? ((IReadOnlyDictionary<string, double>)LifecyclePhaseProfiler.SnapshotSingleExecutionTimes(snapshot.Session)) : ((IReadOnlyDictionary<string, double>)new Dictionary<string, double>()), builder: stringBuilder, milestones: snapshot.Milestones); if (!num) { stringBuilder.AppendLine("Detailed profiler sections unavailable because a newer session began before this deferred report was assembled; timeline totals above remain valid."); } if (num) { if (snapshot.Session == ProfileSession.Startup) { ChainloaderProfiler.AppendStartupReport(stringBuilder); } LifecyclePhaseProfiler.AppendSessionReport(stringBuilder, snapshot.Session); if (snapshot.Session == ProfileSession.Connection || (snapshot.Session == ProfileSession.Startup && LoadTimeProfilerPatcher.IsDedicatedServer)) { DeepLobbyAttributionProfiler.AppendReport(stringBuilder); } } ProfilerLog.WriteBlock(stringBuilder.ToString()); LoadTimeProfilerPatcher.LogInfo(snapshot.Name + " profile " + snapshot.Result + ": " + FormatDuration(snapshot.TotalMilliseconds) + ". See " + ProfilerLog.FilePath + "."); } private static bool IsReportDataCurrent(SessionSnapshot snapshot) { lock (Lock) { return GetState(snapshot.Session).Generation == snapshot.Generation; } } private static void AppendConnectionOutcome(StringBuilder builder, SessionSnapshot snapshot) { builder.AppendLine("Connection outcome:"); if (string.Equals(snapshot.Result, "completed", StringComparison.Ordinal)) { builder.Append(" Normal connection time: ").AppendLine(FormatDuration(snapshot.TotalMilliseconds)); } else if (string.Equals(snapshot.Result, "failed", StringComparison.Ordinal)) { double num = snapshot.FailureDecisionMilliseconds ?? snapshot.TotalMilliseconds; builder.Append(" Failure: ").AppendLine(snapshot.FailureReason ?? "unknown"); builder.Append(" Time to failure decision: ").AppendLine(FormatDuration(num)); builder.Append(" Return to lobby/error display after decision: ").AppendLine(FormatDuration(Math.Max(0.0, snapshot.TotalMilliseconds - num))); } else { builder.Append(" Cancelled attempt time: ").AppendLine(FormatDuration(snapshot.TotalMilliseconds)); } } private static void AppendMilestoneIntervals(StringBuilder builder, Milestone[] milestones, IReadOnlyDictionary<string, double> lifecycleExecutionTimes) { builder.AppendLine("Milestone intervals:"); builder.AppendLine(" Breakdown: lifecycle execution + remaining time until the next milestone."); if (milestones.Length < 2) { builder.AppendLine(" No milestone interval completed."); return; } for (int i = 0; i < milestones.Length - 1; i++) { Milestone milestone = milestones[i]; Milestone milestone2 = milestones[i + 1]; double num = Math.Max(0.0, milestone2.ElapsedMilliseconds - milestone.ElapsedMilliseconds); double num2 = Math.Truncate(num); builder.Append(" ").Append(FormatSeconds(num2)); if (lifecycleExecutionTimes.TryGetValue(milestone.Label, out var value) && value <= num) { double num3 = Math.Min(num2, Math.Truncate(Math.Max(0.0, value))); double milliseconds = num2 - num3; builder.Append(" (").Append(FormatSeconds(num3)).Append(" + ") .Append(FormatSeconds(milliseconds)) .Append(')'); } builder.Append(": ").AppendLine(milestone.Label); } } private static SessionState GetState(ProfileSession session) { if (session != ProfileSession.Startup) { return Connection; } return Startup; } private static double NowMilliseconds() { return (double)Stopwatch.GetTimestamp() * 1000.0 / (double)Stopwatch.Frequency; } internal static string FormatDuration(double milliseconds) { if (milliseconds >= 60000.0) { int num = (int)(milliseconds / 60000.0); double num2 = (milliseconds - (double)num * 60000.0) / 1000.0; return $"{num} min {num2:00.000} s"; } if (milliseconds >= 1000.0) { return $"{milliseconds / 1000.0:0.000} s"; } return $"{milliseconds:0.###} ms"; } internal static string FormatSeconds(double milliseconds) { return (Math.Truncate(Math.Max(0.0, milliseconds)) / 1000.0).ToString("0.000", CultureInfo.InvariantCulture) + " s"; } } public static class RuntimeEntrypoint { private static bool _chainloaderStarted; internal static bool ChainloaderCompleted { get; private set; } public static void BeforeChainloaderStart() { try { LoadTimeProfilerPatcher.InitializeProfiler(); LogFiltering.InstallPluginOwnership(); if (LoadTimeProfilerPatcher.AnyStartupFeatureEnabled) { LoadTimeProfilerPatcher.AttachBepInExLogger(); if (LoadTimeProfilerPatcher.RuntimeInstrumentationNeeded) { RuntimeHookInstaller.Install(); } if (LoadTimeProfilerPatcher.ProfilingEnabled || LoadTimeProfilerPatcher.StartupAccelerationEnabled) { StartupAcceleration.InstallBeforeChainloader(); } if (LoadTimeProfilerPatcher.TimeoutProtectionEnabled) { ConnectionStability.InstallBeforeChainloader(); } if (LoadTimeProfilerPatcher.StartupAccelerationEnabled) { StartupAcceleration.BeginChainloader(); } if (LoadTimeProfilerPatcher.ProfilingEnabled) { ChainloaderProfiler.BeginChainloader(); } _chainloaderStarted = true; } } catch (Exception ex) { try { StartupAcceleration.EndChainloader(); } catch (Exception ex2) { ProfilerLog.WriteLine("Startup acceleration cleanup after initialization failure also failed: " + ex2); } finally { StartupAcceleration.AbortStartupScope(); } RuntimeHookInstaller.RemovePluginConstructionHook(); ProfilerLog.WriteLine("Runtime hook initialization failed: " + ex); LoadTimeProfilerPatcher.LogError("Runtime hook initialization failed: " + ex.Message); } } public static void AfterChainloaderStart() { if (!LoadTimeProfilerPatcher.AnyStartupFeatureEnabled) { ConfigAutoReload.Start(); ConfigManagerIntegration.TryRegister(); return; } bool flag = LoadTimeProfilerPatcher.ProfilingEnabled && _chainloaderStarted && LoadTimeProfilerPatcher.IsDedicatedServer; if (LoadTimeProfilerPatcher.StartupAccelerationEnabled) { try { StartupAcceleration.EndChainloader(); } catch (Exception ex) { ProfilerLog.WriteWarning("Startup acceleration completion failed: " + ex); } try { StartupAcceleration.AfterChainloaderStart(); } catch (Exception ex2) { ProfilerLog.WriteWarning("Localization adapter reconciliation failed: " + ex2); } } try { if (_chainloaderStarted && LoadTimeProfilerPatcher.ProfilingEnabled) { ChainloaderProfiler.EndChainloader(); } } catch (Exception ex3) { ProfilerLog.WriteLine("Chainloader completion measurement failed: " + ex3); } finally { _chainloaderStarted = false; RuntimeHookInstaller.RemovePluginConstructionHook(); } ChainloaderCompleted = true; if (LoadTimeProfilerPatcher.TimeoutProtectionEnabled) { try { ConnectionStability.InstallLoadedModIntegrations(); } catch (Exception ex4) { ProfilerLog.WriteLine("Connection stability integration failed: " + ex4); LoadTimeProfilerPatcher.LogWarning("Connection stability integration failed: " + ex4.Message); } } if (flag) { try { DeepLobbyAttributionProfiler.PrepareForActiveSession(); } catch (Exception ex5) { ProfilerLog.WriteLine("Dedicated server attribution preparation failed: " + ex5); } } ConfigAutoReload.Start(); ConfigManagerIntegration.TryRegister(); } internal static void HandleChainloaderFailure(Exception exception) { if (!_chainloaderStarted) { return; } try { if (LoadTimeProfilerPatcher.ProfilingEnabled) { try { ChainloaderProfiler.EndChainloader(); } catch (Exception ex) { ProfilerLog.WriteLine("Chainloader failure measurement cleanup failed: " + ex); } TimelineProfiler.AbortStartup("BepInEx.Chainloader.Start failed: " + exception.GetType().Name); } } catch (Exception ex2) { ProfilerLog.WriteLine("Chainloader failure report cleanup failed: " + ex2); } finally { _chainloaderStarted = false; RuntimeHookInstaller.RemovePluginConstructionHook(); if (LoadTimeProfilerPatcher.StartupAccelerationEnabled) { StartupAcceleration.AbortStartupScope(); } } } } internal static class RuntimeHookInstaller { private static readonly object Lock = new object(); private static readonly Harmony Harmony = new Harmony("sighsorry.LoadTimeProfiler.runtime"); private static bool _installed; private static MethodBase? _pluginConstructionTarget; private static bool _pluginConstructionPatched; private static MethodBase? _startupCompletionTarget; internal static bool StartupCompletionHookInstalled { get; private set; } internal static bool IsStartupCompletionHookActive() { lock (Lock) { if (!StartupCompletionHookInstalled || _startupCompletionTarget == null) { return false; } try { return Harmony.GetPatchInfo(_startupCompletionTarget)?.Finalizers.Any((Patch patch) => string.Equals(patch.owner, Harmony.Id, StringComparison.Ordinal)) ?? false; } catch (Exception ex) { ProfilerLog.WriteWarning("Runtime hook warning: could not revalidate the startup completion hook: " + ex.Message); return false; } } } internal static void Install() { lock (Lock) { if (!_installed) { _installed = true; bool profilingEnabled = LoadTimeProfilerPatcher.ProfilingEnabled; bool localizationCacheEnabled = LoadTimeProfilerPatcher.LocalizationCacheEnabled; if (profilingEnabled || localizationCacheEnabled) { TryPatchPluginConstruction(profilingEnabled); PatchLifecycleTargets(!profilingEnabled); } } } } internal static void RemovePluginConstructionHook() { lock (Lock) { if (!_pluginConstructionPatched || _pluginConstructionTarget == null) { return; } try { Harmony.Unpatch(_pluginConstructionTarget, (HarmonyPatchType)0, Harmony.Id); } catch (Exception ex) { ProfilerLog.WriteWarning("Runtime hook warning: could not remove the plugin construction hook: " + ex.Message); } finally { _pluginConstructionPatched = false; _pluginConstructionTarget = null; } } } private static bool TryPatchPluginConstruction(bool profiling) { try { MethodBase methodBase = AccessTools.Method(typeof(GameObject), "AddComponent", new Type[1] { typeof(Type) }, (Type[])null); if (methodBase == null) { throw new MissingMethodException(typeof(GameObject).FullName, "AddComponent"); } if (profiling) { Harmony.Patch(methodBase, Highest(typeof(LoadTimeProfilerPluginInitializationPatch), "Prefix"), Lowest(typeof(LoadTimeProfilerPluginInitializationPatch), "Postfix"), (HarmonyMethod)null, Lowest(typeof(LoadTimeProfilerPluginInitializationPatch), "Finalizer"), (HarmonyMethod)null); } else { Harmony.Patch(methodBase, Highest(typeof(LoadTimeProfilerPluginInitializationPatch), "LocalizationDiscoveryPrefix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } _pluginConstructionTarget = methodBase; _pluginConstructionPatched = true; return true; } catch (Exception ex) { ProfilerLog.WriteWarning("Runtime hook warning: could not install the plugin-boundary hook: " + ex.Message); return false; } } private static void PatchLifecycleTargets(bool startupCompletionOnly) { HarmonyMethod val = null; HarmonyMethod val2; try { if (!startupCompletionOnly) { val = Highest(typeof(LoadTimeProfilerLifecyclePatch), "Prefix"); } val2 = Lowest(typeof(LoadTimeProfilerLifecyclePatch), "Finalizer"); } catch (Exception ex) { ProfilerLog.WriteWarning("Runtime hook warning: could not prepare lifecycle hooks: " + ex.Message); return; } foreach (MethodBase target in LifecyclePatches.GetTargets()) { if (startupCompletionOnly && !LifecyclePatches.IsStartupCompletionTarget(target)) { continue; } try { Harmony.Patch(target, val, (HarmonyMethod)null, (HarmonyMethod)null, val2, (HarmonyMethod)null); if (LifecyclePatches.IsStartupCompletionTarget(target)) { StartupCompletionHookInstalled = true; _startupCompletionTarget = target; } } catch (Exception ex2) { string text = (target.DeclaringType?.FullName ?? "<unknown>") + "." + target.Name; ProfilerLog.WriteWarning("Runtime hook warning: could not patch " + text + ": " + ex2.Message); } } } private static HarmonyMethod Highest(Type type, string methodName) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException(type.FullName, methodName); } return new HarmonyMethod(methodInfo) { priority = int.MaxValue }; } private static HarmonyMethod Lowest(Type type, string methodName) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException(type.FullName, methodName); } return new HarmonyMethod(methodInfo) { priority = int.MinValue }; } } internal static class ChainloaderProfiler { internal sealed class PluginInitializationState { internal Type ComponentType { get; } internal PluginIdentity Identity { get; } internal long StartTimestamp { get; } internal bool Completed { get; set; } internal PluginInitializationState(Type componentType, PluginIdentity identity, long startTimestamp) { ComponentType = componentType; Identity = identity; StartTimestamp = startTimestamp; } } internal readonly struct PluginIdentity { internal string Key { get; } internal string DisplayName { get; } internal PluginIdentity(string key, string displayName) { Key = key; DisplayName = displayName; } } private sealed class MutableTiming { private PluginIdentity Identity { get; } private int Count { get; set; } private double ElapsedMilliseconds { get; set; } internal MutableTiming(PluginIdentity identity) { Identity = identity; } internal void Add(double elapsedMilliseconds) { Count++; ElapsedMilliseconds += elapsedMilliseconds; } internal TimingSnapshot Snapshot() { return new TimingSnapshot(Identity, Count, ElapsedMilliseconds); } } private readonly struct TimingSnapshot { internal PluginIdentity Identity { get; } internal int Count { get; } internal double ElapsedMilliseconds { get; } internal TimingSnapshot(PluginIdentity identity, int count, double elapsedMilliseconds) { Identity = identity; Count = count; ElapsedMilliseconds = elapsedMilliseconds; } } private static readonly object Lock = new object(); private static readonly Harmony PluginStartHarmony = new Harmony("sighsorry.LoadTimeProfiler.plugin-start"); private static readonly Dictionary<string, MutableTiming> Initializations = new Dictionary<string, MutableTiming>(StringComparer.Ordinal); private static readonly Dictionary<string, MutableTiming> Starts = new Dictionary<string, MutableTiming>(StringComparer.Ordinal); private static readonly Dictionary<MethodBase, PluginIdentity> StartMethods = new Dictionary<MethodBase, PluginIdentity>(); private static bool _chainloaderActive; private static long _chainloaderStarted; private static double _preChainloaderMilliseconds; private static double _chainloaderMilliseconds; internal static void ResetSession() { lock (Lock) { Initializations.Clear(); Starts.Clear(); _chainloaderActive = false; _chainloaderStarted = 0L; _preChainloaderMilliseconds = 0.0; _chainloaderMilliseconds = 0.0; } } internal static void BeginChainloader() { if (LoadTimeProfilerPatcher.ProfilingEnabled) { lock (Lock) { _chainloaderActive = true; _chainloaderStarted = Stopwatch.GetTimestamp(); _preChainloaderMilliseconds = TimelineProfiler.GetElapsedSincePatcherStart(); } TimelineProfiler.MarkStartup("BepInEx.Chainloader.Start"); } } internal static void EndChainloader() { long timestamp = Stopwatch.GetTimestamp(); lock (Lock) { if (_chainloaderStarted > 0) { _chainloaderMilliseconds = TicksToMilliseconds(timestamp - _chainloaderStarted); _chainloaderStarted = 0L; } _chainloaderActive = false; } TimelineProfiler.MarkStartup("BepInEx.Chainloader.Start complete"); } internal static PluginInitializationState? BeginPlugin(GameObject gameObject, Type componentType) { if (!LoadTimeProfilerPatcher.ProfilingEnabled || !typeof(BaseUnityPlugin).IsAssignableFrom(componentType)) { return null; } lock (Lock) { if (!_chainloaderActive || gameObject != Chainloader.ManagerObject) { return null; } } return new PluginInitializationState(componentType, ResolveIdentity(componentType), Stopwatch.GetTimes