Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pp counter UI #6

Merged
merged 6 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Hooks the constructor of <c>ScoreDisplay</c> to add our own <c>pTextSprite</c> for displaying
/// the performance counter to the ScoreDisplay's sprite manager.
/// This needs [email protected] or score-p.png in your skin's score font assets!
/// </summary>
[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 [email protected]/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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!");
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace Osu.Patcher.Hook.Patches.LivePerformance;

/// <summary>
/// Hooks <c>Ruleset::ResetScore()</c> to also reset our performance calculator.
/// Hooks <c>Ruleset::ResetScore()</c> to also reset our performance calculator and caches.
/// </summary>
[HarmonyPatch]
[UsedImplicitly]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!");
}
Expand Down
38 changes: 38 additions & 0 deletions Osu.Patcher.Hook/Patches/LivePerformance/PerformanceDisplay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using Osu.Stubs;

namespace Osu.Patcher.Hook.Patches.LivePerformance;

internal static class PerformanceDisplay
{
/// <summary>
/// The last known patched instance of our <c>pSpriteText</c> performance counter sprite.
/// </summary>
private static readonly WeakReference<object?> PerformanceCounter = new(null);

/// <summary>
/// Set a new active performance counter to update.
/// </summary>
/// <param name="sprite">The <c>pSpriteText</c> performance counter sprite.</param>
public static void SetPerformanceCounter(object sprite) =>
PerformanceCounter.SetTarget(sprite);

/// <summary>
/// Change the pp value for the currently active performance counter.
/// </summary>
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}");
}
}
}
16 changes: 16 additions & 0 deletions Osu.Stubs/Clocks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;

namespace Osu.Stubs;

/// <summary>
/// Original: <c>osu.Graphics.Sprites.Clocks</c>
/// </summary>
[UsedImplicitly]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class Clocks
{
public const int Game = 0;
public const int Audio = 0;
public const int AudioOnce = 0;
}
1 change: 1 addition & 0 deletions Osu.Stubs/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions Osu.Stubs/Fields.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;

namespace Osu.Stubs;

/// <summary>
/// Original: <c>osu.Graphics.Sprites.Fields</c>
/// </summary>
[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;
}
1 change: 1 addition & 0 deletions Osu.Stubs/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Logger
Leave_S,
Ret,
},
false,
true
);
}
2 changes: 1 addition & 1 deletion Osu.Stubs/Obfuscated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions Osu.Stubs/Opcode/LazyField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,19 @@ public FieldInfo Reference
/// <returns>The current field value casted to the type defined by this LazyField.</returns>
public T Get(object? instance = null) =>
(T)Reference.GetValue(instance);

/// <summary>
/// Set a instance field to a specific value.
/// </summary>
/// <param name="instance">An instance of the field's enclosing class.</param>
/// <param name="value">The new value.</param>
public void Set(object instance, T value) =>
Reference.SetValue(instance, value);

/// <summary>
/// Set a static field to a specific value.
/// </summary>
/// <param name="value">The new value.</param>
public void Set(T value) =>
Reference.SetValue(null, value);
}
15 changes: 11 additions & 4 deletions Osu.Stubs/Opcode/LazyMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ public class LazyMethod
/// </summary>
/// <param name="name"><c>Class#Method</c> name of what this signature is matching.</param>
/// <param name="signature">Sequential opcodes to search the target method with.</param>
/// <param name="isConstructor">Whether this method is a constructor.</param>
/// <param name="entireMethod">Whether the signature is the entire method.</param>
internal LazyMethod(string name, IReadOnlyList<OpCode> signature, bool entireMethod = false)
internal LazyMethod(string name, IReadOnlyList<OpCode> signature,
bool isConstructor = false,
bool entireMethod = false)
{
_name = name;
_lazy = new Lazy<MethodBase?>(() => OpCodeMatcher.FindMethodBySignature(signature, entireMethod));
_lazy = new Lazy<MethodBase?>(() => isConstructor
? OpCodeMatcher.FindConstructorBySignature(signature, entireMethod)
: OpCodeMatcher.FindMethodBySignature(signature, entireMethod));
}

/// <summary>
Expand Down Expand Up @@ -66,8 +71,10 @@ public void Invoke(object? instance = null, object?[]? parameters = null) =>
public class LazyMethod<R> : LazyMethod
{
/// <inheritdoc />
internal LazyMethod(string name, IReadOnlyList<OpCode> signature)
: base(name, signature)
internal LazyMethod(string name, IReadOnlyList<OpCode> signature,
bool isConstructor = false,
bool entireMethod = false)
: base(name, signature, isConstructor, entireMethod)
{
}

Expand Down
6 changes: 4 additions & 2 deletions Osu.Stubs/Opcode/OpCodeMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public static class OpCodeMatcher
/// Search for a constructor inside the osu! assembly by an IL OpCode signature.
/// </summary>
/// <param name="signature">A set of sequential OpCodes to match.</param>
/// <param name="entireMethod">Whether the signature is the entire method to search for.</param>
/// <returns>The found constructor (method) or null if none found.</returns>
public static ConstructorInfo? FindConstructorBySignature(IReadOnlyList<OpCode> signature)
public static ConstructorInfo? FindConstructorBySignature(IReadOnlyList<OpCode> signature,
bool entireMethod = false)
{
if (signature.Count <= 0) return null;

Expand All @@ -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;
}

Expand Down
23 changes: 23 additions & 0 deletions Osu.Stubs/Origins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;

namespace Osu.Stubs;

/// <summary>
/// Original: <c>osu.Graphics.Sprites.Origins</c>
/// </summary>
[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;
}
Loading
Loading