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..48a3d6d
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs
@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.Audio;
+using Osu.Utils.Extensions;
+using static System.Reflection.Emit.OpCodes;
+
+// ReSharper disable InconsistentNaming
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+///
+/// Apply various fixes to AudioTrackBass to make BASS_ATTRIB_TEMPO work again on
+/// "Preview" audio streams. We use this in combination with
+/// to reapply the constant pitch speed modifier (for DoubleTime).
+///
+[OsuPatch]
+[HarmonyPatch]
+[UsedImplicitly]
+internal class FixUpdatePlaybackRate
+{
+ [UsedImplicitly]
+ [HarmonyTargetMethod]
+ private static MethodBase Target() => AudioTrackBass.Constructor.Reference;
+
+ [UsedImplicitly]
+ [HarmonyTranspiler]
+ private static IEnumerable Transpiler(
+ IEnumerable instructions,
+ ILGenerator generator)
+ {
+ // Remove the conditional & block that checks for this.Preview and always do the full initialization
+ // Otherwise, the "quick" initialization never uses BASS_FX_TempoCreate, which makes setting
+ // BASSAttribute.BASS_ATTRIB_TEMPO impossible (what's used for DoubleTime).
+ // instructions = instructions.NoopSignature(
+ instructions = instructions.NoopSignature(
+ // if (Preview) { audioStream = audioStreamForwards = audioStreamPrefilter; }
+ [
+ Ldarg_0,
+ Callvirt,
+ Brfalse_S,
+ Ldarg_0,
+ Ldarg_0,
+ Ldarg_0,
+ Ldfld,
+ Dup,
+ Stloc_1,
+ Stfld,
+ Ldloc_1,
+ Call,
+ Br_S,
+ ]
+ );
+
+ // Change this "BASSFlag flags = Preview ? 0 : (BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_PRESCAN);"
+ // into "BASSFlag flags = Preview ? BASSFlag.BASS_STREAM_DECODE : (BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_PRESCAN);"
+ // This is so BASS_FX_TempoCreate still works when called later
+ var foundFlags = false;
+ const int BASS_STREAM_DECODE = 0x200000;
+ const int BASS_STREAM_PRESCAN = 0x20000;
+ instructions = instructions.Manipulator(
+ inst => foundFlags || inst.Is(Ldc_I4, BASS_STREAM_DECODE | BASS_STREAM_PRESCAN),
+ inst =>
+ {
+ if (inst.OperandIs(BASS_STREAM_DECODE | BASS_STREAM_PRESCAN))
+ {
+ foundFlags = true;
+ return;
+ }
+
+ // This is the "? 0"
+ if (inst.opcode != Ldc_I4_0)
+ return;
+
+ inst.opcode = Ldc_I4;
+ inst.operand = BASS_STREAM_DECODE;
+ foundFlags = false;
+ }
+ );
+
+ // Load speed optimization: disable BASS_FX_ReverseCreate when the "quick" parameter is true (aka. Preview)
+ var found = false;
+ instructions = instructions.Reverse().ManipulatorReplace(
+ inst => found || inst.Is(Stfld, AudioTrackBass.AudioStreamBackwardsHandle.Reference),
+ inst =>
+ {
+ // Only targeting the Call instruction before Stfld
+ if (inst.opcode != Call)
+ {
+ found = true;
+ return [inst];
+ }
+
+ found = false;
+ var labelTrue = generator.DefineLabel();
+ var labelFalse = generator.DefineLabel();
+
+ return new[]
+ {
+ new(Ldarg_2), // "bool quick"
+ new(Brfalse_S, labelFalse),
+
+ // Clean up the 3 values to the Call on stack
+ new(Pop),
+ new(Pop),
+ new(Pop),
+ new(Ldc_I4_0), // Load a "0" to be used for Stfld AudioStreamBackwardsHandle
+ new(Br_S, labelTrue),
+
+ inst.WithLabels([labelFalse]),
+ new(Nop) { labels = [labelTrue] },
+ }.Reverse();
+ }
+ ).Reverse();
+
+ return instructions;
+ }
+}
\ No newline at end of file
diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModAudioEffects.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModAudioEffects.cs
new file mode 100644
index 0000000..2e92487
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModAudioEffects.cs
@@ -0,0 +1,61 @@
+using System;
+using Osu.Stubs.Audio;
+using Osu.Stubs.Scoring;
+using static Osu.Stubs.Other.Mods;
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+///
+/// Handles applying and resetting the audio effects on the AudioEngine.
+///
+internal static class ModAudioEffects
+{
+ ///
+ /// Applies the audio changes to the AudioEngine based on the current global mods.
+ ///
+ internal static void ApplyModEffects()
+ {
+ ResetChanges();
+
+ var mods = ModManager.ModStatus.Get();
+
+ // NC always comes with DT
+ if ((mods & Nightcore) > None)
+ mods &= ~DoubleTime;
+
+ 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);
+ }
+
+ ///
+ /// Gets, modifies, and writes back the CurrentPlaybackRate on the AudioEngine.
+ ///
+ /// Rate transformer.
+ 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/AudioPreview/ModSelectAudioPreview.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs
new file mode 100644
index 0000000..d021185
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs
@@ -0,0 +1,24 @@
+using System.Reflection;
+using System.Threading.Tasks;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.SongSelect;
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+///
+/// Hooks the place where ModButtons get updates in the mod selection menu to apply audio effects.
+///
+[OsuPatch]
+[HarmonyPatch]
+[UsedImplicitly]
+internal class ModSelectAudioPreview
+{
+ [UsedImplicitly]
+ [HarmonyTargetMethod]
+ private static MethodBase Target() => ModSelection.UpdateMods.Reference;
+
+ [UsedImplicitly]
+ [HarmonyPostfix]
+ private static void After() => Task.Run(ModAudioEffects.ApplyModEffects);
+}
\ No newline at end of file
diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/TrackUpdatePreviewMusic.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/TrackUpdatePreviewMusic.cs
new file mode 100644
index 0000000..39cabf0
--- /dev/null
+++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/TrackUpdatePreviewMusic.cs
@@ -0,0 +1,24 @@
+using System.Reflection;
+using System.Threading.Tasks;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Osu.Stubs.Audio;
+
+namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;
+
+///
+/// Hooks the place where preview audio gets loaded to apply our mod audio effects.
+///
+[OsuPatch]
+[HarmonyPatch]
+[UsedImplicitly]
+public class TrackUpdatePreviewMusic
+{
+ [UsedImplicitly]
+ [HarmonyTargetMethod]
+ private static MethodBase Target() => AudioEngine.LoadAudioForPreview.Reference;
+
+ [HarmonyPostfix]
+ [UsedImplicitly]
+ private static void After() => Task.Run(ModAudioEffects.ApplyModEffects);
+}
\ 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..11cf277
--- /dev/null
+++ b/Osu.Stubs/Audio/AudioEngine.cs
@@ -0,0 +1,92 @@
+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: LoadAudioForPreview(Beatmap beatmap, bool continuePlayback, bool previewPoint, bool quick)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod LoadAudioForPreview = LazyMethod.ByPartialSignature(
+ "osu.Audio.AudioEngine::LoadAudioForPreview(Beatmap, bool, bool, bool)",
+ [
+ Call,
+ Ldc_I4_0,
+ Ceq,
+ Stloc_0,
+ Leave_S,
+ Pop,
+ Ldc_I4,
+ Call,
+ ]
+ );
+
+ ///
+ /// 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..52d5985
--- /dev/null
+++ b/Osu.Stubs/Audio/AudioTrackBass.cs
@@ -0,0 +1,54 @@
+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",
+ () => Constructor!.Reference.DeclaringType!
+ );
+
+ ///
+ /// Original: AudioTrackBass(Stream data, bool quick = false, bool loop = false)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyConstructor Constructor = LazyConstructor.ByPartialSignature(
+ "osu.Audio.AudioTrackBass::AudioTrackBass(Stream, bool, bool)",
+ [
+ Newobj,
+ Throw,
+ Ldarg_0,
+ Ldarg_1,
+ Isinst,
+ Stfld,
+ Ldarg_0,
+ Ldfld,
+ ]
+ );
+
+ ///
+ /// Original: audioStreamBackwards
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyField AudioStreamBackwardsHandle = new(
+ "osu.Audio.AudioTrackBass::audioStreamBackwards",
+ () => Class.Reference
+ .GetDeclaredFields()
+ .Where(field => field.FieldType == typeof(int))
+ .Skip(1)
+ .First()
+ );
+}
\ 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/Scoring/ModManager.cs b/Osu.Stubs/Scoring/ModManager.cs
new file mode 100644
index 0000000..a18e6bc
--- /dev/null
+++ b/Osu.Stubs/Scoring/ModManager.cs
@@ -0,0 +1,58 @@
+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.Scoring;
+
+[PublicAPI]
+public class ModManager
+{
+ ///
+ /// Original: osu.GameplayElements.Scoring.ModManager
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyType Class = new(
+ "osu.GameplayElements.Scoring.ModManager",
+ () => AllowRanking!.Reference.DeclaringType!
+ );
+
+ ///
+ /// Original: AllowRanking(Mods enabledMods)
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod AllowRanking = LazyMethod.ByPartialSignature(
+ "osu.GameplayElements.Scoring.ModManager::AllowRanking(Mods)",
+ [
+ Ldloc_2,
+ And,
+ Ldc_I4_0,
+ Cgt,
+ Brfalse_S,
+ Ldc_I4_0,
+ Ret,
+ Ldc_I4,
+ Ldarg_0,
+ Stloc_2,
+ Stloc_1,
+ Ldloc_1,
+ Ldloc_2,
+ ]
+ );
+
+ ///
+ /// Original: ActiveMods
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyField ModStatus = new(
+ "osu.GameplayElements.Scoring.ModManager::ModStatus",
+ () => Class.Reference
+ .GetDeclaredFields()
+ .Single(field => field.FieldType == Mods.Type.Reference)
+ );
+}
\ No newline at end of file
diff --git a/Osu.Stubs/SongSelect/ModSelection.cs b/Osu.Stubs/SongSelect/ModSelection.cs
new file mode 100644
index 0000000..f38f886
--- /dev/null
+++ b/Osu.Stubs/SongSelect/ModSelection.cs
@@ -0,0 +1,30 @@
+using JetBrains.Annotations;
+using Osu.Utils.Lazy;
+using static System.Reflection.Emit.OpCodes;
+
+namespace Osu.Stubs.SongSelect;
+
+///
+/// Original: osu.GameModes.Select.ModSelection
+/// b20240123:
+///
+[PublicAPI]
+public class ModSelection
+{
+ ///
+ /// Original: updateMods()
+ /// b20240123:
+ ///
+ [Stub]
+ public static readonly LazyMethod UpdateMods = LazyMethod.ByPartialSignature(
+ "osu.GameModes.Select.ModSelection::updateMods()",
+ [
+ Ldc_I4_1,
+ Conv_I8,
+ Stloc_0,
+ Br_S,
+ Ldloc_0,
+ Conv_I4,
+ ]
+ );
+}
\ No newline at end of file
diff --git a/Osu.Stubs/SongSelect/SongSelection.cs b/Osu.Stubs/SongSelect/SongSelection.cs
index 2c673bd..f9b29cc 100644
--- a/Osu.Stubs/SongSelect/SongSelection.cs
+++ b/Osu.Stubs/SongSelect/SongSelection.cs
@@ -42,7 +42,7 @@ public static class SongSelection
///
[Stub]
public static readonly LazyMethod BeatmapTreeManagerOnRightClicked = LazyMethod.ByPartialSignature(
- "osu.GameModes.Select.SongSelection::beatmapTreeManager_OnRightClicked",
+ "osu.GameModes.Select.SongSelection::beatmapTreeManager_OnRightClicked(object, BeatmapTreeItem)",
[
Ldarg_2,
Isinst,
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