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 ModDoctor v1.0.0
ModDoctor.dll
Decompiled a day agousing System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mono.Cecil; using Mono.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.LowLevel; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ModDoctor")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("ModDoctor")] [assembly: AssemblyTitle("ModDoctor")] [assembly: AssemblyVersion("1.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 ModDoctor { internal static class Attribution { private sealed class Agg { public Problem Problem; public float WindowStart; public int WindowCount; public bool LoopReported; } private sealed class Result { public ModInfo Mod; public string Frame; public bool AwakeOfPlugin; public List<string> PatchMods = new List<string>(); public string PatchedMethod; public List<string> Frames = new List<string>(); } private static readonly Dictionary<string, Agg> aggs = new Dictionary<string, Agg>(); public static string FirstLine(string s) { if (string.IsNullOrEmpty(s)) { return ""; } int num = s.IndexOfAny(new char[2] { '\r', '\n' }); string text = ((num >= 0) ? s.Substring(0, num) : s); if (text.Length <= 300) { return text; } return text.Substring(0, 300) + "…"; } public static void Process(RawEvent ev) { string text = ev.Stack; if (string.IsNullOrEmpty(text) && ev.Message.Contains("\n")) { text = ev.Message.Substring(ev.Message.IndexOf('\n') + 1); } string text2 = FirstLine(ev.Message); if (ev.Source == "BepInEx" && text2.StartsWith("Could not load [")) { return; } Result result = Analyze(text); string text3; string text4; if (result.Mod != null) { text3 = result.Mod.Name; text4 = "code du mod : " + result.Frame; } else if (result.PatchMods.Count > 0) { text3 = string.Join(" / ", result.PatchMods); text4 = "dans " + result.PatchedMethod + " du jeu, patché par " + text3; } else if (!ev.FromUnity && ev.Source != "BepInEx" && ev.Source != "Unity Log") { text3 = ModIndex.OfName(ev.Source)?.Name ?? ev.Source; text4 = "erreur signalée par le mod dans le journal BepInEx"; } else if (ev.Source == "BepInEx") { text3 = StartupCheck.BracketName(text2) ?? "BepInEx"; text4 = "message du chargeur de mods BepInEx"; } else { text3 = "inconnu"; text4 = "aucun mod trouvé dans la pile d'appels (erreur du jeu, ou mod non identifié)"; } Severity severity = Severity.Warning; string text5; string title; if (result.AwakeOfPlugin) { text5 = "Plantage au chargement"; title = "le mod a planté pendant son démarrage (Awake) : " + text2; severity = Severity.Severe; } else if (ev.Source == "BepInEx") { text5 = "Chargement"; title = text2; severity = Severity.Severe; } else if (ev.Fatal) { text5 = "Erreur fatale"; title = text2; severity = Severity.Severe; } else if (ev.FromUnity) { text5 = "Exception"; title = text2; } else { text5 = "Erreur"; title = text2; } string text6 = text5 + "|" + text3 + "|" + text2; if (!aggs.TryGetValue(text6, out var value)) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Attribution : ").Append(text4); if (result.PatchMods.Count > 0 && result.Mod != null) { stringBuilder.Append("\nPassait par " + result.PatchedMethod + " du jeu, patché par " + string.Join(", ", result.PatchMods)); } foreach (string item in result.Frames.Take(8)) { stringBuilder.Append("\n ").Append(item); } value = new Agg { Problem = Report.Add(new Problem { Key = text6, Kind = text5, Mod = text3, Title = title, Detail = stringBuilder.ToString(), Severity = severity }), WindowStart = Time.realtimeSinceStartup }; aggs[text6] = value; } else { Report.Add(value.Problem); } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup - value.WindowStart > 1f) { value.WindowStart = realtimeSinceStartup; value.WindowCount = 0; } if (++value.WindowCount >= Settings.LoopErrorsPerSecond.Value && !value.LoopReported) { value.LoopReported = true; Report.Add(new Problem { Key = "boucle|" + text6, Kind = "Erreur en boucle", Mod = text3, Severity = Severity.Severe, Title = $"{value.WindowCount}+ fois par seconde : {text2}", Detail = "Une erreur répétée à chaque image ralentit le jeu et empêche souvent la suite (chargement, atterrissage, menus).\n" + text4 }); } } private static Result Analyze(string stack) { Result result = new Result(); if (string.IsNullOrEmpty(stack)) { return result; } string[] array = stack.Split('\n'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0 || text.StartsWith("Stack trace", StringComparison.OrdinalIgnoreCase)) { continue; } if (text.StartsWith("at ")) { text = text.Substring(3); } if (text.StartsWith("(wrapper")) { int num = text.IndexOf(')'); text = ((num > 0) ? text.Substring(num + 1).Trim() : text); } if (result.Frames.Count < 12) { result.Frames.Add((text.Length > 200) ? (text.Substring(0, 200) + "…") : text); } int num2 = text.IndexOf("DMD<", StringComparison.Ordinal); if (num2 >= 0) { int num3 = text.IndexOf(">(", num2, StringComparison.Ordinal); if (num3 < 0) { num3 = text.IndexOf('>', num2); } string text2 = ((num3 > num2) ? text.Substring(num2 + 4, num3 - num2 - 4) : ""); if (!text2.Contains("::")) { text2 = text.Substring(0, Math.Max(0, num2 - 1)) + "::" + text2; } string fullName = text2.Substring(0, text2.IndexOf("::", StringComparison.Ordinal)); ModInfo modInfo = ModIndex.OfTypeName(fullName); if (modInfo != null) { if (result.Mod == null) { result.Mod = modInfo; result.Frame = text2.Replace("::", "."); if (text2.EndsWith("::Awake") && ModIndex.IsPluginType(fullName)) { result.AwakeOfPlugin = true; } } continue; } List<string> list = ModIndex.PatchOwners(text2); if (list == null || result.PatchedMethod != null) { continue; } result.PatchedMethod = text2.Replace("::", "."); foreach (string item in list) { if (!result.PatchMods.Contains(item)) { result.PatchMods.Add(item); } } continue; } int num4 = text.IndexOf(" (", StringComparison.Ordinal); if (num4 < 0) { num4 = text.IndexOf('('); } string text3 = ((num4 > 0) ? text.Substring(0, num4).Trim() : text); int num5 = text3.LastIndexOf('.'); if (num5 <= 0) { continue; } string fullName2 = text3.Substring(0, num5); string text4 = text3.Substring(num5 + 1); ModInfo modInfo2 = ModIndex.OfTypeName(fullName2); if (modInfo2 != null && result.Mod == null) { result.Mod = modInfo2; result.Frame = text3; if (text4 == "Awake" && ModIndex.IsPluginType(fullName2)) { result.AwakeOfPlugin = true; } } } return result; } } internal class AutoTestDriver : MonoBehaviour { private readonly List<string> errors = new List<string>(); private string logFile; private string shotDir; private readonly DateTime started = DateTime.Now; private void Log(string s) { Plugin.Log.LogInfo((object)("[AutoTest] " + s)); try { File.AppendAllText(logFile, s + Environment.NewLine); } catch { } } private void Check(bool ok, string what) { Log((ok ? "OK : " : "ÉCHEC : ") + what); if (!ok) { errors.Add(what); } } private static Problem Find(Func<Problem, bool> pred) { return Report.Snapshot().FirstOrDefault(pred); } private IEnumerator Start() { shotDir = Path.Combine(Report.Dir, "autotest"); Directory.CreateDirectory(shotDir); string[] files = Directory.GetFiles(shotDir); for (int i = 0; i < files.Length; i++) { File.Delete(files[i]); } logFile = Path.Combine(shotDir, "log.txt"); Log("Démarrage du test automatique."); yield return WaitFor(() => Runner.StartupDone, 120f); Check(Runner.StartupDone, "bilan de démarrage terminé"); yield return WaitFor(() => (Object)(object)Object.FindObjectOfType<MenuManager>() != (Object)null, 120f); Log("Menu principal atteint ; attente des pannes de BuggyMod."); yield return WaitFor(() => Watchdog.Resumes > 0, 150f); yield return (object)new WaitForSecondsRealtime(4f); Check(Find((Problem p) => p.Kind == "Non chargé" && p.Mod == "BuggyMissing" && p.Title.Contains("test.does.not.exist")) != null, "plugin non chargé : dépendance absente test.does.not.exist"); Check(Find((Problem p) => p.Kind == "Plantage au chargement" && p.Mod == "BuggyAwake") != null, "plantage au démarrage (Awake) de BuggyAwake"); Problem problem = Find((Problem p) => p.Kind == "Conflit possible" && p.Title.Contains("MenuManager.Update")); Check(problem != null && problem.Mod.Contains("BuggyMod") && problem.Mod.Contains("BuggySecond"), "conflit Harmony sur MenuManager.Update (" + problem?.Mod + ")"); Problem problem2 = Find((Problem p) => p.Kind == "Exception" && p.Title.Contains("Update a planté")); Check(problem2 != null && problem2.Mod == "BuggyMod" && problem2.Detail.Contains("BuggyBehaviour.Update"), $"exception d'Update attribuée à BuggyMod ({problem2?.Mod}, ×{problem2?.Count})"); Problem problem3 = Find((Problem p) => p.Kind == "Exception" && p.Title.Contains("patch a planté")); Check(problem3 != null && problem3.Mod == "BuggyMod" && problem3.Detail.Contains("MenuManager.PlayConfirmSFX"), "exception dans un patch Harmony attribuée à BuggyMod, via MenuManager.PlayConfirmSFX (" + problem3?.Mod + ")"); Problem problem4 = Find((Problem p) => p.Kind == "Erreur" && p.Title.Contains("erreur signalée")); Check(problem4 != null && problem4.Mod == "BuggyMod", "erreur du journal BepInEx attribuée à BuggyMod (" + problem4?.Mod + ")"); Problem problem5 = Find((Problem p) => p.Kind == "Erreur en boucle"); Check(problem5 != null && problem5.Mod == "BuggyMod", "erreur en boucle détectée (" + problem5?.Mod + " : " + problem5?.Title + ")"); Check(Instrumenter.Count > 0, $"{Instrumenter.Count} méthodes de mods suivies (échecs {Instrumenter.Failed})"); Problem problem6 = Find((Problem p) => p.Kind == "Blocage"); Check(problem6 != null && problem6.Mod.Contains("BuggyMod") && problem6.Mod.Contains("FreezeRoutine"), "blocage attribué à la coroutine : " + problem6?.Mod); Check(problem6?.Title.Contains("reparti") ?? false, "reprise après blocage notée (" + problem6?.Title + ")"); List<string> list = (from f in Directory.GetFiles(Report.Dir, "blocage-*.txt") where File.GetLastWriteTime(f) >= started select f).ToList(); Check(list.Count > 0 && File.ReadAllText(list[0]).Contains("FreezeRoutine"), $"fichier de blocage écrit ({list.Count})"); Check(Report.PreviousUnclean && (Report.PreviousSuspect ?? "").Contains("HangRoutine"), "session précédente bloquée détectée, suspect : " + Report.PreviousSuspect); yield return Capture("01_alerte"); yield return PressKey(Settings.OverlayKey.Value); yield return (object)new WaitForSecondsRealtime(0.5f); yield return Capture("02_liste"); yield return PressKey(Settings.OverlayKey.Value); yield return PressKey(Settings.HideKey.Value); Check(!Settings.Overlay.Value, "touche masquer : ModDoctor masqué"); yield return (object)new WaitForSecondsRealtime(3.5f); yield return Capture("03_masque"); yield return PressKey(Settings.HideKey.Value); Check(Settings.Overlay.Value, "touche masquer (2e appui) : ModDoctor réaffiché"); Log("===== RÉSUMÉ ====="); Log((errors.Count == 0) ? "Aucune erreur." : string.Format("{0} échec(s) : {1}", errors.Count, string.Join(" | ", errors))); Application.Quit(); } private IEnumerator Capture(string name) { yield return (object)new WaitForEndOfFrame(); ScreenCapture.CaptureScreenshot(Path.Combine(shotDir, name + ".png")); Log("Capture : " + name); yield return (object)new WaitForSecondsRealtime(0.5f); } private static IEnumerator PressKey(Key key) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) Keyboard kb = Keyboard.current; InputSystem.QueueStateEvent<KeyboardState>((InputDevice)(object)kb, new KeyboardState((Key[])(object)new Key[1] { (Key)(int)key }), -1.0); yield return (object)new WaitForSecondsRealtime(0.15f); InputSystem.QueueStateEvent<KeyboardState>((InputDevice)(object)kb, default(KeyboardState), -1.0); yield return (object)new WaitForSecondsRealtime(0.1f); } private static IEnumerator WaitFor(Func<bool> cond, float timeout) { float t = 0f; while (t < timeout) { bool flag = false; try { flag = cond(); } catch { } if (flag) { break; } t += 0.25f; yield return (object)new WaitForSecondsRealtime(0.25f); } } } public static class Breadcrumbs { public const int MaxDepth = 128; public const int RecentSize = 64; internal static int MainThreadId; internal static readonly MethodBase[] Stack = new MethodBase[128]; internal static volatile int Depth; internal static readonly MethodBase[] Recent = new MethodBase[64]; internal static volatile int RecentCount; public static void Enter(MethodBase __originalMethod) { if (Thread.CurrentThread.ManagedThreadId == MainThreadId) { int depth = Depth; if (depth < 128) { Stack[depth] = __originalMethod; } Depth = depth + 1; int recentCount = RecentCount; Recent[recentCount & 0x3F] = __originalMethod; RecentCount = recentCount + 1; } } public static void Exit(MethodBase __originalMethod) { if (Thread.CurrentThread.ManagedThreadId == MainThreadId) { int depth = Depth; int num = depth - 1; while (num >= 0 && num < 128 && Stack[num] != __originalMethod) { num--; } if (num >= 0) { Depth = num; } else if (depth > 0) { Depth = depth - 1; } } } } internal static class Instrumenter { private const int PerFrame = 25; private static readonly HashSet<string> UnityMessages = new HashSet<string> { "Awake", "Start", "Update", "LateUpdate", "FixedUpdate", "OnGUI", "OnEnable", "OnDisable", "OnDestroy" }; private static Harmony harmony; private static HarmonyMethod enter; private static HarmonyMethod exit; private static readonly HashSet<MethodBase> done = new HashSet<MethodBase>(); private static bool typesScanned; private static bool running; public static int Failed; public static int Count => done.Count; public static IEnumerator Run() { if (!Settings.Instrument.Value || running) { yield break; } running = true; if (harmony == null) { harmony = new Harmony("emeric.moddoctor.suivi"); enter = new HarmonyMethod(typeof(Breadcrumbs).GetMethod("Enter")) { priority = 800 }; exit = new HarmonyMethod(typeof(Breadcrumbs).GetMethod("Exit")) { priority = 0 }; } List<MethodBase> list; try { list = Collect(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Suivi des mods : collecte impossible : " + ex)); running = false; yield break; } int value = Settings.MaxInstrumented.Value; if (done.Count + list.Count > value) { list = list.Take(Math.Max(0, value - done.Count)).ToList(); } if (list.Count == 0) { running = false; yield break; } Stopwatch sw = Stopwatch.StartNew(); int ok = 0; int n = 0; foreach (MethodBase item in list) { if (done.Add(item)) { try { harmony.Patch(item, enter, (HarmonyMethod)null, (HarmonyMethod)null, exit, (HarmonyMethod)null); ok++; } catch (Exception ex2) { Failed++; if (Failed <= 5) { Plugin.Log.LogDebug((object)("Suivi impossible pour " + ModIndex.Describe(item) + " : " + ex2.Message)); } } } int num = n + 1; n = num; if (num % 25 == 0) { yield return null; } } Plugin.Log.LogInfo((object)$"Suivi des mods : {ok} méthodes ajoutées en {sw.Elapsed.TotalSeconds:0.0} s (total {done.Count}, échecs {Failed})."); running = false; } private static List<MethodBase> Collect() { HashSet<string> hashSet = new HashSet<string>(from s in Settings.ExcludedMods.Value.Split(',') select s.Trim() into s where s.Length > 0 select s, StringComparer.OrdinalIgnoreCase); Assembly assembly = typeof(Instrumenter).Assembly; List<MethodBase> list = new List<MethodBase>(); HashSet<MethodBase> seen = new HashSet<MethodBase>(done); if (!typesScanned) { typesScanned = true; HashSet<Assembly> hashSet2 = new HashSet<Assembly>(); foreach (ModInfo mod in ModIndex.Mods) { if (mod.Assembly == null || mod.Assembly == assembly || hashSet.Contains(mod.Guid) || !hashSet2.Add(mod.Assembly)) { continue; } Type[] array = ModIndex.SafeTypes(mod.Assembly); foreach (Type type in array) { if (type.ContainsGenericParameters) { continue; } bool flag = typeof(MonoBehaviour).IsAssignableFrom(type); bool flag2 = type.Name.StartsWith("<") && (typeof(IEnumerator).IsAssignableFrom(type) || typeof(IAsyncStateMachine).IsAssignableFrom(type)); MethodInfo[] methods; try { methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); } catch { continue; } MethodInfo[] array2 = methods; foreach (MethodInfo methodInfo in array2) { if (flag && !methodInfo.IsStatic && UnityMessages.Contains(methodInfo.Name) && methodInfo.GetParameters().Length == 0) { Add(methodInfo); } else if (flag2 && methodInfo.Name == "MoveNext") { Add(methodInfo); } else if (IsMonoModHook(methodInfo)) { Add(methodInfo); } } } } } foreach (MethodBase item in Harmony.GetAllPatchedMethods().ToList()) { Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(item); } catch { continue; } if (patchInfo == null) { continue; } foreach (Patch item2 in patchInfo.Prefixes.Concat(patchInfo.Postfixes).Concat(patchInfo.Finalizers)) { if (item2.owner != null && item2.owner.StartsWith("emeric.moddoctor")) { continue; } MethodInfo patchMethod = item2.PatchMethod; if (!(patchMethod == null) && !(patchMethod.DeclaringType?.Assembly == assembly)) { ModInfo modInfo = ModIndex.OfGuid(item2.owner) ?? ModIndex.OfAssembly(patchMethod.DeclaringType?.Assembly); if (modInfo == null || !hashSet.Contains(modInfo.Guid)) { Add(patchMethod); } } } if (patchInfo.Transpilers.Any((Patch p) => p.owner == null || !p.owner.StartsWith("emeric.moddoctor")) || patchInfo.ILManipulators.Count > 0) { Add(item); } } return list; void Add(MethodBase m) { if (m != null && seen.Add(m) && Patchable(m)) { list.Add(m); } } } private static bool Patchable(MethodBase m) { try { if (m.IsAbstract || m.ContainsGenericParameters || m.IsConstructor) { return false; } if (m.Name.StartsWith("__") || m.DeclaringType == null || m.DeclaringType.ContainsGenericParameters) { return false; } if ((m.MethodImplementationFlags & (MethodImplAttributes)4099) != MethodImplAttributes.IL) { return false; } if (m is MethodInfo methodInfo && (methodInfo.ReturnType.IsByRef || methodInfo.ReturnType.IsByRefLike)) { return false; } ParameterInfo[] parameters = m.GetParameters(); foreach (ParameterInfo parameterInfo in parameters) { if (parameterInfo.ParameterType.IsPointer || parameterInfo.ParameterType.IsByRefLike || (parameterInfo.ParameterType.IsByRef && parameterInfo.ParameterType.GetElementType().IsByRefLike)) { return false; } } return m.GetMethodBody() != null; } catch { return false; } } private static bool IsMonoModHook(MethodInfo m) { try { ParameterInfo[] parameters = m.GetParameters(); if (parameters.Length == 0) { return false; } Type parameterType = parameters[0].ParameterType; return parameterType.Name.StartsWith("orig_") && (parameterType.FullName?.StartsWith("On.") ?? false); } catch { return false; } } } internal sealed class RawEvent { public string Source; public string Message; public string Stack; public bool FromUnity; public bool Fatal; } internal static class LogWatch { private sealed class Listener : ILogListener, IDisposable { public void LogEvent(object sender, LogEventArgs e) { //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Invalid comparison between Unknown and I4 try { ILogSource source = e.Source; string text = ((source != null) ? source.SourceName : null); if (text == "BepInEx" && e.Data is string text2) { if (text2.StartsWith("Loading [")) { CurrentLoading = text2.Substring(9).TrimEnd(']'); LoadingSince = Clock.ElapsedMilliseconds; } else if (text2.StartsWith("Chainloader startup complete")) { CurrentLoading = null; ChainloaderDone = true; } } if ((e.Level & 3) != 0 && !(text == "ModDoctor") && !(text == "Unity Log")) { Enqueue(new RawEvent { Source = text, Message = (e.Data?.ToString() ?? ""), Fatal = ((e.Level & 1) > 0) }); } } catch { } } public void Dispose() { } } [CompilerGenerated] private static class <>O { public static LogCallback <0>__OnUnityLog; } private static readonly ConcurrentQueue<RawEvent> queue = new ConcurrentQueue<RawEvent>(); private static readonly object recentLock = new object(); private static readonly string[] recent = new string[12]; private static int recentCount; public static volatile string CurrentLoading; public static long LoadingSince; public static volatile bool ChainloaderDone; public static readonly Stopwatch Clock = Stopwatch.StartNew(); private static readonly Regex Header = new Regex("^\\[(Fatal|Error|Warning|Message|Info|Debug)\\s*:\\s*(.*?)\\]\\s?(.*)$"); public static void Install() { //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_002a: Expected O, but got Unknown Logger.Listeners.Add((ILogListener)(object)new Listener()); object obj = <>O.<0>__OnUnityLog; if (obj == null) { LogCallback val = OnUnityLog; <>O.<0>__OnUnityLog = val; obj = (object)val; } Application.logMessageReceivedThreaded += (LogCallback)obj; } private static void OnUnityLog(string message, string stack, LogType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Invalid comparison between Unknown and I4 //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 if (((int)type == 0 || (int)type == 4 || (int)type == 1) && (message == null || !message.Contains("ModDoctor"))) { Enqueue(new RawEvent { Source = "Unity", Message = (message ?? ""), Stack = stack, FromUnity = true }); } } private static void Enqueue(RawEvent ev) { queue.Enqueue(ev); lock (recentLock) { recent[recentCount++ % recent.Length] = "[" + ev.Source + "] " + Attribution.FirstLine(ev.Message); } } public static void Drain() { int num = 0; RawEvent result; while (num++ < 500 && queue.TryDequeue(out result)) { try { Attribution.Process(result); } catch (Exception ex) { Plugin.Log.LogDebug((object)("Attribution impossible : " + ex.Message)); } } } public static List<string> RecentErrors() { List<string> list = new List<string>(); lock (recentLock) { int num = Math.Max(0, recentCount - recent.Length); for (int num2 = recentCount - 1; num2 >= num; num2--) { list.Add(recent[num2 % recent.Length]); } return list; } } public static void ReadEarlyLog() { string path = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); if (!File.Exists(path)) { return; } string[] array; try { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8); array = streamReader.ReadToEnd().Split('\n'); } catch (Exception ex) { Plugin.Log.LogDebug((object)("LogOutput.log illisible : " + ex.Message)); return; } RawEvent cur = null; StringBuilder stack = new StringBuilder(); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].TrimEnd('\r'); Match match = Header.Match(text); if (match.Success) { Flush(); string value = match.Groups[1].Value; string text2 = match.Groups[2].Value.Trim(); string value2 = match.Groups[3].Value; if (text2 == "BepInEx" && value2.StartsWith("Loading [ModDoctor ")) { break; } if (value == "Error" || value == "Fatal") { cur = new RawEvent { Source = text2, Message = value2, FromUnity = (text2 == "Unity Log"), Fatal = (value == "Fatal") }; } } else if (cur != null) { stack.AppendLine(text); } } Flush(); void Flush() { if (cur != null) { cur.Stack = stack.ToString(); if (cur.Source != "Unity Log" || cur.Stack.Length > 0 || cur.Message.Length > 0) { Enqueue(cur); } cur = null; stack.Clear(); } } } } internal sealed class ModInfo { public string Guid; public string Name; public string Version; public string Location; public Assembly Assembly; public Type PluginType; public override string ToString() { return Name + " " + Version; } } internal sealed class PatchConflict { public string Method; public List<string> Mods; public List<string> SkipPrefixMods; public List<string> TranspilerMods; public bool HighRisk; } internal static class ModIndex { public static volatile bool Ready; public static List<ModInfo> Mods = new List<ModInfo>(); private static Dictionary<Assembly, ModInfo> byAssembly = new Dictionary<Assembly, ModInfo>(); private static Dictionary<string, ModInfo> byGuid = new Dictionary<string, ModInfo>(StringComparer.OrdinalIgnoreCase); private static Dictionary<string, ModInfo> byName = new Dictionary<string, ModInfo>(StringComparer.OrdinalIgnoreCase); private static Dictionary<string, ModInfo> byType = new Dictionary<string, ModInfo>(); private static HashSet<Type> pluginTypes = new HashSet<Type>(); private static Dictionary<string, List<string>> patchedBy = new Dictionary<string, List<string>>(); public static List<PatchConflict> Conflicts = new List<PatchConflict>(); public static void Build() { List<ModInfo> list = new List<ModInfo>(); Dictionary<string, Assembly> dictionary = new Dictionary<string, Assembly>(StringComparer.OrdinalIgnoreCase); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!assembly.IsDynamic) { string location; try { location = assembly.Location; } catch { continue; } if (!string.IsNullOrEmpty(location) && !dictionary.ContainsKey(location)) { dictionary[location] = assembly; } } } Dictionary<Assembly, ModInfo> dictionary2 = new Dictionary<Assembly, ModInfo>(); Dictionary<string, ModInfo> dictionary3 = new Dictionary<string, ModInfo>(StringComparer.OrdinalIgnoreCase); Dictionary<string, ModInfo> dictionary4 = new Dictionary<string, ModInfo>(StringComparer.OrdinalIgnoreCase); HashSet<Type> hashSet = new HashSet<Type>(); foreach (PluginInfo value2 in Chainloader.PluginInfos.Values) { ModInfo m = new ModInfo { Guid = value2.Metadata.GUID, Name = value2.Metadata.Name, Version = value2.Metadata.Version?.ToString(), Location = value2.Location }; if ((Object)(object)value2.Instance != (Object)null) { m.PluginType = ((object)value2.Instance).GetType(); } Assembly value; if (m.PluginType != null) { m.Assembly = m.PluginType.Assembly; } else if (value2.Location != null && dictionary.TryGetValue(value2.Location, out value)) { m.Assembly = value; } if (m.PluginType == null && m.Assembly != null) { m.PluginType = SafeTypes(m.Assembly).FirstOrDefault((Type t) => t.GetCustomAttributes(typeof(BepInPlugin), inherit: false).Cast<BepInPlugin>().Any((BepInPlugin p) => p.GUID == m.Guid)); } list.Add(m); dictionary3[m.Guid] = m; if (!dictionary4.ContainsKey(m.Name)) { dictionary4[m.Name] = m; } if (m.PluginType != null) { hashSet.Add(m.PluginType); } } foreach (IGrouping<Assembly, ModInfo> item in from x in list where x.Assembly != null group x by x.Assembly) { string asmName = item.Key.GetName().Name; dictionary2[item.Key] = item.FirstOrDefault((ModInfo x) => string.Equals(x.Name, asmName, StringComparison.OrdinalIgnoreCase)) ?? item.FirstOrDefault((ModInfo x) => x.Guid.EndsWith(asmName, StringComparison.OrdinalIgnoreCase)) ?? item.First(); } Dictionary<string, ModInfo> dictionary5 = new Dictionary<string, ModInfo>(); foreach (KeyValuePair<Assembly, ModInfo> item2 in dictionary2) { Type[] array = SafeTypes(item2.Key); foreach (Type type in array) { if (type.FullName != null) { dictionary5[type.FullName] = item2.Value; } } } foreach (ModInfo item3 in list) { if (item3.PluginType?.FullName != null) { dictionary5[item3.PluginType.FullName] = item3; } } Mods = list; byAssembly = dictionary2; byGuid = dictionary3; byName = dictionary4; byType = dictionary5; pluginTypes = hashSet; RefreshPatches(); Ready = true; } public static void RefreshPatches() { Dictionary<string, List<string>> dictionary = new Dictionary<string, List<string>>(); List<PatchConflict> list = new List<PatchConflict>(); foreach (MethodBase item2 in Harmony.GetAllPatchedMethods().ToList()) { Patches patchInfo; try { patchInfo = Harmony.GetPatchInfo(item2); } catch { continue; } if (patchInfo == null) { continue; } List<string> mods = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); Collect(patchInfo.Prefixes, list2, onlySkip: true); Collect(patchInfo.Postfixes, null, onlySkip: false); Collect(patchInfo.Finalizers, null, onlySkip: false); Collect(patchInfo.Transpilers, list3, onlySkip: false); Collect(patchInfo.ILManipulators, list3, onlySkip: false); if (mods.Count != 0) { string text = Key(item2); dictionary[text] = mods; if (mods.Count >= 2) { bool highRisk = list2.Count >= 1 || list3.Count >= 2; list.Add(new PatchConflict { Method = text, Mods = mods, SkipPrefixMods = list2, TranspilerMods = list3, HighRisk = highRisk }); } } void Collect(IEnumerable<Patch> enumerable, List<string> extra, bool onlySkip) { foreach (Patch item3 in enumerable) { if (item3.owner == null || !item3.owner.StartsWith("emeric.moddoctor")) { string item = OfOwner(item3.owner, item3.PatchMethod); if (!mods.Contains(item)) { mods.Add(item); } if (extra != null && (!onlySkip || item3.PatchMethod?.ReturnType == typeof(bool)) && !extra.Contains(item)) { extra.Add(item); } } } } } patchedBy = dictionary; Conflicts = list; } public static string Key(MethodBase m) { return m.DeclaringType?.FullName + "::" + m.Name; } public static ModInfo OfAssembly(Assembly a) { if (!(a != null) || !byAssembly.TryGetValue(a, out var value)) { return null; } return value; } public static ModInfo OfGuid(string guid) { if (guid == null || !byGuid.TryGetValue(guid, out var value)) { return null; } return value; } public static ModInfo OfName(string name) { if (name == null || !byName.TryGetValue(name.Trim(), out var value)) { return null; } return value; } public static bool IsPluginType(string fullName) { if (byType.TryGetValue(fullName, out var value)) { return value.PluginType?.FullName == fullName; } return false; } public static ModInfo OfTypeName(string fullName) { if (string.IsNullOrEmpty(fullName)) { return null; } if (byType.TryGetValue(fullName, out var value)) { return value; } string text = fullName.Replace('/', '+'); int num = text.IndexOf('['); if (num > 0) { text = text.Substring(0, num); } if (byType.TryGetValue(text, out value)) { return value; } int num2 = text.LastIndexOf('.'); if (num2 > 0 && byType.TryGetValue(text.Substring(0, num2) + "+" + text.Substring(num2 + 1), out value)) { return value; } return null; } public static List<string> PatchOwners(string key) { if (!patchedBy.TryGetValue(key, out var value)) { return null; } return value; } public static string OfOwner(string owner, MethodInfo patchMethod) { return (OfGuid(owner) ?? OfAssembly(patchMethod?.DeclaringType?.Assembly))?.Name ?? owner ?? "?"; } public static string ModLabel(MethodBase m) { Assembly assembly = m?.DeclaringType?.Assembly; ModInfo modInfo = OfAssembly(assembly); if (modInfo != null) { return modInfo.Name; } string text = assembly?.GetName().Name ?? "?"; if (text == "Assembly-CSharp") { List<string> list = PatchOwners(Key(m)); if (list == null) { return "jeu"; } return "jeu, modifié par " + string.Join(", ", list); } return text; } public static string Describe(MethodBase m) { if (m == null) { return "?"; } Type declaringType = m.DeclaringType; if (declaringType != null && declaringType.Name.StartsWith("<") && declaringType.DeclaringType != null) { int num = declaringType.Name.IndexOf('>'); string text = ((num > 1) ? declaringType.Name.Substring(1, num - 1) : declaringType.Name); return declaringType.DeclaringType.FullName + "." + text + " (coroutine)"; } return declaringType?.FullName + "." + m.Name; } public static Type[] SafeTypes(Assembly a) { try { return a.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where((Type t) => t != null).ToArray(); } catch { return Array.Empty<Type>(); } } public static string FileName(string path) { if (!string.IsNullOrEmpty(path)) { return Path.GetFileName(path); } return "?"; } } [BepInPlugin("emeric.moddoctor", "ModDoctor", "1.0.0")] public class Plugin : BaseUnityPlugin { public const string Guid = "emeric.moddoctor"; public const string Name = "ModDoctor"; public const string Version = "1.0.0"; internal static Plugin Instance; internal static ManualLogSource Log; private void Awake() { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Breadcrumbs.MainThreadId = Thread.CurrentThread.ManagedThreadId; Settings.Init(((BaseUnityPlugin)this).Config); Report.Init(); LogWatch.Install(); StartupCheck.BeginScan(); Watchdog.Start(); GameObject val = new GameObject("ModDoctorRunner") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)(object)val); val.AddComponent<Runner>(); if (Settings.AutoTest.Value) { val.AddComponent<AutoTestDriver>(); } Log.LogInfo((object)"ModDoctor 1.0.0 chargé : surveillance des erreurs et des blocages active."); } } internal enum Severity { Info, Warning, Severe } internal sealed class Problem { public string Key; public string Kind; public string Mod; public string Title; public string Detail; public Severity Severity; public int Count = 1; public DateTime First = DateTime.Now; public DateTime Last = DateTime.Now; public string SeverityLabel { get { if (Severity != Severity.Severe) { if (Severity != Severity.Warning) { return "Info"; } return "Attention"; } return "GRAVE"; } } } internal static class Report { public const string CleanEndMarker = "=== Fin de session propre ==="; private const string SessionMarker = "=== Session ModDoctor"; public const string SuspectPrefix = "Suspect principal : "; public static readonly object Sync = new object(); private static readonly List<Problem> problems = new List<Problem>(); private static readonly Dictionary<string, Problem> byKey = new Dictionary<string, Problem>(); public static volatile int Version; public static bool PreviousUnclean; public static string PreviousSuspect; public static string Dir => Path.Combine(Paths.BepInExRootPath, "ModDoctor"); public static string FilePath => Path.Combine(Dir, "rapport.txt"); public static string PreviousPath => Path.Combine(Dir, "rapport-precedent.txt"); public static void Init() { try { Directory.CreateDirectory(Dir); if (File.Exists(FilePath)) { string text = File.ReadAllText(FilePath, Encoding.UTF8); File.Copy(FilePath, PreviousPath, overwrite: true); if (text.Contains("=== Session ModDoctor") && !text.Contains("=== Fin de session propre ===")) { PreviousUnclean = true; PreviousSuspect = (from l in text.Split('\n') select l.Trim()).LastOrDefault((string l) => l.StartsWith("Suspect principal : "))?.Substring("Suspect principal : ".Length); } } File.WriteAllText(FilePath, string.Format("{0} {1} — {2:yyyy-MM-dd HH:mm:ss} ===\r\n", "=== Session ModDoctor", "1.0.0", DateTime.Now) + "Profil : " + Paths.BepInExRootPath + "\r\n\r\n", Encoding.UTF8); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Rapport impossible à écrire : " + ex.Message)); } if (PreviousUnclean) { Add(new Problem { Key = "session-precedente", Kind = "Session précédente", Mod = (PreviousSuspect ?? "inconnu"), Severity = Severity.Severe, Title = ((PreviousSuspect != null) ? "la partie précédente s'est arrêtée pendant un blocage" : "la partie précédente ne s'est pas terminée normalement (plantage, blocage ou fermeture forcée)"), Detail = ((PreviousSuspect != null) ? ("Suspect principal : " + PreviousSuspect + "\r\n") : "") + "Détails dans " + PreviousPath }); } } public static void Write(string text) { lock (Sync) { try { File.AppendAllText(FilePath, text.Replace("\r\n", "\n").Replace("\n", "\r\n") + "\r\n", Encoding.UTF8); } catch { } } } public static Problem Add(Problem p) { lock (Sync) { if (p.Key != null && byKey.TryGetValue(p.Key, out var value)) { value.Count++; value.Last = DateTime.Now; return value; } problems.Add(p); if (p.Key != null) { byKey[p.Key] = p; } Version++; } string text = "[" + p.SeverityLabel + "] " + p.Kind + " — " + p.Mod + " : " + p.Title; switch (p.Severity) { case Severity.Severe: Plugin.Log.LogError((object)text); break; case Severity.Warning: Plugin.Log.LogWarning((object)text); break; default: Plugin.Log.LogInfo((object)text); break; } Write($"[{DateTime.Now:HH:mm:ss}] {text}" + (string.IsNullOrEmpty(p.Detail) ? "" : ("\n " + p.Detail.Replace("\n", "\n "))) + "\n"); return p; } public static void Update(Problem p, Severity severity, string title) { lock (Sync) { p.Severity = severity; p.Title = title; p.Last = DateTime.Now; Version++; } } public static List<Problem> Snapshot() { lock (Sync) { return new List<Problem>(problems); } } public static void MarkCleanEnd() { List<Problem> source = Snapshot(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine().AppendLine("--- Bilan de la session ---"); foreach (Problem item in (from x in source where x.Count > 1 orderby x.Count descending select x).Take(30)) { stringBuilder.AppendLine($"×{item.Count} [{item.SeverityLabel}] {item.Kind} — {item.Mod} : {item.Title}"); } if (!source.Any((Problem x) => x.Count > 1)) { stringBuilder.AppendLine("(aucune erreur répétée)"); } stringBuilder.AppendLine("=== Fin de session propre ==="); Write(stringBuilder.ToString()); } } internal class Runner : MonoBehaviour { public static long Frames; public static volatile bool Paused; public static volatile string SceneName; public static bool StartupDone; private bool open; private int seenVersion; private int lastVersion; private float lastNewProblemAt = -1000f; private float toastUntil; private string toast; private GUIStyle style; private Texture2D background; private bool hideDown; private bool listDown; private IEnumerator Start() { Scene activeScene = SceneManager.GetActiveScene(); SceneName = ((Scene)(ref activeScene)).name; SceneManager.sceneLoaded += OnSceneLoaded; while (!LogWatch.ChainloaderDone) { yield return null; } yield return null; ModIndex.Build(); LogWatch.ReadEarlyLog(); while (!StartupCheck.ScanDone) { yield return null; } StartupCheck.Analyze(); StartupDone = true; List<Problem> source = Report.Snapshot(); int num = source.Count((Problem p) => p.Severity == Severity.Severe); int num2 = source.Count((Problem p) => p.Severity == Severity.Warning); string text = $"=== ModDoctor : {ModIndex.Mods.Count} mods chargés, {num} problème(s) grave(s), {num2} avertissement(s), " + $"{ModIndex.Conflicts.Count((PatchConflict c) => c.HighRisk)} conflit(s) Harmony à risque. Rapport : {Report.FilePath} ==="; if (num > 0) { Plugin.Log.LogError((object)text); } else if (num2 > 0) { Plugin.Log.LogWarning((object)text); } else { Plugin.Log.LogMessage((object)text); } yield return Instrumenter.Run(); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { SceneName = ((Scene)(ref scene)).name; if (StartupDone) { ((MonoBehaviour)this).StartCoroutine(AfterSceneLoad()); } } private IEnumerator AfterSceneLoad() { yield return (object)new WaitForSecondsRealtime(3f); ModIndex.RefreshPatches(); yield return Instrumenter.Run(); } private void Update() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) Interlocked.Increment(ref Frames); if (ModIndex.Ready) { LogWatch.Drain(); } int version = Report.Version; if (version != lastVersion) { lastVersion = version; lastNewProblemAt = Time.realtimeSinceStartup; } Keyboard current = Keyboard.current; if (current == null) { return; } bool flag = Pressed(current, Settings.HideKey.Value, ref hideDown); bool flag2 = Pressed(current, Settings.OverlayKey.Value, ref listDown); if (flag) { Settings.Overlay.Value = !Settings.Overlay.Value; open = false; seenVersion = Report.Version; toast = (Settings.Overlay.Value ? $"ModDoctor réaffiché ({Settings.OverlayKey.Value} : liste des problèmes)" : $"ModDoctor masqué — la surveillance continue ({Settings.HideKey.Value} pour réafficher)"); toastUntil = Time.realtimeSinceStartup + 3f; } else if (flag2 && Settings.Overlay.Value) { open = !open; if (!open) { seenVersion = Report.Version; } } } private static bool Pressed(Keyboard kb, Key key, ref bool wasDown) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) bool flag = (int)key != 0 && ((ButtonControl)kb[key]).isPressed; bool result = flag && !wasDown; wasDown = flag; return result; } private void OnApplicationPause(bool pause) { Paused = pause; } private void OnApplicationFocus(bool focus) { Paused = !focus && !Application.runInBackground; } private void OnApplicationQuit() { Watchdog.Stop(); Report.MarkCleanEnd(); } private static bool Counts(Problem p) { if (p.Severity == Severity.Info) { if (Settings.ShowConflicts.Value) { return p.Kind == "Conflit possible"; } return false; } return true; } private void OnGUI() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Unknown result type (might be due to invalid IL or missing references) //IL_029f: Unknown result type (might be due to invalid IL or missing references) //IL_02c0: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) if (!StartupDone) { return; } EnsureStyles(); float num = Mathf.Max(1f, (float)Screen.height / 1080f); GUI.matrix = Matrix4x4.Scale(new Vector3(num, num, 1f)); float num2 = (float)Screen.width / num; float num3 = (float)Screen.height / num; if (Time.realtimeSinceStartup < toastUntil) { Background(new Rect(10f, 10f, 560f, 30f)); GUI.Label(new Rect(18f, 14f, 548f, 26f), "<b><color=#ff8c33>ModDoctor</color></b> : " + Escape(toast), style); } else { if (!Settings.Overlay.Value) { return; } if (!open) { if (Report.Version == seenVersion) { return; } float value = Settings.AlertSeconds.Value; if (value > 0f && Time.realtimeSinceStartup - lastNewProblemAt > value) { return; } } List<Problem> list = Report.Snapshot().Where(Counts).ToList(); if (list.Count == 0) { return; } string text = ((object)Settings.OverlayKey.Value/*cast due to .constrained prefix*/).ToString(); if (!open) { Problem problem = (from p in list orderby p.Severity descending, p.Last descending select p).First(); Background(new Rect(10f, 10f, 560f, 48f)); GUI.Label(new Rect(18f, 13f, 548f, 44f), $"<b><color=#ff8c33>ModDoctor</color></b> : {list.Count} problème(s) — <b>{text}</b> voir, <b>{Settings.HideKey.Value}</b> masquer\n<size=12>{Escape(problem.Mod)} : {Escape(Short(problem.Title, 70))}</size>", style); return; } float num4 = Mathf.Min(900f, num2 - 20f); float num5 = Mathf.Min(620f, num3 - 20f); Background(new Rect(10f, 10f, num4, num5)); GUILayout.BeginArea(new Rect(20f, 16f, num4 - 20f, num5 - 12f)); GUILayout.Label($"<b><color=#ff8c33>ModDoctor</color></b> — {list.Count} problème(s), les plus graves d'abord. <b>{text}</b> fermer, <b>{Settings.HideKey.Value}</b> masquer ModDoctor.", style, Array.Empty<GUILayoutOption>()); foreach (Problem item in (from p in list orderby p.Severity descending, p.Last descending select p).Take(14)) { string text2 = ((item.Severity == Severity.Severe) ? "#ff5555" : ((item.Severity == Severity.Warning) ? "#ffcc44" : "#aaaaaa")); string text3 = ((item.Count > 1) ? $" <color=#888888>×{item.Count}</color>" : ""); GUILayout.Label("<color=" + text2 + ">[" + item.SeverityLabel + "]</color> <b>" + Escape(Short(item.Mod, 70)) + "</b> — " + Escape(item.Kind) + " : " + Escape(Short(item.Title, 140)) + text3, style, Array.Empty<GUILayoutOption>()); } GUILayout.FlexibleSpace(); GUILayout.Label("<size=12>Rapport complet : " + Escape(Report.FilePath) + "</size>", style, Array.Empty<GUILayoutOption>()); GUILayout.EndArea(); } } private void EnsureStyles() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown //IL_0072: Unknown result type (might be due to invalid IL or missing references) if (style == null) { style = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = 14 }; } if ((Object)(object)background == (Object)null) { background = new Texture2D(1, 1) { hideFlags = (HideFlags)61 }; background.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.85f)); background.Apply(); } } private void Background(Rect r) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) GUI.DrawTexture(r, (Texture)(object)background, (ScaleMode)0, true); } private static string Short(string s, int max) { if (s != null) { if (s.Length > max) { return s.Substring(0, max) + "…"; } return s; } return ""; } private static string Escape(string s) { return s?.Replace("<", "‹").Replace(">", "›"); } } internal static class Settings { public static ConfigEntry<float> FreezeSeconds; public static ConfigEntry<bool> Instrument; public static ConfigEntry<int> MaxInstrumented; public static ConfigEntry<string> ExcludedMods; public static ConfigEntry<int> LoopErrorsPerSecond; public static ConfigEntry<bool> Overlay; public static ConfigEntry<Key> OverlayKey; public static ConfigEntry<Key> HideKey; public static ConfigEntry<float> AlertSeconds; public static ConfigEntry<bool> ShowConflicts; public static ConfigEntry<bool> AutoTest; public static void Init(ConfigFile cfg) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Expected O, but got Unknown //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Expected O, but got Unknown //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Expected O, but got Unknown FreezeSeconds = cfg.Bind<float>("Blocages", "SecondesAvantAlerte", 8f, new ConfigDescription("Le jeu est considéré bloqué s'il ne produit plus d'image pendant ce temps. Un chargement long peut déclencher une fausse alerte (le rapport indique alors la reprise).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 120f), Array.Empty<object>())); Instrument = cfg.Bind<bool>("Blocages", "SuivreLesMods", true, "Suit l'exécution des mods (Update, coroutines, patchs Harmony, hooks MonoMod) pour savoir lequel tournait au moment d'un blocage. Coûte un peu de temps au démarrage du menu."); MaxInstrumented = cfg.Bind<int>("Blocages", "MethodesSuiviesMax", 6000, new ConfigDescription("Nombre maximum de méthodes de mods suivies.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(100, 50000), Array.Empty<object>())); ExcludedMods = cfg.Bind<string>("Blocages", "ModsExclus", "", "GUID de mods à ne pas suivre, séparés par des virgules (en cas d'incompatibilité avec le suivi)."); LoopErrorsPerSecond = cfg.Bind<int>("Erreurs", "ErreursEnBoucleParSeconde", 20, new ConfigDescription("À partir de ce nombre d'erreurs identiques par seconde, l'erreur est signalée comme « en boucle ».", (AcceptableValueBase)(object)new AcceptableValueRange<int>(2, 1000), Array.Empty<object>())); Overlay = cfg.Bind<bool>("Affichage", "AlerteEnJeu", true, "Affiche une petite alerte en haut à gauche quand un problème est détecté."); OverlayKey = cfg.Bind<Key>("Affichage", "ToucheRapport", (Key)102, "Ouvre / ferme la liste des problèmes en jeu."); HideKey = cfg.Bind<Key>("Affichage", "ToucheMasquer", (Key)103, "Masque / réaffiche complètement ModDoctor à l'écran (alerte et liste) ; le choix est retenu. La surveillance et le rapport continuent."); AlertSeconds = cfg.Bind<float>("Affichage", "DureeAlerte", 15f, new ConfigDescription("Secondes d'affichage de l'alerte après un nouveau problème (0 = jusqu'à ce qu'on ouvre la liste).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 600f), Array.Empty<object>())); ShowConflicts = cfg.Bind<bool>("Affichage", "ConflitsDansLAlerte", false, "Compte aussi les conflits Harmony possibles dans l'alerte en jeu (ils sont toujours dans le rapport)."); AutoTest = cfg.Bind<bool>("Test", "AutoTest", false, "Développeur : lance tout seul le test automatique."); } } internal static class StartupCheck { private sealed class FoundPlugin { public string Guid; public string Name; public string Version; public string File; public List<(string guid, bool hard, string minVersion)> Deps = new List<(string, bool, string)>(); public List<string> Incompatible = new List<string>(); public List<string> Processes = new List<string>(); } private static readonly List<FoundPlugin> found = new List<FoundPlugin>(); private static readonly List<string> unreadable = new List<string>(); private static volatile bool scanDone; private static Dictionary<string, Dictionary<string, string>> cfg; public static bool ScanDone => scanDone; public static void BeginScan() { Thread thread = new Thread((ThreadStart)delegate { try { Scan(); } catch (Exception ex) { Plugin.Log.LogDebug((object)("Analyse des plugins impossible : " + ex.Message)); } scanDone = true; }); thread.IsBackground = true; thread.Name = "ModDoctor scan"; thread.Start(); } private static void Scan() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_0268: Unknown result type (might be due to invalid IL or missing references) //IL_026d: Unknown result type (might be due to invalid IL or missing references) //IL_01fc: Unknown result type (might be due to invalid IL or missing references) //IL_0201: Unknown result type (might be due to invalid IL or missing references) //IL_0296: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) if (!Directory.Exists(Paths.PluginPath)) { return; } DefaultAssemblyResolver val = new DefaultAssemblyResolver(); ((BaseAssemblyResolver)val).AddSearchDirectory(Paths.BepInExAssemblyDirectory); ((BaseAssemblyResolver)val).AddSearchDirectory(Paths.ManagedPath); ((BaseAssemblyResolver)val).AddSearchDirectory(Paths.PluginPath); string[] files = Directory.GetFiles(Paths.PluginPath, "*.dll", SearchOption.AllDirectories); foreach (string text in files) { try { AssemblyDefinition val2 = AssemblyDefinition.ReadAssembly(text, new ReaderParameters { ReadingMode = (ReadingMode)2, AssemblyResolver = (IAssemblyResolver)(object)val }); try { Enumerator<TypeDefinition> enumerator = val2.MainModule.Types.GetEnumerator(); try { while (enumerator.MoveNext()) { TypeDefinition current = enumerator.Current; CustomAttribute val3; try { val3 = ((IEnumerable<CustomAttribute>)current.CustomAttributes).FirstOrDefault((Func<CustomAttribute, bool>)((CustomAttribute a) => ((MemberReference)a.AttributeType).FullName == "BepInEx.BepInPlugin")); if (val3 == null || val3.ConstructorArguments.Count < 3) { continue; } goto IL_00d9; } catch { } continue; IL_00d9: FoundPlugin foundPlugin = new FoundPlugin(); CustomAttributeArgument val4 = val3.ConstructorArguments[0]; foundPlugin.Guid = ((CustomAttributeArgument)(ref val4)).Value as string; val4 = val3.ConstructorArguments[1]; foundPlugin.Name = ((CustomAttributeArgument)(ref val4)).Value as string; val4 = val3.ConstructorArguments[2]; foundPlugin.Version = ((CustomAttributeArgument)(ref val4)).Value as string; foundPlugin.File = text; FoundPlugin foundPlugin2 = foundPlugin; Enumerator<CustomAttribute> enumerator2 = current.CustomAttributes.GetEnumerator(); try { while (enumerator2.MoveNext()) { CustomAttribute current2 = enumerator2.Current; Collection<CustomAttributeArgument> constructorArguments; try { constructorArguments = current2.ConstructorArguments; } catch { continue; } switch (((MemberReference)current2.AttributeType).FullName) { case "BepInEx.BepInDependency": { if (constructorArguments.Count < 1) { break; } bool item = true; string item2 = null; if (constructorArguments.Count >= 2) { val4 = constructorArguments[1]; if (((CustomAttributeArgument)(ref val4)).Value is string text2) { item2 = text2; } else { val4 = constructorArguments[1]; if (((CustomAttributeArgument)(ref val4)).Value is int num) { item = (num & 1) != 0; } } } List<(string guid, bool hard, string minVersion)> deps = foundPlugin2.Deps; val4 = constructorArguments[0]; deps.Add((((CustomAttributeArgument)(ref val4)).Value as string, item, item2)); break; } case "BepInEx.BepInIncompatibility": if (constructorArguments.Count >= 1) { List<string> incompatible = foundPlugin2.Incompatible; val4 = constructorArguments[0]; incompatible.Add(((CustomAttributeArgument)(ref val4)).Value as string); } break; case "BepInEx.BepInProcess": if (constructorArguments.Count >= 1) { List<string> processes = foundPlugin2.Processes; val4 = constructorArguments[0]; processes.Add(((CustomAttributeArgument)(ref val4)).Value as string); } break; } } } finally { ((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose(); } lock (found) { found.Add(foundPlugin2); } } } finally { ((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose(); } } finally { ((IDisposable)val2)?.Dispose(); } } catch (BadImageFormatException) { } catch (Exception ex2) { lock (found) { unreadable.Add(Rel(text) + " : " + ex2.GetType().Name); } } } } public static void Analyze() { Dictionary<string, PluginInfo> loaded = Chainloader.PluginInfos; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("--- Mods chargés ---"); foreach (PluginInfo item in loaded.Values.OrderBy((PluginInfo i) => i.Metadata.Name)) { stringBuilder.AppendLine($" {item.Metadata.Name} {item.Metadata.Version} ({item.Metadata.GUID}) {Rel(item.Location)}"); } stringBuilder.AppendLine($" Total : {loaded.Count}"); Report.Write(stringBuilder.ToString()); List<FoundPlugin> source; lock (found) { source = new List<FoundPlugin>(found); } Dictionary<string, List<FoundPlugin>> dictionary = source.Where((FoundPlugin foundPlugin) => foundPlugin.Guid != null).GroupBy<FoundPlugin, string>((FoundPlugin foundPlugin) => foundPlugin.Guid, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, FoundPlugin>, string, List<FoundPlugin>>((IGrouping<string, FoundPlugin> g) => g.Key, (IGrouping<string, FoundPlugin> g) => g.ToList(), StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, List<FoundPlugin>> item2 in dictionary.Where((KeyValuePair<string, List<FoundPlugin>> g) => g.Value.Select((FoundPlugin foundPlugin) => foundPlugin.File).Distinct<string>(StringComparer.OrdinalIgnoreCase).Count() > 1)) { Report.Add(new Problem { Key = "doublon|" + item2.Key, Kind = "Doublon", Mod = item2.Value[0].Name, Severity = Severity.Warning, Title = $"présent {item2.Value.Count} fois ; une seule copie est chargée", Detail = string.Join("\n", item2.Value.Select((FoundPlugin foundPlugin) => foundPlugin.Version + " " + Rel(foundPlugin.File))) }); } string process = Path.GetFileName(Paths.ExecutablePath); foreach (KeyValuePair<string, List<FoundPlugin>> item3 in dictionary) { if (loaded.ContainsKey(item3.Key)) { continue; } FoundPlugin f = item3.Value[0]; List<string> list = new List<string>(); foreach (var (text, flag, text2) in f.Deps) { if (text == null) { continue; } if (loaded.TryGetValue(text, out var value)) { if (text2 != null && Version.TryParse(text2, out Version result) && value.Metadata.Version < result) { list.Add($"il faut {text} {text2} ou plus, version installée {value.Metadata.Version}"); } } else if (flag) { list.Add(dictionary.ContainsKey(text) ? ("sa dépendance " + Name(dictionary, text) + " est présente mais n'a pas pu être chargée") : ("dépendance absente : " + text)); } } foreach (string item4 in f.Incompatible.Where((string i) => i != null && loaded.ContainsKey(i))) { list.Add("déclaré incompatible avec " + loaded[item4].Metadata.Name + ", qui est installé"); } if (f.Processes.Count > 0 && !f.Processes.Any((string p) => string.Equals(p, process, StringComparison.OrdinalIgnoreCase))) { list.Add("prévu pour un autre jeu (" + string.Join(", ", f.Processes) + ")"); } List<string> list2 = Chainloader.DependencyErrors.Where((string e) => e.Contains(f.Name ?? "\0") || e.Contains(f.Guid)).ToList(); if (list.Count == 0 && list2.Count == 0) { list.Add("raison inconnue : voir les lignes « Loading » et les erreurs de BepInEx dans LogOutput.log"); } Problem problem = new Problem(); problem.Key = "non-charge|" + item3.Key; problem.Kind = "Non chargé"; problem.Mod = f.Name ?? item3.Key; problem.Severity = Severity.Severe; problem.Title = ((list.Count > 0) ? list[0] : FirstOr(list2)); problem.Detail = f.Guid + " " + f.Version + " " + Rel(f.File) + "\n" + string.Join("\n", list.Skip(1).Concat(list2)); Report.Add(problem); } foreach (string err in Chainloader.DependencyErrors) { if (!dictionary.Values.Any((List<FoundPlugin> l) => err.Contains("[" + l[0].Name + " "))) { Report.Add(new Problem { Key = "dep|" + err, Kind = "Non chargé", Mod = (BracketName(err) ?? "?"), Severity = Severity.Severe, Title = err }); } } foreach (string item5 in unreadable) { Report.Add(new Problem { Key = "illisible|" + item5, Kind = "Fichier illisible", Mod = item5, Severity = Severity.Warning, Title = "DLL de plugin corrompue ou incomplète ?" }); } List<PluginInfo> list3 = loaded.Values.Where((PluginInfo i) => (Object)(object)i.Instance == (Object)null).ToList(); bool flag2 = ReadBepInExCfg("Chainloader", "HideManagerGameObject") == "true"; if (list3.Count > 0 && list3.Count >= loaded.Count / 2 && !flag2) { Report.Add(new Problem { Key = "manager-detruit", Kind = "BepInEx", Mod = "BepInEx", Severity = Severity.Severe, Title = $"{list3.Count} plugins ont été détruits avec l'objet BepInEx", Detail = "Mets « HideManagerGameObject = true » dans BepInEx/config/BepInEx.cfg ([Chainloader])." }); } else { foreach (PluginInfo item6 in list3) { Report.Add(new Problem { Key = "detruit|" + item6.Metadata.GUID, Kind = "Plugin détruit", Mod = item6.Metadata.Name, Severity = Severity.Warning, Title = "le plugin est chargé mais son objet n'existe plus (erreur au démarrage ou détruit par un autre mod)" }); } } CheckLogging(); ReportConflicts(); } private static void CheckLogging() { if (ReadBepInExCfg("Logging.Console", "Enabled") == "false") { Warn("cfg-console", "la console BepInEx est désactivée", "BepInEx/config/BepInEx.cfg : [Logging.Console] Enabled = true (ou l'option « console » de r2modman)."); } if (ReadBepInExCfg("Logging", "UnityLogListening") == "false") { Warn("cfg-unitylog", "les erreurs Unity ne sont pas copiées dans le journal BepInEx", "BepInEx/config/BepInEx.cfg : [Logging] UnityLogListening = true."); } string text = ReadBepInExCfg("Logging.Console", "LogLevels"); if (text != null && !text.Contains("error") && !text.Contains("all")) { Warn("cfg-levels", "la console BepInEx n'affiche pas les erreurs", "BepInEx/config/BepInEx.cfg : [Logging.Console] LogLevels doit contenir Error (ex. « Fatal, Error, Warning, Message, Info »)."); } static void Warn(string key, string title, string detail) { Report.Add(new Problem { Key = key, Kind = "Journal", Mod = "BepInEx", Severity = Severity.Warning, Title = title, Detail = detail }); } } private static void ReportConflicts() { List<PatchConflict> conflicts = ModIndex.Conflicts; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"--- Méthodes du jeu patchées par plusieurs mods : {conflicts.Count} ({conflicts.Count((PatchConflict c) => c.HighRisk)} à risque) ---"); foreach (PatchConflict item in (from c in conflicts orderby c.HighRisk descending, c.Mods.Count descending select c).Take(60)) { string text = (item.HighRisk ? "RISQUE" : " "); string text2 = ((item.SkipPrefixMods.Count > 0) ? (" ; peut annuler la méthode : " + string.Join(", ", item.SkipPrefixMods)) : "") + ((item.TranspilerMods.Count > 0) ? (" ; réécrit le code : " + string.Join(", ", item.TranspilerMods)) : ""); stringBuilder.AppendLine(" " + text + " " + item.Method.Replace("::", ".") + " : " + string.Join(", ", item.Mods) + text2); } Report.Write(stringBuilder.ToString()); foreach (PatchConflict item2 in conflicts.Where((PatchConflict c) => c.HighRisk)) { Report.Add(new Problem { Key = "conflit|" + item2.Method, Kind = "Conflit possible", Mod = string.Join(" / ", item2.Mods), Severity = Severity.Info, Title = string.Format("{0} est modifié par {1} mods", item2.Method.Replace("::", "."), item2.Mods.Count), Detail = ((item2.SkipPrefixMods.Count > 0) ? ("Peut empêcher la méthode d'origine (et des patchs d'autres mods) de s'exécuter : " + string.Join(", ", item2.SkipPrefixMods) + "\n") : "") + ((item2.TranspilerMods.Count > 0) ? ("Réécrit son code : " + string.Join(", ", item2.TranspilerMods) + "\n") : "") + "Ce n'est pas forcément un bug : à regarder si un de ces mods ne marche pas." }); } } public static string ReadBepInExCfg(string section, string key) { if (cfg == null) { cfg = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); try { string path = Path.Combine(Paths.ConfigPath, "BepInEx.cfg"); Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.StartsWith("[") && text.EndsWith("]")) { dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); cfg[text.Substring(1, text.Length - 2)] = dictionary; } else if (!text.StartsWith("#") && text.Contains("=")) { int num = text.IndexOf('='); dictionary[text.Substring(0, num).Trim()] = text.Substring(num + 1).Trim().ToLowerInvariant(); } } } catch { } } if (!cfg.TryGetValue(section, out var value) || !value.TryGetValue(key, out var value2)) { return null; } return value2; } private static string Name(Dictionary<string, List<FoundPlugin>> byGuid, string guid) { if (!byGuid.TryGetValue(guid, out var value)) { return guid; } return value[0].Name ?? guid; } public static string BracketName(string s) { int num = s.IndexOf('['); int num2 = ((num >= 0) ? s.IndexOf(']', num) : (-1)); if (num2 <= num + 1) { return null; } string text = s.Substring(num + 1, num2 - num - 1); int num3 = text.LastIndexOf(' '); if (num3 <= 0) { return text; } return text.Substring(0, num3); } private static string FirstOr(List<string> l) { if (l.Count <= 0) { return "non chargé"; } return l[0]; } private static string Rel(string path) { if (string.IsNullOrEmpty(path)) { return "?"; } string pluginPath = Paths.PluginPath; if (!path.StartsWith(pluginPath, StringComparison.OrdinalIgnoreCase)) { return path; } return "plugins" + path.Substring(pluginPath.Length); } } internal static class Watchdog { private static Thread thread; private static volatile bool running; public static volatile int FreezeReports; public static volatile int Resumes; public static void Start() { running = true; thread = new Thread(Loop) { IsBackground = true, Name = "ModDoctor watchdog", Priority = ThreadPriority.AboveNormal }; thread.Start(); } public static void Stop() { running = false; } private static void Loop() { long num = -1L; long num2 = LogWatch.Clock.ElapsedMilliseconds; double num3 = 0.0; Problem problem = null; string text = null; double num4 = 0.0; while (running) { Thread.Sleep(250); try { double num5 = Settings.FreezeSeconds.Value; long elapsedMilliseconds = LogWatch.Clock.ElapsedMilliseconds; long num6 = Interlocked.Read(in Runner.Frames); if (num6 == 0L) { string currentLoading = LogWatch.CurrentLoading; if (currentLoading != null && !LogWatch.ChainloaderDone) { double num7 = (double)(elapsedMilliseconds - Interlocked.Read(in LogWatch.LoadingSince)) / 1000.0; if (currentLoading != text) { text = currentLoading; num4 = Math.Max(num5, 10.0); } if (num7 >= num4) { num4 *= 2.0; Report.Add(new Problem { Key = "chargement|" + currentLoading + "|" + (int)num4, Kind = "Chargement bloqué", Mod = currentLoading, Severity = Severity.Severe, Title = $"le chargement de ce plugin dure depuis {num7:0} s", Detail = "Suspect principal : " + currentLoading + " (chargement)" }); } } continue; } if (num6 != num) { if (problem != null) { double num8 = (double)(elapsedMilliseconds - num2) / 1000.0; Report.Update(problem, Severity.Warning, $"le jeu a été figé {num8:0.0} s puis est reparti ({problem.Mod})"); Report.Write($"[{DateTime.Now:HH:mm:ss}] Reprise après {num8:0.0} s de blocage.\n"); Plugin.Log.LogWarning((object)$"Le jeu est reparti après {num8:0.0} s de blocage."); Resumes++; problem = null; } num = num6; num2 = elapsedMilliseconds; num3 = num5; continue; } if (Runner.Paused) { num2 = elapsedMilliseconds; continue; } double num9 = (double)(elapsedMilliseconds - num2) / 1000.0; if (!(num9 < num3)) { num3 = Math.Max(num3 * 2.0, num5); Problem problem2 = DescribeFreeze(num9, problem == null); if (problem == null) { problem = problem2; } else { Report.Update(problem, Severity.Severe, problem2.Title); } } } catch (Exception ex) { try { Plugin.Log.LogDebug((object)("Surveillance : " + ex.Message)); } catch { } } } } private static Problem DescribeFreeze(double seconds, bool first) { FreezeReports++; int num = Math.Min(Breadcrumbs.Depth, 128); List<MethodBase> list = new List<MethodBase>(); for (int num2 = num - 1; num2 >= 0; num2--) { if (Breadcrumbs.Stack[num2] != null) { list.Add(Breadcrumbs.Stack[num2]); } } List<MethodBase> list2 = new List<MethodBase>(); int recentCount = Breadcrumbs.RecentCount; for (int i = 0; i < 64 && i < recentCount; i++) { MethodBase methodBase = Breadcrumbs.Recent[(recentCount - 1 - i) & 0x3F]; if (methodBase != null && !list2.Contains(methodBase)) { list2.Add(methodBase); } if (list2.Count >= 12) { break; } } string text; string text2; if (list.Count > 0) { text = ModIndex.ModLabel(list[0]) + " › " + ModIndex.Describe(list[0]); text2 = "le thread principal est à l'intérieur de cette méthode"; } else if (list2.Count > 0) { text = ModIndex.ModLabel(list2[0]) + " (dernier mod exécuté : " + ModIndex.Describe(list2[0]) + ")"; text2 = "aucune méthode de mod n'est en cours : le blocage est dans le jeu, un chargement, ou un mod non suivi ; voici le dernier mod qui a tourné"; } else { text = "inconnu"; text2 = (Settings.Instrument.Value ? "aucune méthode de mod suivie n'a encore tourné" : "le suivi des mods est désactivé (SuivreLesMods = false)"); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"=== BLOCAGE : le jeu ne répond plus depuis {seconds:0} s ({DateTime.Now:yyyy-MM-dd HH:mm:ss}) ==="); stringBuilder.AppendLine("Scène : " + (Runner.SceneName ?? "?")); stringBuilder.AppendLine("Suspect principal : " + text); stringBuilder.AppendLine("Pourquoi : " + text2 + "."); stringBuilder.AppendLine(); stringBuilder.AppendLine("Méthodes de mods en cours (de la plus profonde à la plus haute) :"); if (list.Count == 0) { stringBuilder.AppendLine(" (aucune)"); } foreach (MethodBase item in list) { stringBuilder.AppendLine(" " + ModIndex.ModLabel(item) + " › " + ModIndex.Describe(item)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Dernières méthodes de mods appelées (plus récentes d'abord) :"); if (list2.Count == 0) { stringBuilder.AppendLine(" (aucune)"); } foreach (MethodBase item2 in list2) { stringBuilder.AppendLine(" " + ModIndex.ModLabel(item2) + " › " + ModIndex.Describe(item2)); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Dernières erreurs reçues :"); List<string> list3 = LogWatch.RecentErrors(); if (list3.Count == 0) { stringBuilder.AppendLine(" (aucune)"); } foreach (string item3 in list3) { stringBuilder.AppendLine(" " + item3); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Si le jeu reste figé : ferme-le, retire le mod suspect et relance. Ce rapport est aussi dans rapport-precedent.txt au prochain lancement."); string text3 = stringBuilder.ToString(); try { File.WriteAllText(Path.Combine(Report.Dir, $"blocage-{DateTime.Now:yyyyMMdd-HHmmss}.txt"), text3.Replace("\n", "\r\n").Replace("\r\r", "\r"), Encoding.UTF8); } catch { } Problem problem = new Problem { Key = (first ? ("blocage|" + DateTime.Now.Ticks) : null), Kind = "Blocage", Mod = text, Severity = Severity.Severe, Title = $"le jeu ne répond plus depuis {seconds:0} s", Detail = text3 }; if (first) { return Report.Add(problem); } Report.Write($"[{DateTime.Now:HH:mm:ss}] Toujours bloqué ({seconds:0} s).\n " + "Suspect principal : " + text + "\n"); Plugin.Log.LogError((object)string.Format("Toujours bloqué depuis {0:0} s. {1}{2}", seconds, "Suspect principal : ", text)); return problem; } } }