diff --git a/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs
new file mode 100644
index 0000000..d50922e
--- /dev/null
+++ b/source/SongCore/HarmonyPatches/HarmonyTranspilersFixPatch.cs
@@ -0,0 +1,32 @@
+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.
+ ///
+ // TODO: Remove this once fixed.
+ 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..92d08e6
--- /dev/null
+++ b/source/SongCore/HarmonyPatches/StandardLevelDetailViewControllerPatch.cs
@@ -0,0 +1,78 @@
+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.
+ ///
+ // TODO: Make this use MethodType.Async once supported.
+ [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 cf9dd6d..492187d 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 IPALogger = IPA.Logging.Logger;
@@ -17,8 +18,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; }
@@ -29,7 +30,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)));
@@ -37,6 +38,7 @@ public void Init(IPALogger pluginLogger, PluginMetadata metadata)
Logging.Logger = pluginLogger;
_metadata = metadata;
+ _harmony = new Harmony("com.kyle1413.BeatSaber.SongCore");
}
[OnStart]
@@ -44,7 +46,8 @@ public void OnApplicationStart()
{
BSMLSettings.instance.AddSettingsMenu(nameof(SongCore), "SongCore.UI.settings.bsml", new SCSettingsController());
- _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 276798d..3aadc8c 100644
--- a/source/SongCore/SongCore.csproj
+++ b/source/SongCore/SongCore.csproj
@@ -3,7 +3,7 @@
net472
Library
- 8
+ latest
enable
false
..\Refs
@@ -24,6 +24,10 @@
$(BeatSaberDir)\Libs\0Harmony.dll
False
+
+ $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll
+ False
+
$(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.AppFlow.dll
false
@@ -32,16 +36,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
@@ -71,10 +71,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
@@ -110,10 +118,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;
+ }
+ }
+}