diff --git a/src/CommunityPatch/CommunityPatch.csproj b/src/CommunityPatch/CommunityPatch.csproj index 33f9930..2e831f1 100644 --- a/src/CommunityPatch/CommunityPatch.csproj +++ b/src/CommunityPatch/CommunityPatch.csproj @@ -2,6 +2,7 @@ netstandard2.0 + True MSB3277 Release;Debug X64 @@ -27,6 +28,22 @@ %(Identity) False + + %(Identity) + False + + + %(Identity) + False + + + %(Identity) + False + + + %(Identity) + False + @@ -34,4 +51,13 @@ SubModule.xml + + + + + + + + + diff --git a/src/CommunityPatch/CommunityPatchSubModule.CopyDiagnosticsToClipboard.cs b/src/CommunityPatch/CommunityPatchSubModule.CopyDiagnosticsToClipboard.cs new file mode 100644 index 0000000..db36799 --- /dev/null +++ b/src/CommunityPatch/CommunityPatchSubModule.CopyDiagnosticsToClipboard.cs @@ -0,0 +1,149 @@ +using System; +using System.Runtime; +using System.Runtime.InteropServices; +using System.Text; +using HardwareProviders; +using Helpers; +using TaleWorlds.Core; +using TaleWorlds.Engine; +using TaleWorlds.InputSystem; +using TaleWorlds.Library; +using TaleWorlds.Localization; +using TaleWorlds.MountAndBlade; +using TaleWorlds.MountAndBlade.GauntletUI; + +namespace CommunityPatch { + + public partial class CommunityPatchSubModule { + + public static void CopyDiagnosticsToClipboard() { + var sb = new StringBuilder(); + + try { + sb.AppendLine("Recorded Unhandled Exceptions:"); + var i = 0; + foreach (var exc in RecordedUnhandledExceptions) { + var excStr = RecordedUnhandledExceptions.ToString(); + sb.Append(" ").Append(++i).Append(". ").AppendLine(excStr.Replace("\n", "\n ")); + } + + if (i == 0) + sb.AppendLine(" None."); + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + sb.AppendLine(); + + try { + sb.AppendLine("Recorded First Chance Exceptions:"); + var i = 0; + foreach (var exc in RecordedFirstChanceExceptions) { + var excStr = RecordedFirstChanceExceptions.ToString(); + sb.Append(" ").Append(++i).Append(". ").AppendLine(excStr.Replace("\n", "\n ")); + } + + if (RecordFirstChanceExceptions) { + if (i == 0) + sb.AppendLine(" None recorded."); + } + else { + sb.AppendLine(" Recording disabled."); + } + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + sb.AppendLine(); + + try { + sb.AppendLine("Modules Information:"); + var i = 0; + foreach (var mi in ModuleInfo.GetModules()) { + sb.Append(" ").Append(++i).Append(". ").Append(mi.Id).Append(" ").Append(mi.Version.ToString()); + if (mi.IsSelected) + sb.Append(" *Selected*"); + sb.AppendLine(); + sb.Append(" ").Append(mi.Name); + if (!string.IsNullOrWhiteSpace(mi.Alias)) + sb.Append(" (").Append(mi.Alias).Append(")"); + if (mi.IsOfficial) + sb.Append(" *Official*"); + sb.AppendLine(); + if (mi.DependedModuleIds.Count <= 0) + continue; + + sb.Append(" ").AppendLine("Dependencies:"); + var j = 0; + foreach (var dep in mi.DependedModuleIds) + sb.Append(" ").Append(++j).Append(". ").AppendLine(dep); + } + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + sb.AppendLine(); + + try { + sb.AppendLine("Loaded SubModules:"); + var i = 0; + foreach (var sm in Module.CurrentModule.SubModules) + sb.Append(" ").Append(++i).Append(". ").AppendLine(sm.GetType().AssemblyQualifiedName); + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + sb.AppendLine(); + + try { + sb.AppendLine("System Info:"); + sb.Append(" ").AppendLine(Utilities.GetPCInfo().Replace("\n", "\n ")); + sb.Append($" GPU Memory: {Utilities.GetGPUMemoryMB()}MB").AppendLine(); + sb.Append($" GC Allocated: {GC.GetTotalMemory(false)}B").AppendLine(); + sb.Append($" Engine Memory Used: {Utilities.GetCurrentCpuMemoryUsageMB()}MB").AppendLine(); + sb.Append(" GC Latency Mode: ").Append(GCSettings.IsServerGC ? "Server " : "Client ").AppendLine(GCSettings.LatencyMode.ToString()); + sb.AppendFormat($" GC LOH Compact Mode: {GCSettings.LargeObjectHeapCompactionMode}").AppendLine(); + sb.AppendFormat($" Operating System: {RuntimeInformation.OSDescription}").AppendLine(); + sb.AppendFormat($" Framework Compatibility: {RuntimeInformation.FrameworkDescription}").AppendLine(); + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + try { + var cpus = HardwareProviders.CPU.Cpu.Discover(); + + sb.AppendLine(" CPU Info:"); + for (var i = 0; i < cpus.Length; ++i) { + var cpu = cpus[i]; + var coreCount = cpu.CoreClocks.Length; + sb.Append(" ").Append(i + 1).Append(". ").Append(cpu.Name).Append(" with ").Append(coreCount).AppendLine(" cores:"); + for (var c = 0; c < coreCount; ++c) { + sb.Append(" ").Append(c + 1).Append(". ").Append(cpu.CoreClocks[c].Value).Append("MHz ") + .Append(cpu.CoreTemperatures[c].Value).Append("°C"); + sb.AppendLine(); + } + } + } + catch (Exception ex) { + sb.Append(" *** ERROR: ").Append(ex.GetType().Name).Append(": ").AppendLine(ex.Message); + } + + sb.AppendLine(); + + try { + Input.SetClipboardText(sb.ToString()); + ShowMessage("Diagnostics copied to system clipboard."); + } + catch (Exception ex) { + ShowMessage($"Writing to system clipboard failed!\n{ex.GetType().Name}: {ex.Message}"); + } + } + + } + +} \ No newline at end of file diff --git a/src/CommunityPatch/CommunityPatchSubModule.Logging.cs b/src/CommunityPatch/CommunityPatchSubModule.Logging.cs index 5bc9d76..f7e812b 100644 --- a/src/CommunityPatch/CommunityPatchSubModule.Logging.cs +++ b/src/CommunityPatch/CommunityPatchSubModule.Logging.cs @@ -9,8 +9,9 @@ public partial class CommunityPatchSubModule { [PublicAPI] [Conditional("TRACE")] - public static void Error(Exception ex, FormattableString msg = null) { - Error(msg); + public static void Error(Exception ex, string msg = null) { + if (msg != null) + Error(msg); var st = new StackTrace(ex, true); var f = st.GetFrame(0); @@ -24,12 +25,21 @@ public static void Error(Exception ex, FormattableString msg = null) { [PublicAPI] [Conditional("TRACE")] - public static void Error(FormattableString msg = null) { + public static void Error(Exception ex, FormattableString msg) + => Error(ex, FormattableString.Invariant(msg)); + + [PublicAPI] + [Conditional("TRACE")] + public static void Error(FormattableString msg) + => Error(FormattableString.Invariant(msg)); + + [PublicAPI] + [Conditional("TRACE")] + public static void Error(string msg = null) { if (msg == null) return; - var msgStr = FormattableString.Invariant(msg); - Debugger.Log(3, "CommunityPatch", msgStr); + Debugger.Log(3, "CommunityPatch", msg); } [PublicAPI] diff --git a/src/CommunityPatch/CommunityPatchSubModule.cs b/src/CommunityPatch/CommunityPatchSubModule.cs index 5bf87fc..124c56e 100644 --- a/src/CommunityPatch/CommunityPatchSubModule.cs +++ b/src/CommunityPatch/CommunityPatchSubModule.cs @@ -1,21 +1,136 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using JetBrains.Annotations; using TaleWorlds.Core; +using TaleWorlds.Localization; using TaleWorlds.MountAndBlade; +using Module = TaleWorlds.MountAndBlade.Module; namespace CommunityPatch { [PublicAPI] public partial class CommunityPatchSubModule : MBSubModuleBase { + internal static readonly LinkedList RecordedFirstChanceExceptions + = new LinkedList(); + + internal static readonly LinkedList RecordedUnhandledExceptions + = new LinkedList(); + + internal static readonly OptionsFile Options = new OptionsFile("CommunityPatch.txt"); + + internal static bool DisableIntroVideo { + get => Options.Get(nameof(DisableIntroVideo)); + set => Options.Set(nameof(DisableIntroVideo), value); + } + + internal static bool RecordFirstChanceExceptions { + get => Options.Get(nameof(RecordFirstChanceExceptions)); + set => Options.Set(nameof(RecordFirstChanceExceptions), value); + } + + public override void BeginGameStart(Game game) + => base.BeginGameStart(game); + + public override bool DoLoading(Game game) + => base.DoLoading(game); + + protected override void OnBeforeInitialModuleScreenSetAsRoot() { + var module = Module.CurrentModule; + + { + // remove the space option that DeveloperConsole module adds + var spaceOpt = module.GetInitialStateOptionWithId("Space"); + if (spaceOpt != null) { + var opts = module.GetInitialStateOptions() + .Where(opt => opt != spaceOpt).ToArray(); + module.ClearStateOptions(); + foreach (var opt in opts) + module.AddInitialStateOption(opt); + } + } + + if (DisableIntroVideo) { + try { + typeof(Module) + .GetField("_splashScreenPlayed", BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(module, true); + } + catch (Exception ex) { + Error(ex, "Couldn't disable intro video."); + } + } + + base.OnBeforeInitialModuleScreenSetAsRoot(); + } + + protected override void OnSubModuleLoad() { + var module = Module.CurrentModule; + module.AddInitialStateOption(new InitialStateOption( + "ModOptions", + new TextObject("Mod Options"), + 10001, + ShowModOptions, + false + )); + base.OnSubModuleLoad(); + } + + private static void ShowMessage(string msg) + => InformationManager.DisplayMessage(new InformationMessage(msg)); + + private void ShowModOptions() + => InformationManager.ShowMultiSelectionInquiry(new MultiSelectionInquiryData( + "Mod Options", + "Community Patch Mod Options:", + new List { + new InquiryElement( + nameof(DisableIntroVideo), + DisableIntroVideo ? "Enable Intro Videos" : "Disable Intro Videos", + null + ), + new InquiryElement( + nameof(RecordedFirstChanceExceptions), + DisableIntroVideo ? "Record First Chance Exceptions" : "Ignore First Chance Exceptions", + null + ), + new InquiryElement( + nameof(CopyDiagnosticsToClipboard), + "Copy Diagnostics to Clipboard", + null + ) + }, + true, + true, + "Apply", + "Return", + list => { + var selected = (string) list[0].Identifier; + switch (selected) { + case nameof(DisableIntroVideo): + DisableIntroVideo = !DisableIntroVideo; + ShowMessage($"Intro Videos: {(DisableIntroVideo ? "Disabled" : "Enabled")}."); + Options.Save(); + break; + case nameof(RecordFirstChanceExceptions): + RecordFirstChanceExceptions = !RecordFirstChanceExceptions; + ShowMessage($"Record FCEs: {(RecordFirstChanceExceptions ? "Disabled" : "Enabled")}."); + Options.Save(); + break; + case nameof(CopyDiagnosticsToClipboard): + CopyDiagnosticsToClipboard(); + break; + default: + throw new NotImplementedException(selected); + } + }, null)); + public override void OnGameInitializationFinished(Game game) { var patchType = typeof(IPatch); var patches = new LinkedList(); - Print("BeginGameStart"); - foreach (var type in typeof(CommunityPatchSubModule).Assembly.GetTypes()) { if (!patchType.IsAssignableFrom(type)) continue; diff --git a/src/CommunityPatch/FodyWeavers.xml b/src/CommunityPatch/FodyWeavers.xml new file mode 100644 index 0000000..cb9dc50 --- /dev/null +++ b/src/CommunityPatch/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/CommunityPatch/FodyWeavers.xsd b/src/CommunityPatch/FodyWeavers.xsd new file mode 100644 index 0000000..e24ec01 --- /dev/null +++ b/src/CommunityPatch/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/src/CommunityPatch/ModuleInitializer.cs b/src/CommunityPatch/ModuleInitializer.cs new file mode 100644 index 0000000..2027309 --- /dev/null +++ b/src/CommunityPatch/ModuleInitializer.cs @@ -0,0 +1,16 @@ +using System; +using static CommunityPatch.CommunityPatchSubModule; + +internal static class ModuleInitializer { + + public static void Initialize() { + AppDomain.CurrentDomain.FirstChanceException += (sender, args) => { + if (RecordFirstChanceExceptions) + RecordedFirstChanceExceptions.AddLast(args.Exception); + }; + AppDomain.CurrentDomain.UnhandledException += (sender, args) => { + RecordedUnhandledExceptions.AddLast((Exception) args.ExceptionObject); + }; + } + +} \ No newline at end of file diff --git a/src/CommunityPatch/OptionsFile.cs b/src/CommunityPatch/OptionsFile.cs new file mode 100644 index 0000000..c1f794f --- /dev/null +++ b/src/CommunityPatch/OptionsFile.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using JetBrains.Annotations; +using TaleWorlds.Engine; +using Tomlyn; +using Tomlyn.Syntax; +using Path = System.IO.Path; + +namespace CommunityPatch { + + public class OptionsFile { + + private readonly string _path; + + private readonly DocumentSyntax _toml; + + [PublicAPI] + public OptionsFile(string fileName) { + _path = Path.Combine(Utilities.GetConfigsPath(), fileName); + if (!File.Exists(_path)) { + _toml = new DocumentSyntax(); + return; + } + + var bytes = File.ReadAllBytes(_path); + _toml = Toml.Parse(bytes, _path); + } + + [PublicAPI] + public void Save() { + using var sw = new StreamWriter(_path, false, Encoding.UTF8, 65536); + _toml.WriteTo(sw); + } + + [PublicAPI] + [CanBeNull] + public KeyValueSyntax GetConfig([NotNull] string key) + => _toml.KeyValues + .FirstOrDefault(kv => kv.Key.Key.ToString().Trim() == key); + + [PublicAPI] + [NotNull] + public KeyValueSyntax GetOrCreateConfig([NotNull] string key) { + var kvs = GetConfig(key); + if (kvs != null) + return kvs; + + kvs = new KeyValueSyntax(key, new BareKeySyntax()); + _toml.KeyValues.Add(kvs); + + return kvs; + } + + [PublicAPI] + public void DeleteConfig([NotNull] string key) + => _toml.KeyValues.RemoveChildren(GetConfig(key)); + + public void Set(string key, T value) { + var cfg = GetOrCreateConfig(key); + long iVal; + switch (value) { + // @formatter:off + case bool v: cfg.Value = new BooleanValueSyntax(v); break; + case string v: cfg.Value = new StringValueSyntax(v); break; + case float v: cfg.Value = new FloatValueSyntax(v); break; + case double v: cfg.Value = new FloatValueSyntax(v); break; + case sbyte v: iVal = v; goto setIVal; + case short v: iVal = v; goto setIVal; + case int v: iVal = v; goto setIVal; + case long v: iVal = v; goto setIVal; + case byte v: iVal = v; goto setIVal; + case ushort v: iVal = v; goto setIVal; + case uint v: iVal = v; goto setIVal; + case ulong v: iVal = (long)v; goto setIVal; + // @formatter:on + default: + throw new NotImplementedException(typeof(T).FullName); + } + + return; + + setIVal: + cfg.Value = new IntegerValueSyntax(iVal); + } + + public T Get(string key) { + var cfg = GetConfig(key); + if (cfg == null) + return default; + + var t = typeof(T); + + if (t == typeof(string)) { + string v; + if (cfg.Value is StringValueSyntax svs) + v = svs.Value; + else + throw new NotImplementedException(cfg.Value.GetType().FullName); + + return Unsafe.As(ref v); + } + + if (t == typeof(bool)) { + var v = ReadBoolean(cfg); + return Unsafe.As(ref v); + } + + if (t == typeof(sbyte) || t == typeof(short) || t == typeof(int) || t == typeof(long)) { + var v = ReadInteger(cfg); + return Unsafe.As(ref v); + } + + if (t == typeof(byte) || t == typeof(ushort) || t == typeof(uint) || t == typeof(ulong)) { + var v = unchecked((ulong) ReadInteger(cfg)); + return Unsafe.As(ref v); + } + + if (t == typeof(float)) { + var v = (float) ReadDouble(cfg); + return Unsafe.As(ref v); + } + + if (t == typeof(double)) { + var v = ReadDouble(cfg); + return Unsafe.As(ref v); + } + + throw new NotImplementedException(t.FullName); + } + + private static bool ReadBoolean(KeyValueSyntax cfg) { + var tk = ((BooleanValueSyntax) cfg.Value).Token.TokenKind; + return tk switch { + TokenKind.True => true, + TokenKind.False => false, + _ => throw new NotImplementedException(tk.ToString()) + }; + } + + private static long ReadInteger(KeyValueSyntax cfg) + => long.Parse(((IntegerValueSyntax) cfg.Value).Token.Text); + + private static double ReadDouble(KeyValueSyntax cfg) { + var st = ((FloatValueSyntax) cfg.Value).Token; + + return st.TokenKind switch { + TokenKind.Float => double.Parse(st.Text), + TokenKind.Integer => long.Parse(st.Text), + TokenKind.Nan => double.NaN, + TokenKind.PositiveNan => double.NaN, + TokenKind.NegativeNan => double.NaN, + TokenKind.Infinite => double.PositiveInfinity, + TokenKind.PositiveInfinite => double.PositiveInfinity, + TokenKind.NegativeInfinite => double.NegativeInfinity, + _ => throw new NotImplementedException(st.TokenKind.ToString()) + }; + } + + } + +} \ No newline at end of file