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