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.1
ModDoctor.dll
Decompiled 5 hours agousing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace ModDoctor; [BepInPlugin("bub.moddoctor", "Mod Doctor", "1.0.1")] public class ModDoctorPlugin : BaseUnityPlugin { public enum ReportLanguage { Auto, English, Portuguese } public const string Guid = "bub.moddoctor"; public const string Version = "1.0.1"; private const string ReportRpc = "ModDoctor_Report"; internal static ModDoctorPlugin Instance; internal static ManualLogSource Log; private ConfigEntry<ReportLanguage> language; private ConfigEntry<KeyboardShortcut> toggleKey; private ConfigEntry<bool> showToast; private ConfigEntry<bool> suppressRepeats; private ConfigEntry<int> suppressAfter; private ConfigEntry<bool> checkUpdates; private ConfigEntry<int> updateCacheHours; private ConfigEntry<bool> sendToServer; private ConfigEntry<bool> collectFromClients; internal Diagnosis Diagnosis; private RepeatFilter repeatFilter; private UpdateChecker updates; private string outputDir; private float nextReportWrite; private float nextSend; private int lastSentVersion = -1; private ZRoutedRpc registeredRpc; private bool windowOpen; private Rect windowRect = new Rect(60f, 60f, 760f, 560f); private Vector2 scroll; private string windowText = ""; private string toastText; private float toastUntil; private float nextToastCheck; private bool wasInWorld; private readonly HashSet<string> announced = new HashSet<string>(); private GUIStyle windowStyle; private GUIStyle textStyle; private GUIStyle toastStyle; private void Awake() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; language = ((BaseUnityPlugin)this).Config.Bind<ReportLanguage>("General", "Language", ReportLanguage.Auto, "Report language. Auto follows the game language (Portuguese or English)."); toggleKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "ToggleKey", new KeyboardShortcut((KeyCode)288, (KeyCode[])(object)new KeyCode[0]), "Opens and closes the report window."); showToast = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ShowToast", true, "Show a short notice when a mod starts causing problems."); suppressRepeats = ((BaseUnityPlugin)this).Config.Bind<bool>("Log", "SuppressRepeats", true, "Stop identical errors from being written to LogOutput.log over and over. A summary line is written instead."); suppressAfter = ((BaseUnityPlugin)this).Config.Bind<int>("Log", "SuppressAfter", 20, "How many identical copies are written before the rest are suppressed."); checkUpdates = ((BaseUnityPlugin)this).Config.Bind<bool>("Updates", "CheckThunderstore", true, "Look up the latest version of each installed package on thunderstore.io (public API, no personal data sent)."); updateCacheHours = ((BaseUnityPlugin)this).Config.Bind<int>("Updates", "CacheHours", 12, "How long a Thunderstore lookup is reused before asking again."); sendToServer = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "SendReportsToServer", true, "Send your findings (mod names, versions and errors, with your character name) to the server you are playing on, if it runs Mod Doctor."); collectFromClients = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "CollectClientReports", true, "On a server: store the reports players send, in BepInEx/ModDoctor/players and summary.txt."); outputDir = Path.Combine(Paths.BepInExRootPath, "ModDoctor"); Directory.CreateDirectory(outputDir); Diagnosis = new Diagnosis(); Application.logMessageReceivedThreaded += new LogCallback(OnUnityLog); Logger.Listeners.Add((ILogListener)(object)new ModErrorListener(Diagnosis)); if (suppressRepeats.Value) { repeatFilter = RepeatFilter.Install(suppressAfter.Value); } updates = new UpdateChecker(Path.Combine(outputDir, "thunderstore-cache.txt")); Log.LogInfo((object)("Mod Doctor 1.0.1 watching for mod errors. Report: " + Path.Combine(outputDir, "report.txt"))); } private void Start() { Diagnosis.IndexPlugins(); Backfill.FromLogOutput(Path.Combine(Paths.BepInExRootPath, "LogOutput.log"), Diagnosis); if (checkUpdates.Value) { ((MonoBehaviour)this).StartCoroutine(updates.Run(Diagnosis.Packages(), updateCacheHours.Value)); } } private void OnDestroy() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown Application.logMessageReceivedThreaded -= new LogCallback(OnUnityLog); WriteReport(); } private void OnApplicationQuit() { WriteReport(); } private void OnUnityLog(string condition, string stackTrace, LogType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 if ((int)type == 4 || (int)type == 0 || (int)type == 1) { Diagnosis.Record(condition, stackTrace, null); } } private void Update() { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) Diagnosis.ResolvePending(); if (!IsHeadless()) { UpdateToasts(); } if (!IsHeadless()) { KeyboardShortcut value = toggleKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { windowOpen = !windowOpen; if (windowOpen) { windowText = BuildReport(rich: true); } } } if (repeatFilter != null) { repeatFilter.FlushSummaries(); } if (Time.unscaledTime >= nextReportWrite) { nextReportWrite = Time.unscaledTime + 30f; WriteReport(); } UpdateNetworking(); } private void UpdateToasts() { //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) if (!showToast.Value || Time.unscaledTime < nextToastCheck) { return; } nextToastCheck = Time.unscaledTime + 2f; if (!((Object)(object)Player.m_localPlayer != (Object)null)) { wasInWorld = false; return; } List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(); foreach (Problem item in Diagnosis.Snapshot()) { if (item.Mod == null) { continue; } string text = ReportWriter.Kind(item); if (!(text == "error")) { hashSet.Add(item.Mod.Name); if (announced.Add(item.Mod.Name + "|" + text) && wasInWorld && !list.Contains(item.Mod.Name)) { list.Add(item.Mod.Name); } } } List<string> list2 = (wasInWorld ? list : hashSet.ToList()); wasInWorld = true; if (list2.Count != 0 && !(Time.unscaledTime < toastUntil - 2f)) { string newValue = string.Join(", ", list2.Take(3).ToArray()) + ((list2.Count > 3) ? (" +" + (list2.Count - 3)) : ""); toastText = Text.T((list2.Count == 1) ? "toast" : "toastmany", Lang()).Replace("{mod}", newValue).Replace("{key}", ((object)toggleKey.Value/*cast due to .constrained prefix*/).ToString()); toastUntil = Time.unscaledTime + 12f; } } private static bool IsHeadless() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 return (int)SystemInfo.graphicsDeviceType == 4; } internal string Lang() { if (language.Value == ReportLanguage.English) { return "en"; } if (language.Value == ReportLanguage.Portuguese) { return "pt"; } try { if (Localization.instance != null && Localization.instance.GetSelectedLanguage().StartsWith("Portuguese")) { return "pt"; } } catch (Exception) { } return "en"; } private string BuildReport() { return BuildReport(rich: false); } private string BuildReport(bool rich) { return ReportWriter.Render(Diagnosis.Snapshot(), updates, Diagnosis.PluginCount, Lang(), null, rich); } private void WriteReport() { try { File.WriteAllText(Path.Combine(outputDir, "report.txt"), BuildReport(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && ZNet.instance.IsDedicated() && collectFromClients.Value) { ServerStore.Save(outputDir, "(server)", "1.0.1", Diagnosis.PluginCount, ReportWriter.Serialize(Diagnosis.Snapshot(), updates, 40), Lang()); } } catch (Exception ex) { Log.LogWarning((object)("Could not write report: " + ex.Message)); } } private void UpdateNetworking() { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Expected O, but got Unknown ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || (Object)(object)ZNet.instance == (Object)null) { return; } if (registeredRpc != instance) { instance.Register<ZPackage>("ModDoctor_Report", (Action<long, ZPackage>)OnClientReport); registeredRpc = instance; lastSentVersion = -1; } if (ZNet.instance.IsServer() || !sendToServer.Value || Time.unscaledTime < nextSend) { return; } nextSend = Time.unscaledTime + 60f; int version = Diagnosis.Version; if (version != lastSentVersion) { ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null && !((Object)(object)Game.instance == (Object)null) && Game.instance.GetPlayerProfile() != null) { ZPackage val = new ZPackage(); val.Write(1); val.Write(Game.instance.GetPlayerProfile().GetName()); val.Write("1.0.1"); val.Write(Diagnosis.PluginCount); val.Write(ReportWriter.Serialize(Diagnosis.Snapshot(), updates, 40)); instance.InvokeRoutedRPC(serverPeer.m_uid, "ModDoctor_Report", new object[1] { val }); lastSentVersion = version; } } } private void OnClientReport(long sender, ZPackage pkg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !collectFromClients.Value) { return; } try { int num = pkg.ReadInt(); if (num == 1) { string player = pkg.ReadString(); string clientVersion = pkg.ReadString(); int plugins = pkg.ReadInt(); string text = pkg.ReadString(); if (text.Length > 64000) { text = text.Substring(0, 64000); } ServerStore.Save(outputDir, player, clientVersion, plugins, text, Lang()); } } catch (Exception ex) { Log.LogWarning((object)("Ignored a malformed report from peer " + sender + ": " + ex.Message)); } } private void EnsureStyles() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Expected O, but got Unknown //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Expected O, but got Unknown //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Expected O, but got Unknown //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Expected O, but got Unknown //IL_01f7: Unknown result type (might be due to invalid IL or missing references) if (windowStyle == null) { Texture2D background = Solid(new Color(0.09f, 0.075f, 0.06f, 0.97f)); Texture2D background2 = Solid(new Color(0.12f, 0.08f, 0.04f, 0.95f)); windowStyle = new GUIStyle(GUI.skin.window); windowStyle.normal.background = background; windowStyle.onNormal.background = background; windowStyle.focused.background = background; windowStyle.onFocused.background = background; GUIStyleState normal = windowStyle.normal; Color textColor = (windowStyle.onNormal.textColor = new Color(1f, 0.82f, 0.45f)); normal.textColor = textColor; windowStyle.fontSize = 15; windowStyle.fontStyle = (FontStyle)1; windowStyle.padding = new RectOffset(14, 14, 30, 12); windowStyle.border = new RectOffset(2, 2, 2, 2); textStyle = new GUIStyle(GUI.skin.label); textStyle.wordWrap = true; textStyle.richText = true; textStyle.fontSize = 14; textStyle.normal.textColor = new Color(0.93f, 0.9f, 0.84f); toastStyle = new GUIStyle(GUI.skin.box); toastStyle.normal.background = background2; toastStyle.fontSize = 15; toastStyle.wordWrap = true; toastStyle.alignment = (TextAnchor)3; toastStyle.padding = new RectOffset(14, 14, 10, 10); toastStyle.normal.textColor = new Color(1f, 0.85f, 0.5f); } } private static Texture2D Solid(Color color) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_000b: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, color); val.Apply(); ((Object)val).hideFlags = (HideFlags)61; return val; } private void OnGUI() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Expected O, but got Unknown //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) if (!IsHeadless() && ((toastText != null && !(Time.unscaledTime >= toastUntil)) || windowOpen)) { EnsureStyles(); if (toastText != null && Time.unscaledTime < toastUntil) { float num = toastStyle.CalcHeight(new GUIContent(toastText), 440f); GUI.Box(new Rect(20f, 20f, 440f, num), toastText, toastStyle); } if (windowOpen) { windowRect = GUILayout.Window(19780, windowRect, new WindowFunction(DrawWindow), "Mod Doctor 1.0.1", windowStyle, (GUILayoutOption[])(object)new GUILayoutOption[0]); } } } private void DrawWindow(int id) { //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Unknown result type (might be due to invalid IL or missing references) string lang = Lang(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button(Text.T("refresh", lang), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) })) { windowText = BuildReport(rich: true); } if (GUILayout.Button(Text.T("copy", lang), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) })) { GUIUtility.systemCopyBuffer = BuildReport(rich: false); } if (GUILayout.Button(Text.T("folder", lang), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(130f) })) { Application.OpenURL("file://" + outputDir.Replace('\\', '/')); } GUILayout.FlexibleSpace(); if (GUILayout.Button(Text.T("close", lang), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { windowOpen = false; } GUILayout.EndHorizontal(); GUILayout.Space(6f); scroll = GUILayout.BeginScrollView(scroll, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label(windowText, textStyle, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.EndScrollView(); GUI.DragWindow(new Rect(0f, 0f, 10000f, 22f)); } } public class ModRef { public string Guid; public string Name; public string Version; public string Folder; public string PackageNamespace; public string PackageName; public string PackageVersion; public string FullPackage { get { if (PackageNamespace != null) { return PackageNamespace + "-" + PackageName; } return null; } } public string Display => Name + " " + (PackageVersion ?? Version); } public class Problem { public string Key; public string Kind; public string ExceptionType; public string FirstLine; public string Location; public ModRef Mod; public List<ModRef> PatchedBy = new List<ModRef>(); public string LogSource; public int Count; public DateTime First; public DateTime Last; public string RawStack; public bool Resolved; public bool LikelyFromOtherFailure; public double PerSecond { get { double totalSeconds = (Last - First).TotalSeconds; if (!(totalSeconds < 1.0)) { return (double)Count / totalSeconds; } return 0.0; } } } public class Diagnosis { private readonly object sync = new object(); private readonly Dictionary<string, Problem> problems = new Dictionary<string, Problem>(); private readonly Queue<Problem> pending = new Queue<Problem>(); private readonly Dictionary<Assembly, ModRef> assemblyOwner = new Dictionary<Assembly, ModRef>(); private readonly Dictionary<string, ModRef> typeOwner = new Dictionary<string, ModRef>(); private readonly Dictionary<string, ModRef> byGuid = new Dictionary<string, ModRef>(); private readonly Dictionary<string, ModRef> byLogName = new Dictionary<string, ModRef>(); private readonly Dictionary<string, ModRef> harmonyOwner = new Dictionary<string, ModRef>(); private bool indexed; private int version; public int PluginCount; private static readonly Regex DmdFrame = new Regex("DMD<([^:>]+)::([^>]+)>"); private static readonly Regex ManagedFrame = new Regex("^\\s*(?:at\\s+)?(?:\\(wrapper [^)]*\\)\\s*)?([A-Za-z_<][\\w\\.\\+/<>`\\[\\],]*)\\s*\\("); private static readonly string[] IncompatibleMarkers = new string[9] { "MissingMethodException", "MissingFieldException", "TypeLoadException", "BadImageFormatException", "MissingMemberException", "EntryPointNotFoundException", "Undefined target method", "Could not find method", "expected reference type" }; private static readonly Regex Numbers = new Regex("\\d+"); public int Version { get { lock (sync) { return version; } } } public void Record(string condition, string stackTrace, string logSource) { if (string.IsNullOrEmpty(condition) || condition.StartsWith("[Mod Doctor]")) { return; } string text = FirstLine(condition); string text2 = (string.IsNullOrEmpty(stackTrace) ? ExtractInlineStack(condition) : stackTrace); string key = Normalize(text) + "|" + TopFrames(text2, 3) + "|" + logSource; lock (sync) { if (problems.TryGetValue(key, out var value)) { value.Count++; value.Last = DateTime.Now; if (value.Count == 100 || value.Count == 1000) { version++; } return; } value = new Problem(); value.Key = key; value.FirstLine = ((text.Length > 300) ? text.Substring(0, 300) : text); value.ExceptionType = ExceptionType(text); value.RawStack = text2; value.LogSource = logSource; value.Count = 1; value.First = (value.Last = DateTime.Now); problems[key] = value; pending.Enqueue(value); version++; } } public void ResolvePending() { if (!indexed) { return; } while (true) { Problem problem; lock (sync) { if (pending.Count == 0) { break; } problem = pending.Dequeue(); } try { Resolve(problem); } catch (Exception ex) { ModDoctorPlugin.Log.LogDebug((object)("Could not attribute an error: " + ex.Message)); } lock (sync) { problem.Resolved = true; version++; } } } public List<Problem> Snapshot() { lock (sync) { return problems.Values.Where((Problem p) => p.Resolved).ToList(); } } public IEnumerable<ModRef> Packages() { return byGuid.Values.Where((ModRef m) => m.PackageNamespace != null); } public void IndexPlugins() { string fullPath = Path.GetFullPath(Paths.PluginPath); Dictionary<string, ModRef> dictionary = new Dictionary<string, ModRef>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; ModRef modRef = new ModRef(); modRef.Guid = value.Metadata.GUID; modRef.Name = value.Metadata.Name; modRef.Version = value.Metadata.Version.ToString(); modRef.Folder = TopFolder(fullPath, value.Location); ReadPackage(fullPath, modRef); byGuid[modRef.Guid] = modRef; byLogName[modRef.Name] = modRef; harmonyOwner[modRef.Guid] = modRef; if (modRef.Folder != null && !dictionary.ContainsKey(modRef.Folder)) { dictionary[modRef.Folder] = modRef; } if ((Object)(object)value.Instance != (Object)null) { assemblyOwner[((object)value.Instance).GetType().Assembly] = modRef; } } PluginCount = byGuid.Count; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!assemblyOwner.ContainsKey(assembly) && !assembly.IsDynamic) { string location; try { location = assembly.Location; } catch (Exception) { continue; } string text = TopFolder(fullPath, location); if (text != null && dictionary.TryGetValue(text, out var value2)) { assemblyOwner[assembly] = value2; } } } foreach (KeyValuePair<Assembly, ModRef> item in assemblyOwner) { Type[] types; try { types = item.Key.GetTypes(); } catch (ReflectionTypeLoadException ex2) { types = ex2.Types; } catch (Exception) { continue; } Type[] array = types; foreach (Type type in array) { if (!(type == null) && type.FullName != null) { typeOwner[type.FullName.Replace('/', '+')] = item.Value; } } } indexed = true; } private static string TopFolder(string pluginRoot, string location) { if (string.IsNullOrEmpty(location)) { return null; } string fullPath = Path.GetFullPath(location); if (!fullPath.StartsWith(pluginRoot, StringComparison.OrdinalIgnoreCase)) { return null; } string text = fullPath.Substring(pluginRoot.Length).TrimStart('\\', '/'); int num = text.IndexOfAny(new char[2] { '\\', '/' }); if (num >= 0) { return text.Substring(0, num); } return null; } private static void ReadPackage(string pluginRoot, ModRef mod) { if (mod.Folder == null) { return; } int num = mod.Folder.IndexOf('-'); if (num <= 0 || num == mod.Folder.Length - 1) { return; } mod.PackageNamespace = mod.Folder.Substring(0, num); mod.PackageName = mod.Folder.Substring(num + 1); string path = Path.Combine(Path.Combine(pluginRoot, mod.Folder), "manifest.json"); if (File.Exists(path)) { Match match = Regex.Match(File.ReadAllText(path), "\"version_number\"\\s*:\\s*\"([^\"]+)\""); if (match.Success) { mod.PackageVersion = match.Groups[1].Value; } } } private void Resolve(Problem p) { if (p.LogSource != null && byLogName.TryGetValue(p.LogSource, out var value)) { p.Mod = value; } string[] array = (p.RawStack ?? "").Split('\n'); foreach (string text in array) { string text2 = text.Trim(); if (text2.Length == 0) { continue; } Match match = DmdFrame.Match(text2); if (match.Success) { string text3 = match.Groups[1].Value.Replace('/', '+'); string value2 = match.Groups[2].Value; if (p.Location == null) { p.Location = text3 + "." + value2; } if (typeOwner.TryGetValue(text3, out var value3)) { if (p.Mod == null) { p.Mod = value3; } break; } foreach (ModRef item in PatchersOf(text3, value2)) { if (!p.PatchedBy.Contains(item)) { p.PatchedBy.Add(item); } } if (p.PatchedBy.Count > 0) { break; } continue; } Match match2 = ManagedFrame.Match(text2); if (!match2.Success) { continue; } string text4 = match2.Groups[1].Value.Replace('/', '+'); int num = text4.LastIndexOf('.'); if (num <= 0) { continue; } string text5 = text4.Substring(0, num); if (p.Location == null && !IsFramework(text5)) { p.Location = text4; } if (typeOwner.TryGetValue(text5, out var value4)) { if (p.Mod == null) { p.Mod = value4; } p.Location = text4; break; } } p.Kind = Classify(p); if (p.Mod == null && p.PatchedBy.Count == 1) { p.Mod = p.PatchedBy[0]; } if (p.Mod != null || p.PatchedBy.Count <= 1) { return; } List<ModRef> list; lock (sync) { list = p.PatchedBy.Where((ModRef m) => problems.Values.Any((Problem o) => o != p && o.Resolved && o.Mod == m && (o.Kind == "patch" || o.Kind == "incompatible"))).ToList(); } if (list.Count == 1) { p.Mod = list[0]; p.LikelyFromOtherFailure = true; } } private IEnumerable<ModRef> PatchersOf(string typeName, string methodName) { List<ModRef> list = new List<ModRef>(); Type type = AccessTools.TypeByName(typeName); if (type == null) { return list; } IEnumerable<MethodBase> enumerable = ((methodName == ".ctor") ? type.GetConstructors(AccessTools.all).Cast<MethodBase>() : (from m in type.GetMethods(AccessTools.all) where m.Name == methodName select m).Cast<MethodBase>()); foreach (MethodBase item in enumerable) { Patches patchInfo = Harmony.GetPatchInfo(item); if (patchInfo == null) { continue; } IEnumerable<Patch> enumerable2 = patchInfo.Prefixes.Concat(patchInfo.Postfixes).Concat(patchInfo.Transpilers).Concat(patchInfo.Finalizers); foreach (Patch item2 in enumerable2) { ModRef value = null; MethodInfo patchMethod = item2.PatchMethod; if (patchMethod != null && patchMethod.DeclaringType != null) { assemblyOwner.TryGetValue(patchMethod.DeclaringType.Assembly, out value); } if (value == null) { harmonyOwner.TryGetValue(item2.owner, out value); } if (value != null && !list.Contains(value) && value.Guid != "bub.moddoctor") { list.Add(value); } } } return list; } private static bool IsFramework(string type) { if (!type.StartsWith("UnityEngine") && !type.StartsWith("System") && !type.StartsWith("HarmonyLib") && !type.StartsWith("BepInEx") && !type.StartsWith("MonoMod")) { return type.StartsWith("Mono."); } return true; } private static string Classify(Problem p) { if (p.Mod == null && p.PatchedBy.Count == 0 && p.LogSource == null) { if (p.Location != null) { return "game"; } return "engine"; } string text = p.FirstLine + "\n" + (p.RawStack ?? ""); if (text.IndexOf("HarmonyException", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Patching exception", StringComparison.OrdinalIgnoreCase) >= 0) { return "patch"; } string[] incompatibleMarkers = IncompatibleMarkers; foreach (string value in incompatibleMarkers) { if (text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return "incompatible"; } } return "error"; } private static string FirstLine(string text) { int num = text.IndexOf('\n'); return ((num < 0) ? text : text.Substring(0, num)).Trim(); } private static string ExtractInlineStack(string condition) { int num = condition.IndexOf('\n'); if (num >= 0) { return condition.Substring(num + 1); } return ""; } private static string ExceptionType(string firstLine) { Match match = Regex.Match(firstLine, "^([\\w\\.]*(?:Exception|Error))\\b"); if (!match.Success) { return null; } return match.Groups[1].Value; } private static string Normalize(string s) { return Numbers.Replace(s, "#"); } private static string TopFrames(string stack, int count) { if (string.IsNullOrEmpty(stack)) { return ""; } string[] array = stack.Split('\n'); StringBuilder stringBuilder = new StringBuilder(); int num = 0; string[] array2 = array; foreach (string text in array2) { string text2 = Regex.Replace(text.Trim(), "\\[0x[0-9a-f]+\\]|<[0-9a-f]{32}>:\\d+|\\(at [^)]*\\)", ""); if (text2.Length != 0) { stringBuilder.Append(text2).Append(';'); if (++num == count) { break; } } } return stringBuilder.ToString(); } } internal static class Backfill { private static readonly Regex Header = new Regex("^\\[(Error|Fatal)\\s*:\\s*([^\\]]+)\\]\\s?(.*)$"); public static void FromLogOutput(string path, Diagnosis diagnosis) { string[] array; try { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using StreamReader streamReader = new StreamReader(stream); array = streamReader.ReadToEnd().Replace("\r", "").Split('\n'); } catch (Exception) { return; } for (int i = 0; i < array.Length && !array[i].Contains("[Info :Mod Doctor] Mod Doctor "); i++) { Match match = Header.Match(array[i]); if (!match.Success) { continue; } string text = match.Groups[2].Value.Trim(); string value = match.Groups[3].Value; StringBuilder stringBuilder = new StringBuilder(); int j; for (j = i + 1; j < array.Length && !array[j].StartsWith("["); j++) { if (array[j].Trim() != "Stack trace:") { stringBuilder.Append(array[j]).Append('\n'); } } diagnosis.Record(value, stringBuilder.ToString(), (text == "Unity Log") ? null : text); i = j - 1; } } } internal class ModErrorListener : ILogListener, IDisposable { private readonly Diagnosis diagnosis; public ModErrorListener(Diagnosis diagnosis) { this.diagnosis = diagnosis; } public void LogEvent(object sender, LogEventArgs e) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) if ((e.Level & 3) != 0) { string text = ((e.Source == null) ? null : e.Source.SourceName); if (!(text == "Unity Log") && !(text == "Mod Doctor")) { diagnosis.Record((e.Data == null) ? "" : e.Data.ToString(), null, text); } } } public void Dispose() { } } internal class RepeatFilter : ILogListener, IDisposable { private readonly List<ILogListener> inner; private readonly int limit; private readonly object sync = new object(); private readonly Dictionary<string, int> seen = new Dictionary<string, int>(); private readonly Dictionary<string, int> suppressed = new Dictionary<string, int>(); private float nextSummary; private RepeatFilter(List<ILogListener> inner, int limit) { this.inner = inner; this.limit = Math.Max(1, limit); } public static RepeatFilter Install(int limit) { ICollection<ILogListener> listeners = Logger.Listeners; List<ILogListener> list = listeners.Where((ILogListener l) => l is DiskLogListener || ((object)l).GetType().Name == "ConsoleLogListener").ToList(); if (list.Count == 0) { return null; } RepeatFilter repeatFilter = new RepeatFilter(list, limit); foreach (ILogListener item in list) { listeners.Remove(item); } listeners.Add((ILogListener)(object)repeatFilter); return repeatFilter; } public void LogEvent(object sender, LogEventArgs e) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) bool flag = true; if ((e.Level & 7) != 0 && e.Data != null) { string key = ((e.Source == null) ? "" : e.Source.SourceName) + "|" + e.Data; lock (sync) { seen.TryGetValue(key, out var value); value = (seen[key] = value + 1); if (value > limit) { flag = false; suppressed.TryGetValue(key, out var value2); suppressed[key] = value2 + 1; } if (seen.Count > 5000) { seen.Clear(); } } } if (!flag) { return; } foreach (ILogListener item in inner) { try { item.LogEvent(sender, e); } catch (Exception) { } } } public void FlushSummaries() { if (Time.unscaledTime < nextSummary) { return; } nextSummary = Time.unscaledTime + 60f; List<KeyValuePair<string, int>> list; lock (sync) { if (suppressed.Count == 0) { return; } list = suppressed.ToList(); suppressed.Clear(); } foreach (KeyValuePair<string, int> item in list) { string key = item.Key; int num = key.IndexOf('|'); string text = key.Substring(num + 1); int num2 = text.IndexOf('\n'); if (num2 >= 0) { text = text.Substring(0, num2); } if (text.Length > 160) { text = text.Substring(0, 160) + "..."; } ModDoctorPlugin.Log.LogInfo((object)("Suppressed " + item.Value + " more copies of [" + key.Substring(0, num) + "] " + text)); } } public void Dispose() { foreach (ILogListener item in inner) { ((IDisposable)item).Dispose(); } } } public class UpdateChecker { private readonly string cachePath; private readonly Dictionary<string, string> latest = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, DateTime> checkedAt = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase); public UpdateChecker(string cachePath) { this.cachePath = cachePath; LoadCache(); } public string Latest(ModRef mod) { if (mod == null || mod.FullPackage == null) { return null; } lock (latest) { string value; return latest.TryGetValue(mod.FullPackage, out value) ? value : null; } } public bool HasUpdate(ModRef mod) { string text = Latest(mod); string text2 = ((mod == null) ? null : (mod.PackageVersion ?? mod.Version)); if (text != null && text2 != null) { return Compare(text, text2) > 0; } return false; } public IEnumerator Run(IEnumerable<ModRef> packages, int cacheHours) { List<ModRef> todo = (from m in packages group m by m.FullPackage into g select g.First()).ToList(); foreach (ModRef mod in todo) { lock (latest) { if (checkedAt.TryGetValue(mod.FullPackage, out var when) && (DateTime.Now - when).TotalHours < (double)cacheHours) { continue; } } string url = "https://thunderstore.io/api/experimental/package/" + mod.PackageNamespace + "/" + mod.PackageName + "/"; UnityWebRequest request = UnityWebRequest.Get(url); request.timeout = 15; yield return request.SendWebRequest(); if ((int)request.result == 1) { string text = ParseLatest(request.downloadHandler.text); lock (latest) { if (text != null) { latest[mod.FullPackage] = text; } checkedAt[mod.FullPackage] = DateTime.Now; } } else if (request.responseCode == 404) { lock (latest) { checkedAt[mod.FullPackage] = DateTime.Now; } } request.Dispose(); yield return (object)new WaitForSecondsRealtime(0.4f); } SaveCache(); } private static string ParseLatest(string json) { int num = json.IndexOf("\"latest\"", StringComparison.Ordinal); if (num < 0) { return null; } Match match = Regex.Match(json.Substring(num), "\"version_number\"\\s*:\\s*\"([^\"]+)\""); if (!match.Success) { return null; } return match.Groups[1].Value; } public static int Compare(string a, string b) { string[] array = a.Split('.'); string[] array2 = b.Split('.'); for (int i = 0; i < Math.Max(array.Length, array2.Length); i++) { int result = 0; int result2 = 0; if (i < array.Length) { int.TryParse(Regex.Match(array[i], "\\d+").Value, out result); } if (i < array2.Length) { int.TryParse(Regex.Match(array2[i], "\\d+").Value, out result2); } if (result != result2) { return result.CompareTo(result2); } } return 0; } private void LoadCache() { try { if (!File.Exists(cachePath)) { return; } string[] array = File.ReadAllLines(cachePath); foreach (string text in array) { string[] array2 = text.Split('\t'); if (array2.Length == 3) { if (array2[1].Length > 0) { latest[array2[0]] = array2[1]; } if (long.TryParse(array2[2], out var result)) { checkedAt[array2[0]] = new DateTime(result); } } } } catch (Exception) { } } private void SaveCache() { try { StringBuilder stringBuilder = new StringBuilder(); lock (latest) { foreach (KeyValuePair<string, DateTime> item in checkedAt) { latest.TryGetValue(item.Key, out var value); stringBuilder.Append(item.Key).Append('\t').Append(value ?? "") .Append('\t') .Append(item.Value.Ticks) .Append('\n'); } } File.WriteAllText(cachePath, stringBuilder.ToString()); } catch (Exception) { } } public IEnumerable<KeyValuePair<ModRef, string>> Updates(IEnumerable<ModRef> mods) { foreach (ModRef mod in mods) { if (HasUpdate(mod)) { yield return new KeyValuePair<ModRef, string>(mod, Latest(mod)); } } } } public static class ReportWriter { public static string Render(List<Problem> problems, UpdateChecker updates, int pluginCount, string lang, string header, bool rich) { string text = Render(problems, updates, pluginCount, lang, header); if (!rich) { return text; } return Colorize(text, lang); } private static string Colorize(string plain, string lang) { string value = Text.T("update", lang).Split('{')[0]; List<string> list = new List<string>(); string[] array = new string[6] { "patch", "incompatible", "repeating", "error", "game", "gamerepeating" }; foreach (string text in array) { list.Add(" - " + Text.T("kind_" + text, lang)); } HashSet<string> hashSet = new HashSet<string>(); string[] array2 = new string[6] { "patch", "incompatible", "repeating", "error", "game", "gamerepeating" }; foreach (string text2 in array2) { hashSet.Add(Text.T("hint_" + text2, lang)); } hashSet.Add(Text.T("likely", lang)); StringBuilder stringBuilder = new StringBuilder(); bool flag = false; string[] array3 = plain.Split('\n'); foreach (string line in array3) { string text3 = line.Trim(); if (line.StartsWith("== ")) { flag = line.Contains(Text.T("updates", lang)); stringBuilder.Append("\n<size=16><b><color=#FFD27A>").Append(line).Append("</color></b></size>"); } else if (text3.StartsWith(value) || (flag && line.StartsWith(" - "))) { stringBuilder.Append("<color=#8FE08F>").Append(line).Append("</color>"); } else if (list.Any((string value2) => line.StartsWith(value2))) { stringBuilder.Append("<b><color=#FFA366>").Append(line).Append("</color></b>"); } else if (hashSet.Contains(text3)) { stringBuilder.Append("<i><color=#A8A29A>").Append(line).Append("</color></i>"); } else { stringBuilder.Append(line); } stringBuilder.Append('\n'); } return stringBuilder.ToString(); } public static string Render(List<Problem> problems, UpdateChecker updates, int pluginCount, string lang, string header) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Mod Doctor ").Append("1.0.1").Append(" - ") .Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm")) .Append('\n'); if (header != null) { stringBuilder.Append(header).Append('\n'); } stringBuilder.Append(Text.T("summary", lang).Replace("{plugins}", pluginCount.ToString())).Append("\n\n"); List<Problem> list = problems.Where((Problem p) => p.Kind == "engine").ToList(); problems = problems.Where((Problem p) => p.Kind != "engine").ToList(); List<IGrouping<string, Problem>> list2 = (from p in problems group p by (p.Mod == null) ? ((p.PatchedBy.Count <= 0) ? "" : "?") : p.Mod.Name into g orderby (!(g.Key == "")) ? Severity(g) : (-1) descending select g).ToList(); if (problems.Count == 0) { stringBuilder.Append(Text.T("noproblems", lang)).Append("\n\n"); } HashSet<string> hashSet = new HashSet<string>(); foreach (IGrouping<string, Problem> item in list2) { Problem problem = item.First(); if (item.Key == "") { stringBuilder.Append("== ").Append(Text.T("unknown", lang)).Append(" ==\n"); } else if (item.Key == "?") { stringBuilder.Append("== ").Append(Text.T("suspects", lang)).Append(" ==\n"); } else { hashSet.Add(item.Key); stringBuilder.Append("== ").Append(problem.Mod.Display); if (problem.Mod.FullPackage != null) { stringBuilder.Append(" (").Append(problem.Mod.FullPackage).Append(')'); } stringBuilder.Append(" ==\n"); if (updates != null && updates.HasUpdate(problem.Mod)) { stringBuilder.Append(" ").Append(Text.T("update", lang).Replace("{v}", updates.Latest(problem.Mod))).Append('\n'); } } foreach (Problem item2 in item.OrderByDescending((Problem x) => x.Count)) { stringBuilder.Append(" - ").Append(Text.T("kind_" + Kind(item2), lang)); stringBuilder.Append(" (").Append(item2.Count.ToString("N0")).Append('x'); if (item2.PerSecond >= 5.0) { stringBuilder.Append(", ~").Append(Math.Round(item2.PerSecond)).Append("/s"); } stringBuilder.Append(")\n"); stringBuilder.Append(" ").Append(item2.FirstLine).Append('\n'); if (item2.Location != null) { stringBuilder.Append(" ").Append(Text.T("where", lang)).Append(' ') .Append(item2.Location) .Append('\n'); } if (item2.PatchedBy.Count > 0) { stringBuilder.Append(" ").Append(Text.T("patchedby", lang)).Append(' ') .Append(string.Join(", ", item2.PatchedBy.Select((ModRef m) => m.Display).ToArray())) .Append('\n'); } if (item2.LikelyFromOtherFailure) { stringBuilder.Append(" ").Append(Text.T("likely", lang)).Append('\n'); } stringBuilder.Append(" ").Append(Text.T("hint_" + Kind(item2), lang)).Append('\n'); } stringBuilder.Append('\n'); } if (list.Count > 0) { stringBuilder.Append("== ").Append(Text.T("engine", lang)).Append(" ==\n"); foreach (Problem item3 in list.OrderByDescending((Problem x) => x.Count)) { stringBuilder.Append(" - ").Append((item3.FirstLine.Length > 140) ? (item3.FirstLine.Substring(0, 140) + "...") : item3.FirstLine).Append(" (") .Append(item3.Count.ToString("N0")) .Append("x)\n"); } stringBuilder.Append('\n'); } if (updates != null) { List<string> list3 = new List<string>(); foreach (KeyValuePair<ModRef, string> item4 in updates.Updates(AllMods(problems))) { if (!hashSet.Contains(item4.Key.Name)) { list3.Add(" - " + item4.Key.Display + " -> " + item4.Value); } } if (list3.Count > 0) { stringBuilder.Append("== ").Append(Text.T("updates", lang)).Append(" ==\n"); foreach (string item5 in list3) { stringBuilder.Append(item5).Append('\n'); } } } return stringBuilder.ToString(); } private static IEnumerable<ModRef> AllMods(List<Problem> problems) { return from m in ModDoctorPlugin.Instance.Diagnosis.Packages() group m by m.FullPackage into g select g.First(); } private static int Severity(IGrouping<string, Problem> g) { int num = 0; foreach (Problem item in g) { string text = Kind(item); num += ((text == "patch" || text == "incompatible") ? 1000000 : 0); num += Math.Min(item.Count, 999999); } return num; } public static string Kind(Problem p) { if ((p.Kind == "error" || p.Kind == "game") && (p.Count >= 100 || p.PerSecond >= 5.0)) { if (!(p.Kind == "game")) { return "repeating"; } return "gamerepeating"; } return p.Kind ?? "error"; } public static string Serialize(List<Problem> problems, UpdateChecker updates, int max) { StringBuilder stringBuilder = new StringBuilder(); foreach (Problem item in (from x in problems where x.Kind != "engine" orderby x.Count descending select x).Take(max)) { ModRef mod = item.Mod; stringBuilder.Append(Kind(item)).Append('\t').Append(Clean((mod == null) ? "" : mod.Name)) .Append('\t') .Append(Clean((mod == null) ? "" : (mod.PackageVersion ?? mod.Version))) .Append('\t') .Append(Clean((mod == null) ? "" : (mod.FullPackage ?? ""))) .Append('\t') .Append(Clean((mod != null && updates != null && updates.HasUpdate(mod)) ? updates.Latest(mod) : "")) .Append('\t') .Append(item.Count) .Append('\t') .Append(Math.Round(item.PerSecond)) .Append('\t') .Append(Clean(item.Location ?? "")) .Append('\t') .Append(Clean(string.Join(", ", item.PatchedBy.Select((ModRef x) => x.Display).ToArray()))) .Append('\t') .Append(Clean(item.FirstLine)) .Append('\n'); } return stringBuilder.ToString(); } private static string Clean(string s) { s = s.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); if (s.Length <= 300) { return s; } return s.Substring(0, 300); } } public static class ServerStore { private class SummaryRow { public string Mod; public List<string> Players = new List<string>(); public HashSet<string> Kinds = new HashSet<string>(); public HashSet<string> Versions = new HashSet<string>(); public int Count; public string Latest; public string Sample; } private static readonly object Sync = new object(); public static void Save(string outputDir, string player, string clientVersion, int plugins, string body, string lang) { lock (Sync) { string text = Path.Combine(outputDir, "players"); Directory.CreateDirectory(text); string text2 = Regex.Replace(player, "[^\\w\\- ]", "_").Trim(); if (text2.Length == 0) { text2 = "unknown"; } string text3 = "#player\t" + player.Replace('\t', ' ') + "\n#time\t" + DateTime.Now.ToString("yyyy-MM-dd HH:mm") + "\n#moddoctor\t" + clientVersion + "\n#plugins\t" + plugins + "\n"; File.WriteAllText(Path.Combine(text, text2 + ".tsv"), text3 + body, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); WriteSummary(outputDir, text, lang); } } private static void WriteSummary(string outputDir, string dir, string lang) { Dictionary<string, SummaryRow> dictionary = new Dictionary<string, SummaryRow>(); List<string> list = new List<string>(); string[] files = Directory.GetFiles(dir, "*.tsv"); foreach (string path in files) { string text = Path.GetFileNameWithoutExtension(path); string text2 = ""; string[] array = File.ReadAllLines(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); foreach (string text3 in array) { if (text3.StartsWith("#player\t")) { text = text3.Substring(8); } else if (text3.StartsWith("#time\t")) { text2 = text3.Substring(6); } else { if (text3.StartsWith("#")) { continue; } string[] array2 = text3.Split('\t'); if (array2.Length >= 10) { string text4 = ((array2[1].Length > 0) ? array2[1] : ((array2[8].Length > 0) ? (Text.T("suspectsof", lang) + " " + array2[8]) : Text.T("gamecode", lang))); if (!dictionary.TryGetValue(text4, out var value)) { value = new SummaryRow(); value.Mod = text4; dictionary[text4] = value; } if (!value.Players.Contains(text)) { value.Players.Add(text); } int.TryParse(array2[5], out var result); value.Count += result; value.Kinds.Add(array2[0]); if (array2[2].Length > 0) { value.Versions.Add(array2[2]); } if (array2[4].Length > 0) { value.Latest = array2[4]; } if (value.Sample == null) { value.Sample = array2[9] + ((array2[7].Length > 0) ? (" @ " + array2[7]) : ""); } } } } list.Add(text + " (" + text2 + ")"); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Mod Doctor - ").Append(Text.T("serversummary", lang)).Append(" - ") .Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm")) .Append('\n'); stringBuilder.Append(Text.T("reportsfrom", lang)).Append(' ').Append(string.Join(", ", list.ToArray())) .Append("\n\n"); foreach (SummaryRow item in from r in dictionary.Values orderby r.Players.Count descending, r.Count descending select r) { stringBuilder.Append("== ").Append(item.Mod); if (item.Versions.Count > 0) { stringBuilder.Append(' ').Append(string.Join("/", item.Versions.ToArray())); } stringBuilder.Append(" ==\n"); stringBuilder.Append(" ").Append(Text.T("affected", lang)).Append(' ') .Append(item.Players.Count) .Append(": ") .Append(string.Join(", ", item.Players.ToArray())) .Append('\n'); stringBuilder.Append(" ").Append(Text.T("occurrences", lang)).Append(' ') .Append(item.Count.ToString("N0")) .Append(" [") .Append(string.Join(", ", item.Kinds.Select((string k) => Text.T("kind_" + k, lang)).ToArray())) .Append("]\n"); if (item.Latest != null) { stringBuilder.Append(" ").Append(Text.T("update", lang).Replace("{v}", item.Latest)).Append('\n'); } if (item.Sample != null) { stringBuilder.Append(" ").Append(item.Sample).Append('\n'); } stringBuilder.Append('\n'); } if (dictionary.Count == 0) { stringBuilder.Append(Text.T("noproblems", lang)).Append('\n'); } File.WriteAllText(Path.Combine(outputDir, "summary.txt"), stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } } public static class Text { private static readonly Dictionary<string, string[]> Words = new Dictionary<string, string[]> { { "summary", new string[2] { "{plugins} plugins loaded. Problems are grouped by the mod that most likely caused them.", "{plugins} plugins carregados. Os problemas estão agrupados pelo mod que mais provavelmente os causou." } }, { "noproblems", new string[2] { "No mod errors so far.", "Nenhum erro de mod até agora." } }, { "unknown", new string[2] { "Errors not traced to a mod", "Erros sem mod identificado" } }, { "suspects", new string[2] { "Errors inside a vanilla method patched by several mods (see suspects)", "Erros dentro de um método do jogo alterado por vários mods (veja os suspeitos)" } }, { "update", new string[2] { "Update available on Thunderstore: {v}", "Atualização disponível no Thunderstore: {v}" } }, { "updates", new string[2] { "Other updates available", "Outras atualizações disponíveis" } }, { "where", new string[2] { "Where:", "Onde:" } }, { "likely", new string[2] { "Pinned on this mod because it is the only one of them that also failed to load part of itself.", "Atribuído a este mod porque ele é o único desses que também falhou ao carregar parte de si." } }, { "patchedby", new string[2] { "Mods patching this method:", "Mods que alteram este método:" } }, { "kind_patch", new string[2] { "Patch failed to apply", "Patch falhou ao ser aplicado" } }, { "kind_incompatible", new string[2] { "Incompatible with this game version", "Incompatível com esta versão do jogo" } }, { "kind_repeating", new string[2] { "Error repeating constantly", "Erro se repetindo sem parar" } }, { "kind_error", new string[2] { "Error", "Erro" } }, { "kind_game", new string[2] { "Error in Valheim's own code", "Erro no código do próprio Valheim" } }, { "kind_gamerepeating", new string[2] { "Error in Valheim's own code, repeating", "Erro no código do próprio Valheim, se repetindo" } }, { "hint_game", new string[2] { "No mod appears in the trace. Usually a game bug, or a mod leaving bad data behind; watch whether it follows a specific action.", "Nenhum mod aparece no rastro do erro. Normalmente é bug do jogo, ou um mod deixando dados ruins; veja se acontece depois de alguma ação específica." } }, { "hint_gamerepeating", new string[2] { "No mod appears in the trace, but it keeps firing. If it started after adding or updating a mod, that mod is the first suspect.", "Nenhum mod aparece no rastro, mas continua disparando. Se começou depois de instalar ou atualizar algum mod, ele é o primeiro suspeito." } }, { "engine", new string[2] { "Engine messages with no mod involved (usually harmless)", "Mensagens do motor do jogo, sem mod envolvido (normalmente inofensivas)" } }, { "suspectsof", new string[2] { "Suspects:", "Suspeitos:" } }, { "gamecode", new string[2] { "Valheim's own code (no mod in the trace)", "Código do próprio Valheim (nenhum mod no rastro)" } }, { "hint_patch", new string[2] { "Part of this mod did not load, usually because it was built for another Valheim version. Update it or remove it if nothing breaks without it.", "Parte deste mod não carregou, normalmente porque foi feito para outra versão do Valheim. Atualize, ou remova se nada fizer falta." } }, { "hint_incompatible", new string[2] { "The mod expects game code that no longer exists (built for an older Valheim). Update it; if there is no update, remove it.", "O mod espera um código do jogo que não existe mais (feito para um Valheim mais antigo). Atualize; se não houver atualização, remova." } }, { "hint_repeating", new string[2] { "A bug that fires every frame: it floods the log and costs performance. Check for an update; if none, disable the feature involved or remove the mod.", "Um bug que dispara a cada frame: enche o log e custa desempenho. Procure atualização; se não houver, desligue o recurso envolvido ou remova o mod." } }, { "hint_error", new string[2] { "Occasional error. Worth reporting to the mod author if it keeps happening.", "Erro ocasional. Vale relatar ao autor do mod se continuar acontecendo." } }, { "toast", new string[2] { "Mod Doctor: {mod} is causing errors. Press {key} for details.", "Mod Doctor: {mod} está causando erros. Aperte {key} para ver detalhes." } }, { "toastmany", new string[2] { "Mod Doctor: {mod} are causing errors. Press {key} for details.", "Mod Doctor: {mod} estão causando erros. Aperte {key} para ver detalhes." } }, { "refresh", new string[2] { "Refresh", "Atualizar" } }, { "copy", new string[2] { "Copy", "Copiar" } }, { "folder", new string[2] { "Open folder", "Abrir pasta" } }, { "close", new string[2] { "Close", "Fechar" } }, { "serversummary", new string[2] { "player summary", "resumo dos jogadores" } }, { "reportsfrom", new string[2] { "Reports from:", "Relatórios de:" } }, { "affected", new string[2] { "Players affected", "Jogadores afetados" } }, { "occurrences", new string[2] { "Occurrences:", "Ocorrências:" } } }; public static string T(string key, string lang) { if (!Words.TryGetValue(key, out var value)) { return key; } if (!(lang == "pt")) { return value[0]; } return value[1]; } }