From 052f719446d1a122bf7cfdef8565dc20a9b85473 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sun, 31 Dec 2023 19:50:05 -0500 Subject: [PATCH 1/2] Show error and catch exceptions when failing to load beatmap --- .../HarmonyTranspilersFixPatch.cs | 31 ++++++++ .../StandardLevelDetailViewControllerPatch.cs | 77 +++++++++++++++++++ source/SongCore/Plugin.cs | 11 ++- source/SongCore/SongCore.csproj | 26 ++++--- .../Utilities/CodeMatcherExtensions.cs | 48 ++++++++++++ 5 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs create mode 100644 source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs create mode 100644 source/SongCore/Utilities/CodeMatcherExtensions.cs diff --git a/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs new file mode 100644 index 0000000..8f54c79 --- /dev/null +++ b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using SongCore.Utilities; + +namespace SongCore.HarmonyPatches +{ + /// + /// This patch fixes an issue with HarmonyX that causes it + /// to remove certain instructions when patching a method with a transpiler. It removes the condition in + /// that deletes existing Leave, Endfinally + /// and Endfilter instructions from the patched method when they are followed by an exception block. + /// + internal class HarmonyTranspilersFixPatch + { + public static MethodBase TargetMethod() => AccessTools.Method("HarmonyLib.Internal.Patching.ILManipulator:WriteTo"); + + public static IEnumerable Transpiler(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchStartForward( + new CodeMatch(OpCodes.Ldloc_3), + new CodeMatch(OpCodes.Ldfld), + new CodeMatch(i => i.opcode == OpCodes.Ldsfld && ((FieldInfo)i.operand).Name == nameof(OpCodes.Leave))) + .ThrowIfInvalid() + .RemoveInstructionsInRange(76, 112) + .InstructionEnumeration(); + } + } +} diff --git a/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs b/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs new file mode 100644 index 0000000..64b83b6 --- /dev/null +++ b/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using MonoMod.Utils; +using Polyglot; +using SongCore.Utilities; + +namespace SongCore.HarmonyPatches +{ + /// + /// This patch catches all exceptions and displays an error message to the user + /// in the when the game is loading beatmap levels. + /// + [HarmonyPatch] + internal class StandardLevelDetailViewControllerPatch + { + private static MethodBase TargetMethod() => AccessTools.Method(typeof(StandardLevelDetailViewController), nameof(StandardLevelDetailViewController.ShowLoadingAndDoSomething)).GetStateMachineTarget(); + + private static IEnumerable Transpiler(IEnumerable instructions) + { + var codeMatcher = new CodeMatcher(instructions) + .MatchStartForward(new CodeMatch(i => i.blocks.FirstOrDefault()?.blockType == ExceptionBlockType.BeginCatchBlock)) + .ThrowIfInvalid(); + codeMatcher.Instruction.blocks[0].catchType = typeof(Exception); + return codeMatcher + .SetOpcodeAndAdvance(OpCodes.Stloc_3) + .Insert( + new CodeInstruction(OpCodes.Ldloc_1), + new CodeInstruction(OpCodes.Ldloc_3), + Transpilers.EmitDelegate>((standardLevelDetailViewController, ex) => + { + var handled = false; + switch (ex) + { + case OperationCanceledException: + // Base game skips those. + return; + case ArgumentOutOfRangeException: + { + if (ex.StackTrace.Contains(nameof(BeatmapCharacteristicSegmentedControlController))) + { + const string errorText = "Error loading beatmap. Missing or unknown characteristic."; + standardLevelDetailViewController.ShowContent(StandardLevelDetailViewController.ContentType.Error, errorText); + Logging.Logger.Error(errorText); + handled = true; + } + + break; + } + case ArgumentNullException: + { + if (ex.StackTrace.Contains(nameof(BeatmapSaveDataHelpers.GetVersion))) + { + const string errorText = "Error loading beatmap version."; + standardLevelDetailViewController.ShowContent(StandardLevelDetailViewController.ContentType.Error, errorText); + Logging.Logger.Error(errorText); + handled = true; + } + + break; + } + } + + if (!handled) + { + standardLevelDetailViewController.ShowContent(StandardLevelDetailViewController.ContentType.Error, Localization.Get(StandardLevelDetailViewController.kLoadingDataErrorLocalizationKey)); + } + + Logging.Logger.Error(ex); + })) + .InstructionEnumeration(); + } + } +} diff --git a/source/SongCore/Plugin.cs b/source/SongCore/Plugin.cs index 5a19ddd..fc04da8 100644 --- a/source/SongCore/Plugin.cs +++ b/source/SongCore/Plugin.cs @@ -9,6 +9,7 @@ using IPA.Config; using IPA.Config.Stores; using IPA.Loader; +using SongCore.HarmonyPatches; using UnityEngine; using UnityEngine.SceneManagement; using IPALogger = IPA.Logging.Logger; @@ -19,8 +20,8 @@ namespace SongCore [Plugin(RuntimeOptions.SingleStartInit)] public class Plugin { - private static PluginMetadata _metadata; - private static Harmony? _harmony; + private readonly PluginMetadata _metadata; + private readonly Harmony _harmony; internal static SConfiguration Configuration { get; private set; } @@ -31,7 +32,7 @@ public class Plugin public static string noArrowsCharacteristicName = "NoArrows"; [Init] - public void Init(IPALogger pluginLogger, PluginMetadata metadata) + public Plugin(IPALogger pluginLogger, PluginMetadata metadata) { // Workaround for creating BSIPA config in Userdata subdir Directory.CreateDirectory(Path.Combine(UnityGame.UserDataPath, nameof(SongCore))); @@ -39,6 +40,7 @@ public void Init(IPALogger pluginLogger, PluginMetadata metadata) Logging.Logger = pluginLogger; _metadata = metadata; + _harmony = new Harmony("com.kyle1413.BeatSaber.SongCore"); } [OnStart] @@ -69,7 +71,8 @@ public void OnApplicationStart() BSMLSettings.instance.AddSettingsMenu("SongCore", "SongCore.UI.settings.bsml", new SCSettingsController()); SceneManager.activeSceneChanged += OnActiveSceneChanged; - _harmony = Harmony.CreateAndPatchAll(_metadata.Assembly, "com.kyle1413.BeatSaber.SongCore"); + _harmony.Patch(HarmonyTranspilersFixPatch.TargetMethod(), null, null, new HarmonyMethod(AccessTools.Method(typeof(HarmonyTranspilersFixPatch), nameof(HarmonyTranspilersFixPatch.Transpiler)))); + _harmony.PatchAll(_metadata.Assembly); BasicUI.GetIcons(); BS_Utils.Utilities.BSEvents.levelSelected += BSEvents_levelSelected; diff --git a/source/SongCore/SongCore.csproj b/source/SongCore/SongCore.csproj index 6d7261a..3456569 100644 --- a/source/SongCore/SongCore.csproj +++ b/source/SongCore/SongCore.csproj @@ -3,7 +3,7 @@ net472 Library - 8 + latest enable false ..\Refs @@ -28,6 +28,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\AdditionalContentModel.Interfaces.dll false + + $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll + False + $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.AppFlow.dll false @@ -36,16 +40,12 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.UnityExtension.dll false - - $(BeatSaberDir)\Plugins\BSML.dll - False - $(BeatSaberDir)\Plugins\BS_Utils.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll + + $(BeatSaberDir)\Plugins\BSML.dll False @@ -78,10 +78,18 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\Menu.ColorSettings.dll false + + $(BeatSaberDir)\Libs\MonoMod.Utils.dll + false + $(BeatSaberDir)\Libs\Newtonsoft.Json.dll False + + $(BeatSaberDir)\Beat Saber_Data\Managed\Polyglot.dll + false + $(BeatSaberDir)\Beat Saber_Data\Managed\Tweening.dll False @@ -117,10 +125,6 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll false - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Polyglot.dll - false diff --git a/source/SongCore/Utilities/CodeMatcherExtensions.cs b/source/SongCore/Utilities/CodeMatcherExtensions.cs new file mode 100644 index 0000000..9b55b08 --- /dev/null +++ b/source/SongCore/Utilities/CodeMatcherExtensions.cs @@ -0,0 +1,48 @@ +using System; +using HarmonyLib; +using IPA.Utilities; + +namespace SongCore.Utilities +{ + public static class CodeMatcherExtensions + { + private static readonly FieldAccessor.Accessor LastErrorAccessor = + FieldAccessor.GetAccessor("lastError"); + + /// Prints the list of instructions of this code matcher instance. + /// The code matcher instance. + /// The code matcher instance. + public static CodeMatcher PrintInstructions(this CodeMatcher codeMatcher) + { + var instructions = codeMatcher.Instructions(); + for (var i = 0; i < instructions.Count; i++) + { + Logging.Logger.Info($"\t {i} {instructions[i]}"); + } + + return codeMatcher; + } + + /// Throws an exception if current state is invalid (position out of bounds/last match failed). + /// The code matcher instance. + /// Optional explanation of where/why the exception was thrown that will be added to the exception message. + /// Current state is invalid. + /// The code matcher instance. + public static CodeMatcher ThrowIfInvalid(this CodeMatcher codeMatcher, string? explanation = null) + { + if (codeMatcher.IsInvalid) + { + var lastError = LastErrorAccessor(ref codeMatcher); + var errMsg = lastError; + if (!string.IsNullOrWhiteSpace(explanation)) + { + errMsg = $"{explanation} - Current state is invalid. Details: {lastError}"; + } + + throw new InvalidOperationException(errMsg); + } + + return codeMatcher; + } + } +} From e01b3b6392700d761cb3c7b61c48b7a285c5c6fb Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:32:21 -0500 Subject: [PATCH 2/2] Snap a couple of TODO --- source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs | 1 + .../HarmonyPatches/StandardLevelDetailViewControllerPatch.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs index 8f54c79..d50922e 100644 --- a/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs +++ b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs @@ -12,6 +12,7 @@ namespace SongCore.HarmonyPatches /// that deletes existing Leave, Endfinally /// and Endfilter instructions from the patched method when they are followed by an exception block. /// + // TODO: Remove this once fixed. internal class HarmonyTranspilersFixPatch { public static MethodBase TargetMethod() => AccessTools.Method("HarmonyLib.Internal.Patching.ILManipulator:WriteTo"); diff --git a/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs b/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs index 64b83b6..92d08e6 100644 --- a/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs +++ b/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs @@ -14,6 +14,7 @@ namespace SongCore.HarmonyPatches /// This patch catches all exceptions and displays an error message to the user /// in the when the game is loading beatmap levels. /// + // TODO: Make this use MethodType.Async once supported. [HarmonyPatch] internal class StandardLevelDetailViewControllerPatch {