diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PatchAddPerformanceToUi.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PatchAddPerformanceToUi.cs new file mode 100644 index 0000000..880f229 --- /dev/null +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PatchAddPerformanceToUi.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs; + +namespace Osu.Patcher.Hook.Patches.LivePerformance; + +/// +/// Hooks the constructor of ScoreDisplay to add our own pTextSprite for displaying +/// the performance counter to the ScoreDisplay's sprite manager. +/// This needs score-p@2x.png or score-p.png in your skin's score font assets! +/// +[HarmonyPatch] +[UsedImplicitly] +public class PatchAddPerformanceToScoreDisplay +{ + [UsedImplicitly] + [HarmonyTargetMethod] + private static MethodBase Target() => ScoreDisplay.Constructor.Reference; + + [UsedImplicitly] + [HarmonyPostfix] + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static void After( + object __instance, // ScoreDisplay + [HarmonyArgument(0)] object spriteManager, // SpriteManager + [HarmonyArgument(1)] object position, // Vector2 + [HarmonyArgument(2)] bool alignRight, + [HarmonyArgument(3)] float scale + ) + { + var currentSkin = SkinManager.Current.Get(); + var scoreFont = SkinOsu.FontScore.Get(currentSkin); + var scoreFontOverlap = SkinOsu.FontScoreOverlap.Get(currentSkin); + + var performanceSprite = ((ConstructorInfo)pSpriteText.Constructor.Reference).Invoke( + [ + /* text: */ "00.0pp", + /* fontName: */ scoreFont, + /* spacingOverlap: */ (float)scoreFontOverlap, + /* fieldType: */ alignRight ? Fields.TopRight : Fields.TopLeft, + /* origin: */ alignRight ? Origins.TopRight : Origins.TopLeft, + /* clock: */ Clocks.Game, + /* startPosition: */ ((ConstructorInfo)Vector2.Constructor.Reference).Invoke([0f, 0f]), + /* drawDepth: */ 0.95f, + /* alwaysDraw: */ true, + /* color: */ Color.White, + /* precache: */ true, + /* source: */ SkinSource.ExceptBeatmap, + ]); + + // Cannot be startPosition directly + // TODO: don't add 9f offset if score-p@2x.png/score-p.png texture exists + var positionX = Vector2.X.Get(position) + 9f; + var positionY = GetYOffset(Vector2.Y.Get(position), scale, __instance); + var newPosition = ((ConstructorInfo)Vector2.Constructor.Reference).Invoke([positionX, positionY]); + pDrawable.Position.Set(performanceSprite, newPosition); + + pDrawable.Scale.Set(performanceSprite, 0.50f); + pSpriteText.TextConstantSpacing.Set(performanceSprite, true); + pSpriteText.MeasureText.Invoke(performanceSprite); + + SpriteManager.Add.Invoke(spriteManager, [performanceSprite]); + PerformanceDisplay.SetPerformanceCounter(performanceSprite); + } + + [UsedImplicitly] + [HarmonyFinalizer] + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static void Finalizer(Exception? __exception) + { + if (__exception != null) + { + Console.WriteLine($"Exception due to {nameof(PatchAddPerformanceToScoreDisplay)}: {__exception}"); + } + } + + private static float GetYOffset(float baseYPosition, float scale, object scoreDisplay) + { + // Read the heights of both pSpriteTexts: s_Score, s_Accuracy + var sprites = ScoreDisplay.RuntimeType + .GetDeclaredFields() + .Where(f => f.FieldType == pSpriteText.RuntimeType) + .Select(f => f.GetValue(scoreDisplay)); + var spriteSizes = sprites + .Where(s => s != null) + .Select(s => pSpriteText.MeasureText.Invoke(s)); + var totalSpriteHeight = spriteSizes.Sum(v => Vector2.Y.Get(v)) * 0.58f * scale; + + // Preserve additional spacing between s_Score and s_Accuracy + var additionalOffset = SkinManager.GetUseNewLayout.Invoke() ? 3f : 0f; + + return baseYPosition + totalSpriteHeight + additionalOffset; + } +} \ No newline at end of file diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackOnScoreHit.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackOnScoreHit.cs index 587e180..09f3d1e 100644 --- a/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackOnScoreHit.cs +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackOnScoreHit.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Threading.Tasks; using HarmonyLib; using JetBrains.Annotations; using Osu.Performance; @@ -28,7 +29,6 @@ private static void After( [HarmonyArgument(0)] int increaseScoreType, [HarmonyArgument(2)] bool increaseCombo) { - Console.WriteLine(increaseScoreType); if (!PerformanceCalculator.IsInitialized) { Console.WriteLine("OnIncreaseScoreHit called before performance calculator initialized!"); @@ -53,7 +53,7 @@ private static void After( var CurrentScore = Ruleset.CurrentScore.Get(__instance); var MaxCombo = Score.MaxCombo.Get(CurrentScore); - PerformanceCalculator.Calculator?.AddJudgement(judgement, (uint)MaxCombo); + Task.Run(() => PerformanceCalculator.Calculator?.AddJudgement(judgement, (uint)MaxCombo)); } [UsedImplicitly] diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackResetScore.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackResetScore.cs index e30e2a9..b2c3b1b 100644 --- a/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackResetScore.cs +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PatchTrackResetScore.cs @@ -8,7 +8,7 @@ namespace Osu.Patcher.Hook.Patches.LivePerformance; /// -/// Hooks Ruleset::ResetScore() to also reset our performance calculator. +/// Hooks Ruleset::ResetScore() to also reset our performance calculator and caches. /// [HarmonyPatch] [UsedImplicitly] diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceCalculator.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceCalculator.cs index e2abaa2..abfdaed 100644 --- a/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceCalculator.cs +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceCalculator.cs @@ -54,7 +54,7 @@ private static void ResetCalculatorSync() if (beatmapPath == null) return; Calculator = new OsuPerformance(beatmapPath, (uint)mods); - Calculator.OnNewCalculation += Console.WriteLine; + Calculator.OnNewCalculation += PerformanceDisplay.UpdatePerformanceCounter; Debug.WriteLine("Initialized performance calculator!"); } diff --git a/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceDisplay.cs b/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceDisplay.cs new file mode 100644 index 0000000..3372f03 --- /dev/null +++ b/Osu.Patcher.Hook/Patches/LivePerformance/PerformanceDisplay.cs @@ -0,0 +1,38 @@ +using System; +using Osu.Stubs; + +namespace Osu.Patcher.Hook.Patches.LivePerformance; + +internal static class PerformanceDisplay +{ + /// + /// The last known patched instance of our pSpriteText performance counter sprite. + /// + private static readonly WeakReference PerformanceCounter = new(null); + + /// + /// Set a new active performance counter to update. + /// + /// The pSpriteText performance counter sprite. + public static void SetPerformanceCounter(object sprite) => + PerformanceCounter.SetTarget(sprite); + + /// + /// Change the pp value for the currently active performance counter. + /// + public static void UpdatePerformanceCounter(double pp) + { + try + { + if (!PerformanceCounter.TryGetTarget(out var sprite) || sprite == null) + return; + + // Technically this should be run with "GameBase.Scheduler.AddOnce(() => ...)" but it works anyways, so... + pText.SetText.Invoke(sprite, [$"{pp:00.0}pp"]); + } + catch (Exception e) + { + Console.WriteLine($"Failed to set performance counter sprite text: {e}"); + } + } +} \ No newline at end of file diff --git a/Osu.Stubs/Clocks.cs b/Osu.Stubs/Clocks.cs new file mode 100644 index 0000000..1caae6e --- /dev/null +++ b/Osu.Stubs/Clocks.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Sprites.Clocks +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public class Clocks +{ + public const int Game = 0; + public const int Audio = 0; + public const int AudioOnce = 0; +} \ No newline at end of file diff --git a/Osu.Stubs/Color.cs b/Osu.Stubs/Color.cs index c3cc923..637544a 100644 --- a/Osu.Stubs/Color.cs +++ b/Osu.Stubs/Color.cs @@ -11,6 +11,7 @@ public static class Color { public static readonly XnaColor Red = GetColor("Red"); public static readonly XnaColor Orange = GetColor("Orange"); + public static readonly XnaColor White = GetColor("White"); public static readonly XnaColor GhostWhite = GetColor("GhostWhite"); private static XnaColor GetColor(string name) diff --git a/Osu.Stubs/Fields.cs b/Osu.Stubs/Fields.cs new file mode 100644 index 0000000..14b8925 --- /dev/null +++ b/Osu.Stubs/Fields.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Sprites.Fields +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public class Fields +{ + public const int GameField = 1; + public const int GameFieldWide = 2; + public const int Storyboard = 3; + public const int StoryboardCentre = 4; + public const int Native = 5; + public const int TopLeft = 6; + public const int TopCentre = 7; + public const int TopRight = 8; + public const int CentreLeft = 9; + public const int Centre = 10; + public const int CentreRight = 11; + public const int BottomLeft = 12; + public const int BottomCentre = 13; + public const int BottomRight = 14; + public const int StandardGameFieldScale = 15; + public const int NativeStandardScale = 16; + public const int NativeRight = 17; + public const int NativeBottomRight = 18; + public const int NativeBottomCentre = 19; +} \ No newline at end of file diff --git a/Osu.Stubs/Logger.cs b/Osu.Stubs/Logger.cs index 7860087..0f76e0d 100644 --- a/Osu.Stubs/Logger.cs +++ b/Osu.Stubs/Logger.cs @@ -31,6 +31,7 @@ public class Logger Leave_S, Ret, }, + false, true ); } \ No newline at end of file diff --git a/Osu.Stubs/Obfuscated.cs b/Osu.Stubs/Obfuscated.cs index 1bad808..54577b1 100644 --- a/Osu.Stubs/Obfuscated.cs +++ b/Osu.Stubs/Obfuscated.cs @@ -19,7 +19,7 @@ public class Obfuscated Stsfld, Ldc_I4, Ldc_I4_0, - Callvirt, // This is a reference to Scheduler::AddDelayed, TODO: use this? + Callvirt, Pop, Leave_S, Ldarg_0, diff --git a/Osu.Stubs/Opcode/LazyField.cs b/Osu.Stubs/Opcode/LazyField.cs index 339ec0e..1a7865c 100644 --- a/Osu.Stubs/Opcode/LazyField.cs +++ b/Osu.Stubs/Opcode/LazyField.cs @@ -45,4 +45,19 @@ public FieldInfo Reference /// The current field value casted to the type defined by this LazyField. public T Get(object? instance = null) => (T)Reference.GetValue(instance); + + /// + /// Set a instance field to a specific value. + /// + /// An instance of the field's enclosing class. + /// The new value. + public void Set(object instance, T value) => + Reference.SetValue(instance, value); + + /// + /// Set a static field to a specific value. + /// + /// The new value. + public void Set(T value) => + Reference.SetValue(null, value); } \ No newline at end of file diff --git a/Osu.Stubs/Opcode/LazyMethod.cs b/Osu.Stubs/Opcode/LazyMethod.cs index 9625a48..e4571ab 100644 --- a/Osu.Stubs/Opcode/LazyMethod.cs +++ b/Osu.Stubs/Opcode/LazyMethod.cs @@ -18,11 +18,16 @@ public class LazyMethod /// /// Class#Method name of what this signature is matching. /// Sequential opcodes to search the target method with. + /// Whether this method is a constructor. /// Whether the signature is the entire method. - internal LazyMethod(string name, IReadOnlyList signature, bool entireMethod = false) + internal LazyMethod(string name, IReadOnlyList signature, + bool isConstructor = false, + bool entireMethod = false) { _name = name; - _lazy = new Lazy(() => OpCodeMatcher.FindMethodBySignature(signature, entireMethod)); + _lazy = new Lazy(() => isConstructor + ? OpCodeMatcher.FindConstructorBySignature(signature, entireMethod) + : OpCodeMatcher.FindMethodBySignature(signature, entireMethod)); } /// @@ -66,8 +71,10 @@ public void Invoke(object? instance = null, object?[]? parameters = null) => public class LazyMethod : LazyMethod { /// - internal LazyMethod(string name, IReadOnlyList signature) - : base(name, signature) + internal LazyMethod(string name, IReadOnlyList signature, + bool isConstructor = false, + bool entireMethod = false) + : base(name, signature, isConstructor, entireMethod) { } diff --git a/Osu.Stubs/Opcode/OpCodeMatcher.cs b/Osu.Stubs/Opcode/OpCodeMatcher.cs index 67f1a4b..d815917 100644 --- a/Osu.Stubs/Opcode/OpCodeMatcher.cs +++ b/Osu.Stubs/Opcode/OpCodeMatcher.cs @@ -36,8 +36,10 @@ public static class OpCodeMatcher /// Search for a constructor inside the osu! assembly by an IL OpCode signature. /// /// A set of sequential OpCodes to match. + /// Whether the signature is the entire method to search for. /// The found constructor (method) or null if none found. - public static ConstructorInfo? FindConstructorBySignature(IReadOnlyList signature) + public static ConstructorInfo? FindConstructorBySignature(IReadOnlyList signature, + bool entireMethod = false) { if (signature.Count <= 0) return null; @@ -50,7 +52,7 @@ public static class OpCodeMatcher var instructions = method.GetMethodBody()?.GetILAsByteArray(); if (instructions == null) continue; - if (InstructionsMatchesSignature(instructions, signature, false)) + if (InstructionsMatchesSignature(instructions, signature, entireMethod)) return method; } diff --git a/Osu.Stubs/Origins.cs b/Osu.Stubs/Origins.cs new file mode 100644 index 0000000..bc9977a --- /dev/null +++ b/Osu.Stubs/Origins.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Sprites.Origins +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public class Origins +{ + public const int TopLeft = 0; + public const int Centre = 1; + public const int CentreLeft = 2; + public const int TopRight = 3; + public const int BottomCentre = 4; + public const int TopCentre = 5; + public const int Custom = 6; + public const int CentreRight = 7; + public const int BottomLeft = 8; + public const int BottomRight = 9; +} \ No newline at end of file diff --git a/Osu.Stubs/Ruleset.cs b/Osu.Stubs/Ruleset.cs index d8ce921..f3cd0ea 100644 --- a/Osu.Stubs/Ruleset.cs +++ b/Osu.Stubs/Ruleset.cs @@ -3,6 +3,7 @@ using System.Reflection; using JetBrains.Annotations; using Osu.Stubs.Opcode; +using Osu.Stubs.Utils; using static System.Reflection.Emit.OpCodes; namespace Osu.Stubs; @@ -78,6 +79,31 @@ public static class Ruleset } ); + /// + /// Original: Initialize() + /// b20240123: + /// + [UsedImplicitly] + public static readonly LazyMethod Initialize = new( + "Ruleset#Initialize()", + new[] + { + Ldarg_0, + Callvirt, + }.Duplicate(8) + ); + + /// + /// Original: CurrentScore + /// b20240123: Instance + /// + [UsedImplicitly] + public static readonly LazyField Instance = new( + "Ruleset#Instance", + () => RuntimeType.GetRuntimeFields() + .Single(field => field.FieldType == RuntimeType) + ); + /// /// Original: CurrentScore /// b20240123: #=zk4sdboE= @@ -89,6 +115,17 @@ public static class Ruleset .Single(field => field.FieldType == Score.RuntimeType) ); + /// + /// Original: ScoreDisplay + /// b20240123: TODO: find this + /// + [UsedImplicitly] + public static readonly LazyField ScoreDisplay = new( + "Ruleset#ScoreDisplay", + () => RuntimeType.GetRuntimeFields() + .Single(field => field.FieldType == Stubs.ScoreDisplay.RuntimeType) + ); + [UsedImplicitly] public static Type RuntimeType => OnIncreaseScoreHit.Reference.DeclaringType!; } \ No newline at end of file diff --git a/Osu.Stubs/ScoreDisplay.cs b/Osu.Stubs/ScoreDisplay.cs new file mode 100644 index 0000000..ad6a1ad --- /dev/null +++ b/Osu.Stubs/ScoreDisplay.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +/// +/// Original: osu.GameModes.Play.Components.ScoreDisplay +/// b20240124: #=z6dniqZasYGnUF21A3FQQhhWHV7POD$6AVg== +/// +[UsedImplicitly] +public class ScoreDisplay +{ + /// + /// Original: + /// + /// ScoreDisplay(SpriteManager spriteManager, + /// Vector2 position, + /// bool alignRight, + /// float scale, + /// bool showScore, + /// bool showAccuracy) + /// + /// b20240124: Same as class + /// + [UsedImplicitly] + public static readonly LazyMethod Constructor = new( + "ScoreDisplay::(...)", + new[] + { + Conv_R4, + Ldarg_3, + Brtrue_S, + Ldc_I4_6, + Br_S, + Ldc_I4_8, + Ldarg_3, + Brtrue_S, + Ldc_I4_0, + Br_S, + }, + true + ); + + /// + /// Original: Hide() + /// b20240123: #=zRjDThRI= + /// + [UsedImplicitly] + public static readonly LazyMethod Hide = new( // TODO: support hiding performance counter + "ScoreDisplay#Hide()", + new[] + { + Ldc_I4_0, + Ldc_I4_0, + Ldc_I4_0, + Callvirt, + Ldarg_0, + Ldfld, + Brfalse_S, + Ldarg_0, + Ldfld, + Ldc_I4_0, + Ldc_I4_0, + Ldc_I4_0, + Callvirt, + Ret, + } + ); + + /// + /// Original: spriteManager + /// b20240123: #=zK4XquDeTazcx + /// + [UsedImplicitly] + public static readonly LazyField SpriteManager = new( + "ScoreDisplay#spriteManager", + () => RuntimeType.GetDeclaredFields() + .Single(field => field.FieldType == Stubs.SpriteManager.RuntimeType) + ); + + [UsedImplicitly] + public static Type RuntimeType => Constructor.Reference.DeclaringType!; +} \ No newline at end of file diff --git a/Osu.Stubs/SkinManager.cs b/Osu.Stubs/SkinManager.cs new file mode 100644 index 0000000..be3238a --- /dev/null +++ b/Osu.Stubs/SkinManager.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Skinning.SkinManager +/// b20240123: #=zdwZLyAQXwqtPhTfOQ$e2PRLm39DcCX13EA== +/// +[UsedImplicitly] +public static class SkinManager +{ + /// + /// Original: get_UseNewLayout() + /// b20240123: #=zOwgqVurLFLwR + /// + [UsedImplicitly] + public static readonly LazyMethod GetUseNewLayout = new( + "SkinManager#get_UseNewLayout()", + new[] + { + Ldsfld, + Brfalse_S, + Call, + Brtrue_S, + Ldsfld, + Brfalse_S, + Ldsfld, + Ldfld, + } + ); + + /// + /// Original: Current + /// b20240123: #=zUzFTHbU= + /// + [UsedImplicitly] + public static readonly LazyField Current = new( + "SkinManager#Current", + // There is two fields with type SkinOsu; Current and CurrentUserSkin in that order + () => RuntimeType.GetDeclaredFields() + .First(f => f.FieldType == SkinOsu.RuntimeType) + ); + + [UsedImplicitly] + public static Type RuntimeType = GetUseNewLayout.Reference.DeclaringType!; +} \ No newline at end of file diff --git a/Osu.Stubs/SkinOsu.cs b/Osu.Stubs/SkinOsu.cs new file mode 100644 index 0000000..9d57781 --- /dev/null +++ b/Osu.Stubs/SkinOsu.cs @@ -0,0 +1,40 @@ +using System; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Skinning.OsuSkin +/// b20240123: osu.Graphics.Skinning.OsuSkin +/// Most names are present because of this class is [Serializable]. +/// +[UsedImplicitly] +public static class SkinOsu +{ + /// + /// Original: FontScoreOverlap + /// b20240123: FontScoreOverlap + /// + [UsedImplicitly] + public static readonly LazyField FontScoreOverlap = new( + "OsuSkin#FontScoreOverlap", + () => RuntimeType.GetDeclaredFields() + .Find(f => f.Name == "FontScoreOverlap") + ); + + /// + /// Original: FontScore + /// b20240123: FontScore + /// + [UsedImplicitly] + public static readonly LazyField FontScore = new( + "OsuSkin#FontScore", + () => RuntimeType.GetDeclaredFields() + .Find(f => f.Name == "FontScore") + ); + + [UsedImplicitly] + public static Type RuntimeType => OsuAssembly.GetType("osu.Graphics.Skinning.SkinOsu"); +} \ No newline at end of file diff --git a/Osu.Stubs/SkinSource.cs b/Osu.Stubs/SkinSource.cs new file mode 100644 index 0000000..5868cb4 --- /dev/null +++ b/Osu.Stubs/SkinSource.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Skinning.SkinSource +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class SkinSource +{ + public const int None = 0; + public const int Osu = 1; + public const int Skin = 2; + public const int Beatmap = 4; + public const int Temporal = 8; + public const int ExceptBeatmap = Osu | Skin; + public const int All = Osu | Skin | Beatmap; +} \ No newline at end of file diff --git a/Osu.Stubs/SpriteManager.cs b/Osu.Stubs/SpriteManager.cs new file mode 100644 index 0000000..30a4165 --- /dev/null +++ b/Osu.Stubs/SpriteManager.cs @@ -0,0 +1,38 @@ +using System; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.Sprites.SpriteManager +/// b20240123: #=zaNwi4uR9iF1HqyG9UwEA2vinmw4mMbeYaQ== +/// +[UsedImplicitly] +public class SpriteManager +{ + /// + /// Original: Add(pDrawable p) + /// b20240123: #=zJdXS36o= + /// + [UsedImplicitly] + public static readonly LazyMethod Add = new( + "SpriteManager#Add(...)", + new[] + { + Not, + Ldarg_1, + Callvirt, + Leave_S, + Ldloc_2, + Brfalse_S, + Ldloc_1, + Call, + Endfinally, + } + ); + + [UsedImplicitly] + public static Type RuntimeType => Add.Reference.DeclaringType!; +} \ No newline at end of file diff --git a/Osu.Stubs/Utils/Extensions.cs b/Osu.Stubs/Utils/Extensions.cs index 0f2e67b..758a8c5 100644 --- a/Osu.Stubs/Utils/Extensions.cs +++ b/Osu.Stubs/Utils/Extensions.cs @@ -7,4 +7,15 @@ public static class Extensions [UsedImplicitly] public static T? GetOrDefault(this T[] array, int index, T? defaultValue) => index < array.Length ? array[index] : defaultValue; + + [UsedImplicitly] + public static T[] Duplicate(this T[] array, int times) + { + var newArray = new T[array.Length * times]; + + for (var i = 0; i < times; i++) + array.CopyTo(newArray, i * array.Length); + + return newArray; + } } \ No newline at end of file diff --git a/Osu.Stubs/Vector2.cs b/Osu.Stubs/Vector2.cs new file mode 100644 index 0000000..61d1d77 --- /dev/null +++ b/Osu.Stubs/Vector2.cs @@ -0,0 +1,33 @@ +using System; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; + +namespace Osu.Stubs; + +[UsedImplicitly] +public class Vector2 +{ + [UsedImplicitly] + public static Type RuntimeType = OsuAssembly.GetType("Microsoft.Xna.Framework.Vector2"); + + /// + /// Constructor that creates a Vector2 from two distinct float values. + /// + [UsedImplicitly] + public static LazyMethod Constructor = new( + "Vector2#(float, float)", + () => RuntimeType.GetConstructor([typeof(float), typeof(float)]) + ); + + [UsedImplicitly] + public static LazyField X = new( + "Vector2#X", + () => RuntimeType.GetField("X") + ); + + [UsedImplicitly] + public static LazyField Y = new( + "Vector2#Y", + () => RuntimeType.GetField("Y") + ); +} \ No newline at end of file diff --git a/Osu.Stubs/pDrawable.cs b/Osu.Stubs/pDrawable.cs new file mode 100644 index 0000000..be1c0ec --- /dev/null +++ b/Osu.Stubs/pDrawable.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +/// +/// Original: osu.Graphics.pDrawable +/// b20240123: #=zB63SnFDTnRqYMKLlscCRpu_ww$IG +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static class pDrawable +{ + /// + /// Original: Click(bool confirmed) + /// b20240123: #=zcJ6mazw= + /// + [UsedImplicitly] + public static readonly LazyMethod Click = new( + "pDrawable#Click(...)", + new[] + { + Ret, + Ldarg_0, + Ldfld, + Ldnull, + Cgt_Un, + Dup, + Brfalse_S, + Ldarg_0, + Ldfld, + } + ); + + /// + /// Original: ScaleTo(float final, int duration, EasingTypes easing) + /// b20240123: #=zVyF2njk= + /// + [UsedImplicitly] + public static readonly LazyMethod ScaleTo = new( + "pDrawable#ScaleTo(...)", + new[] + { + Ldc_I4_4, // TransformationType.Scale + Ldarg_0, + Ldfld, // this.Scale + Conv_R4, + Ldarg_1, + Ldarg_0, + Ldfld, + Call, + Ldsfld, + Conv_I4, + Sub, + } + ); + + /// + /// Original: Position + /// b20240123: #=ztOn8vDI= + /// + [UsedImplicitly] + public static readonly LazyField Position = new( + "pDrawable#Position", + // There is 3 fields with a type of Vector2 on this class. The middle one is Position. + () => RuntimeType.GetDeclaredFields().AsEnumerable() + .Reverse() + .Where(field => field.FieldType == Vector2.RuntimeType) + .Skip(1) + .First() + ); + + /// + /// Original: Scale + /// b20240123: #=zmbpQ79A= + /// + [UsedImplicitly] + public static readonly LazyField Scale = new( + "pDrawable#Scale", + () => + { + // The last "ldsfld float" in the ScaleTo method is a reference to this.Scale + var instruction = MethodReader.GetInstructions(ScaleTo.Reference) + .Reverse() + .Where(inst => inst.Opcode == Ldfld) + .First(inst => ((FieldInfo)inst.Operand).FieldType == typeof(float)); + + return (FieldInfo)instruction.Operand; + } + ); + + [UsedImplicitly] + public static Type RuntimeType => ScaleTo.Reference.DeclaringType!; +} \ No newline at end of file diff --git a/Osu.Stubs/pSpriteText.cs b/Osu.Stubs/pSpriteText.cs new file mode 100644 index 0000000..8b4d36a --- /dev/null +++ b/Osu.Stubs/pSpriteText.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +[UsedImplicitly] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class pSpriteText +{ + /// + /// Original: + /// + /// pSpriteText(string text, string fontname, float spacingOverlap, Fields fieldType, Origins origin, + /// Clocks clock, Vector2 startPosition, float drawDepth, bool alwaysDraw, Color colour, + /// bool precache = true, SkinSource source = (SkinSource)7) + /// + /// b20240123: + /// + public static readonly LazyMethod Constructor = new( + "pSpriteText#(...)", + new[] + { + Ldarg_2, + Ldarg_S, + Call, + Stfld, + Ldarg_S, + Brfalse, + Ldc_I4_0, + Stloc_0, + Br_S, + Ldarg_0, + Ldfld, + Ldloca_S, + }, + true + ); + + /// + /// Updates internal pSpriteText state and returns a Vector2 of the size. + /// Original: MeasureText() + /// b20240123: #=z2Klmy0o= + /// + [UsedImplicitly] + public static readonly LazyMethod MeasureText = new( + "pSpriteText#MeasureText()", + new[] + { + Ldarg_0, + Ldfld, + Brfalse_S, + Ldarg_0, + Callvirt, + Pop, + Ldarg_0, + Ldfld, + Ret, + } + ); + + /// + /// Original: TextConstantSpacing + /// b20240123: #=zWUFISsTiUxtU + /// + [UsedImplicitly] + public static readonly LazyField TextConstantSpacing = new( + "pSpriteText#TextConstantSpacing", + () => RuntimeType.GetDeclaredFields() + .Single(field => field.FieldType == typeof(bool)) + ); + + [UsedImplicitly] + public static Type RuntimeType => Constructor.Reference.DeclaringType!; +} \ No newline at end of file diff --git a/Osu.Stubs/pText.cs b/Osu.Stubs/pText.cs new file mode 100644 index 0000000..7a374b9 --- /dev/null +++ b/Osu.Stubs/pText.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Osu.Stubs.Opcode; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs; + +/// +/// pSpriteUnloadable that handles text. This is the base class of pSpriteText. +/// Original: osu.Graphics.Sprites.pText +/// b20240123: +/// +[UsedImplicitly] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class pText +{ + /// + /// Original: set_Text(string value) (property setter) + /// b20240123: + /// + [UsedImplicitly] + public static readonly LazyMethod SetText = new( + "pText#set_Text(...)", + new[] + { + Ldarg_0, + Ldfld, + Ldarg_1, + Call, + Brfalse_S, + Ret, + Ldarg_0, + Ldc_I4_1, + Stfld, + Ldarg_0, + Ldarg_1, + Stfld, + Ret, + }, + entireMethod: true + ); +} \ No newline at end of file