From dfafe068b4f6fcfc6b071bcd2b6689a6cc30a5a7 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:29:21 -0700 Subject: [PATCH] mods!!!! --- .../PatchUpdatePerformanceCalculator.cs | 34 +++++----- .../Osu.Performance.ROsu.csproj | 2 +- Osu.Performance.ROsu/OsuPerformance.cs | 68 ++++++++++--------- Osu.Performance.ROsu/rosu-ffi/src/lib.rs | 5 +- Osu.Performance/Osu.Performance.csproj | 1 + Osu.Stubs/Beatmap.cs | 62 +++++++++++------ Osu.Stubs/IncreaseScoreType.cs | 25 ++++++- Osu.Stubs/Obfuscated.cs | 46 +++++++++++++ Osu.Stubs/Opcode/OsuAssembly.cs | 2 +- Osu.Stubs/Score.cs | 16 +++++ 10 files changed, 183 insertions(+), 78 deletions(-) create mode 100644 Osu.Stubs/Obfuscated.cs diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PatchUpdatePerformanceCalculator.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PatchUpdatePerformanceCalculator.cs index 1b81a45..a718c96 100644 --- a/Osu.Patcher.Hook/Patches/LivePerformance/PatchUpdatePerformanceCalculator.cs +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PatchUpdatePerformanceCalculator.cs @@ -1,12 +1,12 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Reflection; +using System.Threading; using HarmonyLib; using JetBrains.Annotations; using Osu.Performance.ROsu; using Osu.Stubs; -using Osu.Stubs.Opcode; namespace Osu.Patcher.Hook.Patches.LivePerformance; @@ -16,26 +16,31 @@ public static class PatchUpdatePerformanceCalculator { private static OsuPerformance? _performance; - internal static void ResetCalculator() + internal static void ResetCalculator() => new Thread(() => { + Debug.WriteLine("Resetting performance calculator"); + _performance?.Dispose(); _performance = null; var currentScore = Player.CurrentScore.Get(); if (currentScore == null) return; + var modsObfuscated = Score.EnabledMods.Get(currentScore); + var mods = Score.EnabledModsGetValue.Invoke(modsObfuscated); + + // Clear relax mod for now (live pp calculations for relax are fucking garbage) + mods &= ~(1 << 7); + var beatmap = Score.Beatmap.Get(currentScore); if (beatmap == null) return; - var beatmapSubPath = Beatmap.GetBeatmapPath(beatmap); - if (beatmapSubPath == null) return; + var beatmapPath = Beatmap.GetBeatmapPath(beatmap); + if (beatmapPath == null) return; - var osuDir = Path.GetDirectoryName(OsuAssembly.Assembly.Location)!; - var beatmapPath = Path.Combine(osuDir, "Songs", beatmapSubPath); - - _performance = new OsuPerformance(beatmapPath, 0); + _performance = new OsuPerformance(beatmapPath, (uint)mods); _performance.OnNewCalculation += Console.WriteLine; - } + }).Start(); [UsedImplicitly] [HarmonyTargetMethod] @@ -51,17 +56,12 @@ private static void After( { if (_performance == null) return; - const int HitScoreMask = IncreaseScoreType.Osu300 | - IncreaseScoreType.Osu100 | - IncreaseScoreType.Osu50 | - IncreaseScoreType.MissBit; - - var judgement = (increaseScoreType & HitScoreMask) switch + var judgement = (increaseScoreType & ~IncreaseScoreType.OsuComboModifiers) switch { IncreaseScoreType.Osu300 => OsuJudgement.Result300, IncreaseScoreType.Osu100 => OsuJudgement.Result100, IncreaseScoreType.Osu50 => OsuJudgement.Result50, - IncreaseScoreType.MissBit => OsuJudgement.ResultMiss, + IncreaseScoreType.OsuMiss => OsuJudgement.ResultMiss, _ => OsuJudgement.None, }; diff --git a/Osu.Performance.ROsu/Osu.Performance.ROsu.csproj b/Osu.Performance.ROsu/Osu.Performance.ROsu.csproj index 7d776ed..bf7d4e8 100644 --- a/Osu.Performance.ROsu/Osu.Performance.ROsu.csproj +++ b/Osu.Performance.ROsu/Osu.Performance.ROsu.csproj @@ -28,7 +28,7 @@ rosu.ffi%(Extension) - PreserveNewest + Always diff --git a/Osu.Performance.ROsu/OsuPerformance.cs b/Osu.Performance.ROsu/OsuPerformance.cs index 6712909..d9d16fc 100644 --- a/Osu.Performance.ROsu/OsuPerformance.cs +++ b/Osu.Performance.ROsu/OsuPerformance.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; namespace Osu.Performance.ROsu; @@ -8,30 +9,28 @@ namespace Osu.Performance.ROsu; [UsedImplicitly] public class OsuPerformance : IDisposable { - private readonly Thread _calculatingThread; private readonly ConcurrentQueue _queue; + private readonly CancellationTokenSource _queueTaskCancellation; private readonly IntPtr _state; - private volatile int _closed; public OsuPerformance(string mapPath, uint mods) { _state = Native.InitializeOsuGradualPerformance(mapPath, mods); _queue = new ConcurrentQueue(); - _closed = 0; - _calculatingThread = new Thread(ProcessQueue) - { - IsBackground = true, - }; - - _calculatingThread.Start(); + _queueTaskCancellation = new CancellationTokenSource(); + Task.Factory.StartNew( + ProcessQueue, + _queueTaskCancellation.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ); } public void Dispose() { - Interlocked.Exchange(ref _closed, 1); + _queueTaskCancellation.Cancel(); OnNewCalculation = null; - _calculatingThread.Abort(); Native.DisposeGradualOsuPerformance(_state); } @@ -49,7 +48,7 @@ public void Dispose() [UsedImplicitly] public void AddJudgement(OsuJudgement judgement, uint maxCombo) { - if (_closed > 0) return; + if (_queueTaskCancellation.IsCancellationRequested) return; _queue.Enqueue(new PendingCalculation { @@ -58,40 +57,45 @@ public void AddJudgement(OsuJudgement judgement, uint maxCombo) }); } - private void ProcessQueue() + private async void ProcessQueue() { while (true) { - if (_closed > 0) return; + if (_queueTaskCancellation.IsCancellationRequested) return; while (_queue.TryDequeue(out var item)) { var performance = Native.CalculateGradualOsuPerformance(_state, item.Judgement, item.MaxCombo); - var clamped = Math.Max(0, performance); - OnNewCalculation?.Invoke(clamped); + if (performance < 0f) + { + Console.WriteLine(new Exception("Cannot calculate performance after the end of a beatmap!")); + break; + } + + OnNewCalculation?.Invoke(performance); - if (_closed > 0) return; + if (_queueTaskCancellation.IsCancellationRequested) return; } - Thread.Sleep(100); + await Task.Delay(200); } } - /// - /// Calculates the performance metrics of a score while and returns the complete info. - /// If this is a failed score, or is in progress for whatever reason, then the end of the score will be - /// calculated based on the sum of the amount of hits recorded in . - /// - /// The precalculated/cached difficulty attributes of a map. - /// A completed (or failed) score's info on the associated map. - /// The set of mods that were used on this score. - [UsedImplicitly] - public static OsuPerformanceInfo CalculateScore( - OsuDifficultyAttributes difficulty, - OsuScoreState score, - uint mods - ) => Native.CalculateOsuPerformance(ref difficulty, ref score, mods); + // /// + // /// Calculates the performance metrics of a score while and returns the complete info. + // /// If this is a failed score, or is in progress for whatever reason, then the end of the score will be + // /// calculated based on the sum of the amount of hits recorded in . + // /// + // /// The precalculated/cached difficulty attributes of a map. + // /// A completed (or failed) score's info on the associated map. + // /// The set of mods that were used on this score. + // [UsedImplicitly] + // public static OsuPerformanceInfo CalculateScore( + // OsuDifficultyAttributes difficulty, + // OsuScoreState score, + // uint mods + // ) => Native.CalculateOsuPerformance(ref difficulty, ref score, mods); private struct PendingCalculation { diff --git a/Osu.Performance.ROsu/rosu-ffi/src/lib.rs b/Osu.Performance.ROsu/rosu-ffi/src/lib.rs index b9e0b46..c709960 100644 --- a/Osu.Performance.ROsu/rosu-ffi/src/lib.rs +++ b/Osu.Performance.ROsu/rosu-ffi/src/lib.rs @@ -68,8 +68,9 @@ extern "C" fn calculate_osu_performance_gradual( state.performance.next(state.score.clone()) }; - // TODO: handle errors - return performance.unwrap().pp; + return performance + .map(|attrs| attrs.pp) + .unwrap_or(-1.0); } #[no_mangle] diff --git a/Osu.Performance/Osu.Performance.csproj b/Osu.Performance/Osu.Performance.csproj index 712a463..be330c9 100644 --- a/Osu.Performance/Osu.Performance.csproj +++ b/Osu.Performance/Osu.Performance.csproj @@ -27,6 +27,7 @@ + Build;BuildNative TargetFramework=net452 diff --git a/Osu.Stubs/Beatmap.cs b/Osu.Stubs/Beatmap.cs index 6ca4e62..c7d7141 100644 --- a/Osu.Stubs/Beatmap.cs +++ b/Osu.Stubs/Beatmap.cs @@ -3,10 +3,10 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Reflection.Emit; using HarmonyLib; using JetBrains.Annotations; using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; namespace Osu.Stubs; @@ -17,6 +17,30 @@ namespace Osu.Stubs; [UsedImplicitly] public static class Beatmap { + /// + /// Original: Unknown, best guess: SetContainingFolder(string absoluteDirPath) + /// b20240123: #=zQwzJucCIbIUZrSZR8Q== + /// + private static readonly LazyMethod SetContainingFolder = new( + "Beatmap#SetContainingFolder(...)", + new[] + { + Callvirt, + Starg_S, + Ldarg_0, + Ldarg_1, + Ldc_I4_1, + Newarr, + Dup, + Ldc_I4_0, + Ldsfld, + Stelem_I2, + Callvirt, + Stfld, // Reference to ContainingFolder + Ret, + } + ); + /// /// Original: Filename /// b20240123: #=zdZI_NOQ= @@ -33,32 +57,28 @@ public static class Beatmap .Skip(1) .First(); - Debug.Assert(storeInstruction.Opcode == OpCodes.Stfld); + Debug.Assert(storeInstruction.Opcode == Stfld); return (FieldInfo)storeInstruction.Operand; } ); /// - /// Original: ContainingFolderAbsolute + /// Original: Unknown, best guess: ContainingFolder (not absolute) /// b20240123: #=zDmW9P6igScNm /// [UsedImplicitly] - public static readonly LazyField ContainingFolderAbsolute = new( - "Beatmap#ContainingFolderAbsolute", + public static readonly LazyField ContainingFolder = new( + "Beatmap#ContainingFolder", () => { - // TODO: find this field properly - return RuntimeType.Field("#=zDmW9P6igScNm"); + // Last Stfld is a reference to ContainingFolder + var storeInstruction = MethodReader + .GetInstructions(SetContainingFolder.Reference) + .Reverse() + .First(inst => inst.Opcode == Stfld); - // // Second Stfld is a reference to ContainingFolderAbsolute - // var storeInstruction = MethodReader - // .GetInstructions(PrimaryConstructor) - // .Where(inst => inst.Opcode == OpCodes.Stfld) - // .Skip(1) - // .First(); - // - // return (FieldInfo)storeInstruction.Operand; + return (FieldInfo)storeInstruction.Operand; } ); @@ -77,7 +97,7 @@ public static class Beatmap .ParameterType; /// - /// Utility wrapper to get the full beatmap path of a Beatmap object + /// Utility wrapper to get the full beatmap path of a Beatmap. /// /// An instance of Beatmap that was initialized with the filepath. /// The absolute path, or null if this isn't a file-backed Beatmap. @@ -85,13 +105,11 @@ public static class Beatmap public static string? GetBeatmapPath(object beatmap) { var filename = Filename.Get(beatmap); - var directory = ContainingFolderAbsolute.Get(beatmap); + var folder = ContainingFolder.Get(beatmap); + if (filename == null || folder == null) return null; - if (filename != null && directory != null) - { - return Path.Combine(directory, filename); - } + var osuDir = Path.GetDirectoryName(OsuAssembly.Assembly.Location)!; - return null; + return Path.Combine(osuDir, "Songs", folder, filename); } } \ No newline at end of file diff --git a/Osu.Stubs/IncreaseScoreType.cs b/Osu.Stubs/IncreaseScoreType.cs index 7390139..3c2581e 100644 --- a/Osu.Stubs/IncreaseScoreType.cs +++ b/Osu.Stubs/IncreaseScoreType.cs @@ -10,13 +10,32 @@ namespace Osu.Stubs; [UsedImplicitly] public class IncreaseScoreType { - // TODO: reverse engineer all enum values - public const int MissBit = 1 << 31; // No clue if this is right + // For HitCircles, the only IST emitted is the final hit result (Osu50, Osu100, Osu300, OsuMiss) + + // For OsuSliders, an IST is emitted for the slider head, slider ticks, slider end, and the overall hit result. + // Slider head values: If hit within any timing window, then OsuSliderHead, otherwise OsuSliderHeadMiss + // Slider ticks: If properly tracked, then OsuSliderTick, otherwise OsuSliderTickMiss + // Slider end: If properly tracked, then OsuSliderEnd, otherwise OsuSliderEndMiss + // Final result: Any of the values that are emitted by HitCircle. This is the only IST from sliders that is tracked in scores. + + // This can be combined with any Osu300,Osu100,Osu50 if a significant amount of the combo + // was missed but the combo end object was hit + public const int OsuComboEndSmall = 1 << 0; + public const int OsuComboEndMedium = 1 << 1; // Same as above but more of the combo was hit + public const int OsuComboEndFull = 1 << 2; // Same as above but the entire combo was 300s + public const int OsuComboModifiers = OsuComboEndSmall | OsuComboEndMedium | OsuComboEndFull; + + public const int OsuSliderTick = 1 << 3; + public const int OsuSliderHead = 1 << 6; + public const int OsuSliderEnd = 1 << 7; + public const int OsuSliderHeadMiss = -0x40000; + public const int OsuSliderTickMiss = -0x40000; + public const int OsuSliderEndMiss = -0x80000; - public const int Miss = -131072; public const int Osu50 = 1 << 8; public const int Osu100 = 1 << 9; public const int Osu300 = 1 << 10; + public const int OsuMiss = -0x20000; // Used as the first parameter in the constructor for ScoreChange [UsedImplicitly] diff --git a/Osu.Stubs/Obfuscated.cs b/Osu.Stubs/Obfuscated.cs new file mode 100644 index 0000000..1bad808 --- /dev/null +++ b/Osu.Stubs/Obfuscated.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +[UsedImplicitly] +public class Obfuscated +{ + private static readonly LazyMethod Finalize = new( + "Obfuscated#Finalize()", + new[] + { + Newobj, + Dup, + Stsfld, + Ldc_I4, + Ldc_I4_0, + Callvirt, // This is a reference to Scheduler::AddDelayed, TODO: use this? + Pop, + Leave_S, + Ldarg_0, + Call, + Endfinally, + Ret, + } + ); + + [UsedImplicitly] + public static readonly LazyMethod GetValue = new( + "Obfuscated#get_Value()", + () => RuntimeType.GetRuntimeMethods() + .First(mtd => mtd.GetParameters().Length == 0 && mtd.ReturnType.IsGenericParameter) + ); + + [UsedImplicitly] + public static Type RuntimeType => Finalize.Reference.DeclaringType!; + + // Binds the generic parameter in Obfuscated so the method becomes callable + public static MethodBase BindGetValue(Type type) => RuntimeType + .MakeGenericType(type) + .GetMethod(GetValue.Reference.Name)!; +} \ No newline at end of file diff --git a/Osu.Stubs/Opcode/OsuAssembly.cs b/Osu.Stubs/Opcode/OsuAssembly.cs index 29b1d7c..5e8950c 100644 --- a/Osu.Stubs/Opcode/OsuAssembly.cs +++ b/Osu.Stubs/Opcode/OsuAssembly.cs @@ -26,7 +26,7 @@ static OsuAssembly() Module = module; } - public static Assembly Assembly { [UsedImplicitly] get; private set; } + internal static Assembly Assembly { [UsedImplicitly] get; private set; } /// /// Retrieve all the types located in the osu! assembly. diff --git a/Osu.Stubs/Score.cs b/Osu.Stubs/Score.cs index 585099f..82ca360 100644 --- a/Osu.Stubs/Score.cs +++ b/Osu.Stubs/Score.cs @@ -88,6 +88,22 @@ public class Score } ); + // Is of type Obfuscated + [UsedImplicitly] + public static readonly LazyField EnabledMods = new( + "Score#EnabledMods", + () => RuntimeType.GetDeclaredFields() + .Single(field => + field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == Obfuscated.RuntimeType) + ); + + // Generic method Obfuscated.get_Value() bound to the type parameter of + [UsedImplicitly] + public static readonly LazyMethod EnabledModsGetValue = new( + "Obfuscated#get_Value()", + () => Obfuscated.BindGetValue(EnabledMods.Reference.FieldType.GetGenericArguments().First()) + ); + [UsedImplicitly] public static Type RuntimeType => GetAccuracy.Reference.DeclaringType!; } \ No newline at end of file