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
{