diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs
new file mode 100644
index 0000000..0064117
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Reflection;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.Audio;
+using Osu.Utils.Extensions;
+using static System.Reflection.Emit.OpCodes;
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+///
+/// AudioTrackBass::updatePlaybackRate() forcibly resets the tempo to
+/// normal if a pitch isn't applied. This forces the tempo to be set if pitch isn't applied,
+/// to be used with the patch
+///
+[OsuPatch]
+[HarmonyPatch]
+[UsedImplicitly]
+internal static class FixUpdatePlaybackRate
+{
+ // static FixUpdatePlaybackRate()
+ // {
+ // Task.Run(async () =>
+ // {
+ // await Task.Delay(2000);
+ // try
+ // {
+ // var mtd = AccessTools.Method(AudioTrackBass.Class.Reference.Name + ":" +
+ // AudioTrackBass.UpdatePlaybackRate.Reference.Name);
+ // foreach (var instruction in MethodReader.GetInstructions(mtd))
+ // {
+ // Console.WriteLine($"{instruction.Opcode} {instruction.Operand}");
+ // }
+ // }
+ // catch (Exception e)
+ // {
+ // Console.WriteLine(e);
+ // }
+ // });
+ // }
+
+ [UsedImplicitly]
+ [HarmonyTargetMethod]
+ private static MethodBase Target() => AudioTrackBass.UpdatePlaybackRate.Reference;
+
+ // TODO: WHY THE FUCK ISN'T THIS TRANSPILER APPLYING CHANGES????? PRE/POST PATCHES WORK FINE BUT THIS DOESN'T????????
+ [UsedImplicitly]
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ instructions = instructions.ManipulatorReplace(
+ // Find inst that loads 0f as the parameter "value" to BASS_ChannelSetAttribute
+ inst => inst.Is(Ldc_R4, 0f),
+ inst => new CodeInstruction[]
+ {
+ new(Ldarg_0) { labels = inst.labels }, // Load "this"
+ new(Ldfld, AudioTrackBass.PlaybackRate.Reference), // Load the float64 "playbackRate"
+ new(Ldc_R8, 100.0), // Load the float64 "100.0"
+ new(Sub), // Subtract 100.0 from playbackRate
+ new(Conv_R4), // Convert to float32
+ }
+ );
+
+ return instructions;
+ }
+}
\ No newline at end of file
diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs
new file mode 100644
index 0000000..02e3e6e
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.Audio;
+using Osu.Stubs.SongSelect;
+using static Osu.Stubs.Other.Mods;
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+[OsuPatch]
+[HarmonyPatch]
+[UsedImplicitly]
+internal class ModSelectAudioPreview
+{
+ [UsedImplicitly]
+ [HarmonyTargetMethod]
+ private static MethodBase Target() => ModButton.SetStatus.Reference;
+
+ [UsedImplicitly]
+ [HarmonyPostfix]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ private static void After(
+ object __instance, // typeof(ModButton)
+ [HarmonyArgument(1)] int mod,
+ [HarmonyArgument(2)] bool playSound)
+ {
+ // These calls happen for all mods on any mod update and don't actually do anything
+ if (!playSound) return;
+
+ // var availableModStates = ModButton.AvailableStates.Get(__instance);
+ //
+ // // Check that this is the DT+NC or HF button
+ // if (availableModStates[0] is not (DoubleTime or HalfTime))
+ // return;
+
+ ApplyChanges(mod);
+ }
+
+ internal static void ApplyChanges(int mods)
+ {
+ ResetChanges();
+
+ switch (mods & (DoubleTime | Nightcore | HalfTime))
+ {
+ case DoubleTime:
+ UpdateAudioRate(rate => rate * 1.5);
+ break;
+ case Nightcore:
+ AudioEngine.Nightcore.Set(true);
+ UpdateAudioRate(rate => rate * 1.5);
+ break;
+ case HalfTime:
+ UpdateAudioRate(rate => rate * 0.75);
+ break;
+ }
+ }
+
+ ///
+ /// Resets the audio stream effects back to default.
+ ///
+ private static void ResetChanges()
+ {
+ AudioEngine.Nightcore.Set(false);
+ UpdateAudioRate(_ => 100);
+ }
+
+ private static void UpdateAudioRate(Func onModify)
+ {
+ var currentRate = AudioEngine.GetCurrentPlaybackRate.Invoke();
+ var newRate = onModify.Invoke(currentRate);
+
+ AudioEngine.SetCurrentPlaybackRate.Invoke(parameters: [newRate]);
+ }
+}
\ No newline at end of file
diff --git a/Osu.Patcher.Hook/Patches/Mods/PatchSuddenDeathAutoRetry.cs b/Osu.Patcher.Hook/Patches/Mods/SuddenDeathAutoRetry.cs
similarity index 82%
rename from Osu.Patcher.Hook/Patches/Mods/PatchSuddenDeathAutoRetry.cs
rename to Osu.Patcher.Hook/Patches/Mods/SuddenDeathAutoRetry.cs
index bf0b27e..306e55c 100644
--- a/Osu.Patcher.Hook/Patches/Mods/PatchSuddenDeathAutoRetry.cs
+++ b/Osu.Patcher.Hook/Patches/Mods/SuddenDeathAutoRetry.cs
@@ -4,6 +4,7 @@
using JetBrains.Annotations;
using Osu.Stubs.Rulesets;
using static System.Reflection.Emit.OpCodes;
+using static Osu.Stubs.Other.Mods;
namespace Osu.Patcher.Hook.Patches.Mods;
@@ -24,11 +25,8 @@ namespace Osu.Patcher.Hook.Patches.Mods;
[OsuPatch]
[HarmonyPatch]
[UsedImplicitly]
-internal static class PatchSuddenDeathAutoRetry
+internal static class SuddenDeathAutoRetry
{
- private const int ModPerfect = 1 << 14;
- private const int ModSuddenDeath = 1 << 5;
-
[UsedImplicitly]
[HarmonyTargetMethod]
private static MethodBase Target() => Ruleset.Fail.Reference;
@@ -42,8 +40,8 @@ internal static class PatchSuddenDeathAutoRetry
private static IEnumerable Transpiler(IEnumerable instructions)
{
instructions = instructions.Manipulator(
- inst => inst.opcode == Ldc_I4 && inst.OperandIs(ModPerfect),
- inst => inst.operand = ModPerfect | ModSuddenDeath
+ inst => inst.opcode == Ldc_I4 && inst.OperandIs(Perfect),
+ inst => inst.operand = Perfect | SuddenDeath
);
return instructions;
diff --git a/Osu.Stubs/Audio/AudioEngine.cs b/Osu.Stubs/Audio/AudioEngine.cs
new file mode 100644
index 0000000..e71b7aa
--- /dev/null
+++ b/Osu.Stubs/Audio/AudioEngine.cs
@@ -0,0 +1,73 @@
+using System.Linq;
+using System.Reflection;
+using JetBrains.Annotations;
+using Osu.Utils.IL;
+using Osu.Utils.Lazy;
+using static System.Reflection.Emit.OpCodes;
+
+namespace Osu.Stubs.Audio;
+
+///
+/// Original: osu.Audio.AudioEngine
+/// b20240123:
+///
+[PublicAPI]
+public class AudioEngine
+{
+ ///
+ /// Original: get_CurrentPlaybackRate() (property getter)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod GetCurrentPlaybackRate = LazyMethod.BySignature(
+ "osu.Audio.AudioEngine::get_CurrentPlaybackRate()",
+ [
+ Ldsfld,
+ Dup,
+ Brtrue_S,
+ Pop,
+ Ldc_R8,
+ Ret,
+ Callvirt,
+ Ret,
+ ]
+ );
+
+ ///
+ /// Original: set_CurrentPlaybackRate() (property setter)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod SetCurrentPlaybackRate = LazyMethod.ByPartialSignature(
+ "osu.Audio.AudioEngine::set_CurrentPlaybackRate()",
+ [
+ Ldsfld, // Reference to AudioEngine::Nightcore
+ Ldc_I4_0,
+ Ceq,
+ Callvirt,
+ Ldloc_0,
+ Ldarg_0,
+ Callvirt,
+ Ret,
+ ]
+ );
+
+ ///
+ /// Original: Nightcore
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyField Nightcore = new(
+ "osu.Audio.AudioEngine::Nightcore",
+ () =>
+ {
+ // Last Ldsfld in get_CurrentPlaybackRate() is a reference to AudioEngine::Nightcore
+ var instruction = MethodReader
+ .GetInstructions(SetCurrentPlaybackRate.Reference)
+ .Reverse()
+ .First(inst => inst.Opcode == Ldsfld);
+
+ return (FieldInfo)instruction.Operand;
+ }
+ );
+}
\ No newline at end of file
diff --git a/Osu.Stubs/Audio/AudioTrackBass.cs b/Osu.Stubs/Audio/AudioTrackBass.cs
new file mode 100644
index 0000000..2933136
--- /dev/null
+++ b/Osu.Stubs/Audio/AudioTrackBass.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Utils.Lazy;
+using static System.Reflection.Emit.OpCodes;
+
+namespace Osu.Stubs.Audio;
+
+[PublicAPI]
+public class AudioTrackBass
+{
+ ///
+ /// Original: osu.Audio.AudioTrackBass
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyType Class = new(
+ "osu.Audio.AudioTrackBass",
+ () => UpdatePlaybackRate!.Reference.DeclaringType!
+ );
+
+ ///
+ /// Original: updatePlaybackRate()
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod UpdatePlaybackRate = LazyMethod.ByPartialSignature(
+ "osu.Audio.AudioTrackBass::updatePlaybackRate()",
+ [
+ Conv_R8,
+ Ldarg_0,
+ Ldfld,
+ Mul,
+ Ldc_R8,
+ Div,
+ Conv_R4,
+ Call,
+ Pop,
+ ]
+ );
+
+ ///
+ /// Original: playbackRate
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyField PlaybackRate = new(
+ "osu.Audio.AudioTrackBass::playbackRate",
+ () => Class.Reference
+ .GetDeclaredFields()
+ .Single(field => field.FieldType == typeof(double))
+ );
+}
\ No newline at end of file
diff --git a/Osu.Stubs/Other/Mods.cs b/Osu.Stubs/Other/Mods.cs
new file mode 100644
index 0000000..046c37e
--- /dev/null
+++ b/Osu.Stubs/Other/Mods.cs
@@ -0,0 +1,45 @@
+using JetBrains.Annotations;
+using Osu.Utils.Lazy;
+
+namespace Osu.Stubs.Other;
+
+[PublicAPI]
+public class Mods
+{
+ public const int None = 0;
+ public const int NoFail = 1 << 0;
+ public const int Easy = 1 << 1;
+ public const int Hidden = 1 << 3;
+ public const int HardRock = 1 << 4;
+ public const int SuddenDeath = 1 << 5;
+ public const int DoubleTime = 1 << 6;
+ public const int Relax = 1 << 7;
+ public const int HalfTime = 1 << 8;
+ public const int Nightcore = 1 << 9;
+ public const int Flashlight = 1 << 10;
+ public const int Autoplay = 1 << 11;
+ public const int SpunOut = 1 << 12;
+ public const int Relax2 = 1 << 13;
+ public const int Perfect = 1 << 14;
+ public const int Key4 = 1 << 15;
+ public const int Key5 = 1 << 16;
+ public const int Key6 = 1 << 17;
+ public const int Key7 = 1 << 18;
+ public const int Key8 = 1 << 19;
+ public const int FadeIn = 1 << 20;
+ public const int Random = 1 << 21;
+ public const int Cinema = 1 << 22;
+ public const int Target = 1 << 23;
+ public const int Key9 = 1 << 24;
+ public const int KeyCoop = 1 << 25;
+ public const int Key1 = 1 << 26;
+ public const int Key3 = 1 << 27;
+ public const int Key2 = 1 << 28;
+
+ ///
+ /// Original: osu_common.Mods
+ /// b20240123: osu_common.Mods
+ ///
+ [Stub]
+ public static readonly LazyType Type = LazyType.ByName("osu_common.Mods");
+}
\ No newline at end of file
diff --git a/Osu.Stubs/SongSelect/ModButton.cs b/Osu.Stubs/SongSelect/ModButton.cs
new file mode 100644
index 0000000..97df240
--- /dev/null
+++ b/Osu.Stubs/SongSelect/ModButton.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Linq;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.Other;
+using Osu.Utils.Lazy;
+using static System.Reflection.Emit.OpCodes;
+
+namespace Osu.Stubs.SongSelect;
+
+[PublicAPI]
+public class ModButton
+{
+ ///
+ /// Original: osu.GameModes.Select.ModButton
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyType Class = new(
+ "osu.GameModes.Select.ModButton",
+ () => SetStatus!.Reference.DeclaringType!
+ );
+
+ ///
+ /// Original: SetStatus(bool status, Mods specificStatus, bool playSound)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod SetStatus = LazyMethod.ByPartialSignature(
+ "osu.GameModes.Select.ModButton::SetStatus(bool, Mods, bool)",
+ [
+ Ldfld,
+ Ceq,
+ Ldc_I4_0,
+ Ceq,
+ Ldarg_3,
+ And,
+ Brfalse_S,
+ Ldloca_S,
+ Initobj,
+ ]
+ );
+
+ ///
+ /// Original: AvailableStates
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyField> AvailableStates = new(
+ "osu.GameModes.Select.ModButton::AvailableStates",
+ () =>
+ {
+ // typeof(List)
+ var modsListType = typeof(List<>).MakeGenericType(Mods.Type.Reference);
+
+ return Class.Reference
+ .GetDeclaredFields()
+ .Single(field => field.FieldType == modsListType);
+ }
+ );
+}
\ No newline at end of file
diff --git a/Osu.Utils/Extensions/TranspilerExtensions.cs b/Osu.Utils/Extensions/TranspilerExtensions.cs
index 1343255..def57df 100644
--- a/Osu.Utils/Extensions/TranspilerExtensions.cs
+++ b/Osu.Utils/Extensions/TranspilerExtensions.cs
@@ -210,4 +210,35 @@ public static IEnumerable NoopAfterSignature(
if (found && replacementRemaining > 0 && replaceAfterSignature != replacementRemaining)
throw new Exception("Not enough space in method to noop more instructions!");
}
+
+ ///
+ /// A transpiler that replaces instructions that match a predicate with new instruction(s)
+ ///
+ /// The input instructions to patch, mainly coming from a HarmonyTranspiler.
+ /// A predicate selecting the instructions to act upon.
+ /// An action to insert the new matching instructions. Labels are NOT carried over!
+ public static IEnumerable ManipulatorReplace(
+ this IEnumerable instructions,
+ Func predicate,
+ Func> action)
+ {
+ var found = false;
+
+ foreach (var instruction in instructions)
+ {
+ if (!predicate(instruction))
+ {
+ yield return instruction;
+ }
+ else
+ {
+ found = true;
+ foreach (var newInstruction in action(instruction))
+ yield return newInstruction;
+ }
+ }
+
+ if (!found)
+ throw new Exception("ManipulatorReplace didn't find any matches!");
+ }
}
\ No newline at end of file