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