From 443f09de94495ae9d7d7819a48cf65ab13a4a6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 10:47:54 +0200 Subject: [PATCH 1/6] Move common const out to `LegacySkin` This `1.6f` constant repeats too much everywhere and I am not about to repeat it once more. I will do a pass on all of the places that inline this (and there are many) as a follow-up. --- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 2 +- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 15 +++++---------- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 12 ++++++------ osu.Game/Skinning/LegacySkin.cs | 5 +++++ osu.Game/Skinning/LegacySkinEncoder.cs | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 5ece5df66fb8..7f4fa2613723 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -52,7 +52,7 @@ private void onDirectionChanged() float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - float hitPositionFromTop = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + float hitPositionFromTop = 480f * LegacySkin.POSITION_SCALE_FACTOR - hitPosition; if (scorePosition > hitPositionFromTop / 2f) { diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 58c6585e27b9..68b3a1483341 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -11,17 +11,12 @@ namespace osu.Game.Skinning { public class LegacyManiaSkinConfiguration : IHasCustomColours { - /// - /// Conversion factor from converting legacy positioning values (based in x480 dimensions) to x768. - /// - public const float POSITION_SCALE_FACTOR = 1.6f; - /// /// Size of a legacy column in the default skin, used for determining relative scale factors. /// - public const float DEFAULT_COLUMN_SIZE = 30 * POSITION_SCALE_FACTOR; + public const float DEFAULT_COLUMN_SIZE = 30 * LegacySkin.POSITION_SCALE_FACTOR; - public const float DEFAULT_HIT_POSITION = (480 - 402) * POSITION_SCALE_FACTOR; + public const float DEFAULT_HIT_POSITION = (480 - 402) * LegacySkin.POSITION_SCALE_FACTOR; public readonly int Keys; @@ -38,9 +33,9 @@ public class LegacyManiaSkinConfiguration : IHasCustomColours public readonly float[] HoldNoteLightWidth; public float HitPosition = DEFAULT_HIT_POSITION; - public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; - public float ComboPosition = 111 * POSITION_SCALE_FACTOR; - public float ScorePosition = 300 * POSITION_SCALE_FACTOR; + public float LightPosition = (480 - 413) * LegacySkin.POSITION_SCALE_FACTOR; + public float ComboPosition = 111 * LegacySkin.POSITION_SCALE_FACTOR; + public float ScorePosition = 300 * LegacySkin.POSITION_SCALE_FACTOR; public float BarLineHeight = 1.2f; public bool ShowJudgementLine = true; public bool KeysUnderNotes; diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 94bd79fea3de..6f7634a04106 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -91,19 +91,19 @@ private void flushPendingLines() break; case "HitPosition": - currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacySkin.POSITION_SCALE_FACTOR; break; case "LightPosition": - currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacySkin.POSITION_SCALE_FACTOR; break; case "ComboPosition": - currentConfig.ComboPosition = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.ComboPosition = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacySkin.POSITION_SCALE_FACTOR; break; case "ScorePosition": - currentConfig.ScorePosition = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.ScorePosition = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacySkin.POSITION_SCALE_FACTOR; break; case "JudgementLine": @@ -128,7 +128,7 @@ private void flushPendingLines() break; case "WidthForNoteHeightScale": - currentConfig.WidthForNoteHeightScale = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.WidthForNoteHeightScale = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacySkin.POSITION_SCALE_FACTOR; break; case "LightFramePerSecond": @@ -209,7 +209,7 @@ private void parseArrayValue(string value, float[] output, bool applyScaleFactor parsedValue = 0; if (applyScaleFactor) - parsedValue *= LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + parsedValue *= LegacySkin.POSITION_SCALE_FACTOR; output[i] = parsedValue; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 219f909403f9..8443ae784f23 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -30,6 +30,11 @@ namespace osu.Game.Skinning { public class LegacySkin : Skin { + /// + /// Conversion factor from converting legacy positioning values (based in x480 dimensions) to x768. + /// + public const float POSITION_SCALE_FACTOR = 1.6f; + protected virtual bool AllowManiaConfigLookups => true; /// diff --git a/osu.Game/Skinning/LegacySkinEncoder.cs b/osu.Game/Skinning/LegacySkinEncoder.cs index 326af9b991e3..9e5f6cff4f7c 100644 --- a/osu.Game/Skinning/LegacySkinEncoder.cs +++ b/osu.Game/Skinning/LegacySkinEncoder.cs @@ -110,10 +110,10 @@ public void Encode(TextWriter textWriter) writeValue(textWriter, @"JudgementLine", maniaConfig.ShowJudgementLine ? @"1" : @"0"); writeValue(textWriter, @"BarlineHeight", maniaConfig.BarLineHeight.ToString(CultureInfo.InvariantCulture), defaultValue: @"1.2"); - float hitPosition = 480 - (maniaConfig.HitPosition / LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR); + float hitPosition = 480 - (maniaConfig.HitPosition / LegacySkin.POSITION_SCALE_FACTOR); writeValue(textWriter, @"HitPosition", hitPosition.ToString(CultureInfo.InvariantCulture), defaultValue: @"402"); - float lightPosition = 480 - (maniaConfig.LightPosition / LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR); + float lightPosition = 480 - (maniaConfig.LightPosition / LegacySkin.POSITION_SCALE_FACTOR); writeValue(textWriter, @"LightPosition", lightPosition.ToString(CultureInfo.InvariantCulture), defaultValue: @"413"); writeValue(textWriter, @"ComboPosition", undoPositionScaleFactor(maniaConfig.ComboPosition).ToString(CultureInfo.InvariantCulture), defaultValue: @"111"); @@ -189,7 +189,7 @@ private void writeGenericColour(TextWriter textWriter, Dictionary f / LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + private float undoPositionScaleFactor(float f) => f / LegacySkin.POSITION_SCALE_FACTOR; private string enumerableToString(IEnumerable ts) where T : IFormattable From 04729cf0801c7ed8697dba3663f88e42d81a490d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 11:57:44 +0200 Subject: [PATCH 2/6] Implement legacy bar hit error meter --- .../Visual/Gameplay/TestSceneHitErrorMeter.cs | 23 +++ .../HitErrorMeters/LegacyBarHitErrorMeter.cs | 151 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 24215ed92512..a71acb701501 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -247,6 +247,29 @@ private void recreateDisplay(HitWindows hitWindows, float overallDifficulty) Rotation = 270, Margin = new MarginPadding { Left = 50 } }); + + Add(new LegacyBarHitErrorMeter + { + Anchor = Anchor.CentreRight, + Origin = Anchor.BottomCentre, + Rotation = 90, + X = -100, + }); + + Add(new LegacyBarHitErrorMeter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomCentre, + Rotation = 90, + X = 100, + }); + + Add(new LegacyBarHitErrorMeter + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = -100 + }); } private void newJudgement(double offset = 0, HitResult result = HitResult.Perfect) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs new file mode 100644 index 000000000000..6364f5d601cd --- /dev/null +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs @@ -0,0 +1,151 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD.HitErrorMeters +{ + public partial class LegacyBarHitErrorMeter : HitErrorMeter + { + private const float bar_height = 3; + + private (HitResult result, double length)[] hitWindows = null!; + private DrawablePool judgementLinePool = null!; + private Container judgementContainer = null!; + private Triangle arrow = null!; + + private double maxHitWindow = 1; + private float floatingError; + + [BackgroundDependencyLoader] + private void load() + { + Container colourBarContainer; + + hitWindows = HitWindows.GetAllAvailableWindows().Where(w => w.result.IsHit()).ToArray(); + + AutoSizeAxes = Axes.X; + Height = (bar_height * 4) * LegacySkin.POSITION_SCALE_FACTOR; + Origin = Anchor.Centre; + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black, + Alpha = 0.6f, + }, + colourBarContainer = new Container + { + AutoSizeAxes = Axes.X, + Height = bar_height * LegacySkin.POSITION_SCALE_FACTOR, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1.5f, bar_height * 4) * LegacySkin.POSITION_SCALE_FACTOR, + }, + judgementLinePool = new DrawablePool(50), + judgementContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + arrow = new Triangle + { + Size = new Vector2(17, 8) * 0.6f, + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + RelativePositionAxes = Axes.X, + Scale = new Vector2(1, -1), + } + }; + + foreach (var hitWindow in hitWindows) + { + maxHitWindow = Math.Max(maxHitWindow, hitWindow.length); + + colourBarContainer.Add(new Box + { + Size = new Vector2((float)hitWindow.length, bar_height) * LegacySkin.POSITION_SCALE_FACTOR, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = GetColourForHitResult(hitWindow.result), + }); + } + } + + protected override void OnNewJudgement(JudgementResult judgement) + { + if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0) + return; + + if (!judgement.Type.IsScorable() || judgement.Type.IsBonus()) + return; + + float relativePosition = getRelativeJudgementPosition(judgement.TimeOffset); + + judgementLinePool.Get(drawableJudgement => + { + drawableJudgement.X = relativePosition; + drawableJudgement.Colour = GetColourForHitResult(judgement.Type); + + judgementContainer.Add(drawableJudgement); + }); + + floatingError = floatingError * 0.8f + relativePosition * 0.2f; + arrow.MoveToX(floatingError, 800, Easing.Out); + } + + private float getRelativeJudgementPosition(double value) => Math.Clamp((float)(value / maxHitWindow) / 2, -0.5f, 0.5f); + + public override void Clear() + { + foreach (var j in judgementContainer) + { + j.ClearTransforms(); + j.Expire(); + } + } + + internal partial class JudgementLine : PoolableDrawable + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Y; + Width = 3; + RelativePositionAxes = Axes.X; + Blending = BlendingParameters.Additive; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + this.FadeTo(0.4f) + .Then() + .FadeOut(10000) + .Expire(); + } + } + } +} From 357e5e4fcd6343742bc800953c6f1f42fd8610bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 12:07:34 +0200 Subject: [PATCH 3/6] Fix hit error meters not looking correctly in layouts test scene --- .../Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs index 131b901d6f21..7a8950a4f1d7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlayRulesetLayouts.cs @@ -22,6 +22,7 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -101,6 +102,7 @@ public void TestLayout( (typeof(IGameplayClock), gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false)), (typeof(SpectatorClient), spectatorClient), (typeof(IGameplayLeaderboardProvider), new TestGameplayLeaderboardProvider()), + (typeof(DrawableRuleset), ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(ruleset.RulesetInfo).GetPlayableBeatmap(ruleset.RulesetInfo))) ]; if (drawableRuleset is IDrawableScrollingRuleset scrolling) From 41f5b326f0be3d3c39ec3a11be6c872f5fab50e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 12:07:43 +0200 Subject: [PATCH 4/6] Use legacy bar hit error meter in legacy skin --- osu.Game/Skinning/LegacySkin.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 8443ae784f23..ed744891f9c8 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -453,8 +453,7 @@ protected override void ParseConfigurationStream(Stream stream) if (hitError != null) { hitError.Anchor = Anchor.BottomCentre; - hitError.Origin = Anchor.CentreLeft; - hitError.Rotation = -90; + hitError.Origin = Anchor.BottomCentre; } foreach (var d in container.OfType()) @@ -466,7 +465,7 @@ protected override void ParseConfigurationStream(Stream stream) new LegacyScoreCounter(), new LegacyAccuracyCounter(), new LegacySongProgress(), - new BarHitErrorMeter(), + new LegacyBarHitErrorMeter(), // to match stable, health bars are in front of everything else // for the sake of hacky full screen area health bars From 310236c00ec7eca0b54224d2572e843f77d7536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 13:49:40 +0200 Subject: [PATCH 5/6] Add backreference to stable implementation --- .../Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs index 6364f5d601cd..76e9a2e45cd1 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/LegacyBarHitErrorMeter.cs @@ -15,6 +15,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { + /// public partial class LegacyBarHitErrorMeter : HitErrorMeter { private const float bar_height = 3; From 83a698d2b47358c7a82281600a9e653b03a71cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Aug 2026 14:27:04 +0200 Subject: [PATCH 6/6] Add deserialisation test coverage Literally every time with this test. --- .../Archives/modified-classic-20260806.osk | Bin 0 -> 1443 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-classic-20260806.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20260806.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20260806.osk new file mode 100644 index 0000000000000000000000000000000000000000..d90d6ba923f398e569f7dea051824bd67730525c GIT binary patch literal 1443 zcmWIWW@Zs#VBp|jkS%75O}w4A=`aHW!)GAo0OI28%sjo!yiC_#LqBFi0oUJ_M<-}a zjJa`cozorFiIe);95&xN^oB30JF-8edg|f80+n?Qt|~A7)&Gir|NHam-2qFU>}*%J zpX_|{H1j&PZ)fJ)fBy^5x#v+PvNCerM^5saNZ3Uth4%N|p|7 zFTZjnI62hyE#JSbQ?)i<-f#WloRIg{kcIk(OXj}t*!SfFa{!7*PdiN%Zvc958W8hB zJ(`)9mamspoS(P)Sx`X0C+{=9p__sNPU-7~hI9mao%7VuJLl6E7<9qF$iU=U$b}PU z{ZIOw_VHyj>NS~K;!riGJ4o33Q@6t{r_Y-{e>(kH;nBRxqpF>isxv!PXa205SDE=U zp!w^a#%V8D)6<_lO?|fX_0A>BRHr?g@?*&|b+zy&mr{|0t|>dR7*U;?a5%f5l!<}i zwIBn77|^M{iJ5sGp)St(xdr)osd**EU^m~Len0<~fxz+mzr$T@*tH9toYX#EIB2%a zkhiz2dWBZXr0fl^Zk;IGvS`)+-!dGz4CR-#>PkOM_)&fC=b7hv>2l{}mVabkvZ%@Y zp5?yizUoPTl!V#l|Liq*)O@jLma(DYnV$7x$;&T^Z|nZa)3a1Dq|t7owZr1*QN?RZo)3uG`1$t7V_vZ|bI*nDBj`i`aM1l&}9M zr&VRCP5F5JiQR>oRolN7&+}_FsS>zyC4bVhDfz3Wb6s(-;=g&>IZ7CtPvDy>JSERnY^e5Ij~jl9=H?|@ecl^4GpmZ-G&@S^leeic=kH@c&gw|L2`B{hRQoQ2HT*yXPL)*|H?Z% z@+`B05Sfb0TUYDcne)M$f())O zevIuBIxRf8D9HBnrB97bR~-x&WYiUEP7s#mesZ1MboZXa@u|f+$#7 zg{})dt3&lLFf_h|>Vju{bgk%F521AmBd&~yZU%a&Ak463K@Xz