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