Decompiled source of ModDoctor v1.0.0

ModDoctor.dll

Decompiled a day ago
using 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;
		}
	}
}