diff --git a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs index 89d152c..8b5f43a 100644 --- a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs +++ b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs @@ -49,6 +49,7 @@ namespace Aki.SinglePlayer new EmptyInfilFixPatch().Enable(); new SmokeGrenadeFuseSoundFixPatch().Enable(); new PlayerToggleSoundFixPatch().Enable(); + new PluginErrorNotifierPatch().Enable(); } catch (Exception ex) { diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs new file mode 100644 index 0000000..e4a2547 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs @@ -0,0 +1,78 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using BepInEx.Bootstrap; +using EFT.Communications; +using EFT.UI; +using HarmonyLib; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + /*** + * On the first show of the main menu, check if any BepInEx plugins have failed to load, and inform + * the user. This is done via a toast in the bottom right, with a more detailed console message + **/ + internal class PluginErrorNotifierPatch : ModulePatch + { + private static MethodInfo _displayMessageNotificationMethod; + private static MethodInfo _directLogMethod; + private static bool _messageShown = false; + + protected override MethodBase GetTargetMethod() + { + _displayMessageNotificationMethod = AccessTools.Method(typeof(NotificationManagerClass), "DisplayMessageNotification"); + _directLogMethod = AccessTools.Method(typeof(ConsoleScreen), "method_5"); + + var desiredType = typeof(MenuScreen); + var desiredMethod = desiredType.GetMethod("Show", PatchConstants.PrivateFlags); + return desiredMethod; + } + + [PatchPostfix] + private static void PatchPostfix() + { + var failedPluginCount = Chainloader.DependencyErrors.Count; + + // Skip if we've already shown the message, or there are no errors + if (_messageShown || failedPluginCount == 0) + { + return; + } + + // Show a toast in the bottom right of the screen indicating how many plugins failed to load + var consoleHeaderMessage = $"{failedPluginCount} plugin{(failedPluginCount > 1 ? "s" : "")} failed to load due to errors"; + var toastMessage = $"{consoleHeaderMessage}. Please check the console for details."; + _displayMessageNotificationMethod.Invoke(null, new object[] { toastMessage, ENotificationDurationType.Infinite, ENotificationIconType.Alert, Color.red }); + + // Build the error message we'll put in the BepInEx and in-game consoles + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine($"{consoleHeaderMessage}:"); + foreach (string error in Chainloader.DependencyErrors) + { + stringBuilder.AppendLine(error); + } + var errorMessage = stringBuilder.ToString(); + + // Show an error in the BepInEx console/log file + Logger.LogError(errorMessage); + + // Show an error in the in-game console, we have to write this in reverse order because the + // in-game console shows newer messages at the top + ConsoleScreen consoleScreen = MonoBehaviourSingleton.Instance.Console; + foreach (string line in errorMessage.Split('\n').Reverse()) + { + if (line.Length > 0) + { + // Note: We directly call the internal Log method to work around a bug in 'LogError' that passes an empty string + // as the StackTrace parameter, which results in extra newlines being added to the console logs + _directLogMethod.Invoke(consoleScreen, new object[] { line, null, LogType.Error }); + } + } + + _messageShown = true; + } + } +}