From dc588e6d56a8e88eb408d75f7b7d2cb1780e5ef9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Dec 2023 04:58:17 +0300 Subject: [PATCH 001/539] Implement OsuModDepth --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 163 ++++++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 +- 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs new file mode 100644 index 0000000000..9c0e34d0c8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -0,0 +1,163 @@ +using System; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset + { + public override string Name => "Depth"; + public override string Acronym => "DH"; + public override IconUsage? Icon => FontAwesome.Solid.Cube; + public override ModType Type => ModType.Fun; + public override LocalisableString Description => "3D. Almost."; + public override double ScoreMultiplier => 1; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray(); + + private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -100); + + [SettingSource("Maximum Depth", "How far away object appear.", 0)] + public BindableFloat MaxDepth { get; } = new BindableFloat(100) + { + Precision = 10, + MinValue = 50, + MaxValue = 200 + }; + + [SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)] + public BindableBool ShowApproachCircles { get; } = new BindableBool(true); + + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); + + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + // Hide judgment displays and follow points as they won't make any sense. + // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. + drawableRuleset.Playfield.DisplayJudgements.Value = false; + (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + } + + public override void ApplyToDrawableHitObject(DrawableHitObject dho) + { + base.ApplyToDrawableHitObject(dho); + + switch (dho) + { + case DrawableSliderHead: + case DrawableSliderTail: + case DrawableSliderTick: + case DrawableSliderRepeat: + return; + + case DrawableHitCircle: + case DrawableSlider: + dho.Anchor = Anchor.Centre; + break; + } + } + + private void applyTransform(DrawableHitObject drawable, ArmedState state) + { + switch (drawable) + { + case DrawableSliderHead head: + if (!ShowApproachCircles.Value) + { + var hitObject = (OsuHitObject)drawable.HitObject; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + + using (head.BeginAbsoluteSequence(appearTime)) + head.ApproachCircle.Hide(); + } + + break; + + case DrawableSliderTail: + case DrawableSliderTick: + case DrawableSliderRepeat: + return; + + case DrawableHitCircle circle: + + if (!ShowApproachCircles.Value) + { + var hitObject = (OsuHitObject)drawable.HitObject; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + + using (circle.BeginAbsoluteSequence(appearTime)) + circle.ApproachCircle.Hide(); + } + + setStartPosition(drawable); + break; + + case DrawableSlider: + setStartPosition(drawable); + break; + } + } + + private void setStartPosition(DrawableHitObject drawable) + { + var hitObject = (OsuHitObject)drawable.HitObject; + + float d = mappedDepth(MaxDepth.Value); + + using (drawable.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) + { + drawable.MoveTo(positionAtDepth(d, hitObject.Position)); + drawable.ScaleTo(new Vector2(d)); + } + } + + public void Update(Playfield playfield) + { + double time = playfield.Time.Current; + + foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + { + switch (drawable) + { + case DrawableSliderHead: + case DrawableSliderTail: + case DrawableSliderTick: + case DrawableSliderRepeat: + continue; + + case DrawableHitCircle: + case DrawableSlider: + var hitObject = (OsuHitObject)drawable.HitObject; + + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + double moveDuration = hitObject.TimePreempt; + float z = time > appearTime + moveDuration ? 0 : (MaxDepth.Value - (float)((time - appearTime) / moveDuration * MaxDepth.Value)); + + float d = mappedDepth(z); + drawable.Position = positionAtDepth(d, hitObject.Position); + drawable.Scale = new Vector2(d); + break; + } + } + } + + private static float mappedDepth(float depth) => 100 / (depth - camera_position.Z); + + private static Vector2 positionAtDepth(float mappedDepth, Vector2 positionAtZeroDepth) + { + return (positionAtZeroDepth - camera_position.Xy) * mappedDepth; + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index aaf0ab41a0..50e6a5b572 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -211,7 +211,8 @@ namespace osu.Game.Rulesets.Osu new ModAdaptiveSpeed(), new OsuModFreezeFrame(), new OsuModBubbles(), - new OsuModSynesthesia() + new OsuModSynesthesia(), + new OsuModDepth() }; case ModType.System: From cf6e50f73c5f0fabd35141602187981c86842a44 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Dec 2023 05:07:40 +0300 Subject: [PATCH 002/539] Add header and fix typo --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 9c0e34d0c8..b2d52aef42 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -1,3 +1,6 @@ +// 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.Bindables; @@ -27,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -100); - [SettingSource("Maximum Depth", "How far away object appear.", 0)] + [SettingSource("Maximum depth", "How far away objects appear.", 0)] public BindableFloat MaxDepth { get; } = new BindableFloat(100) { Precision = 10, From 937689ee6bb4eeee18b007ea153b36e5c8e6a961 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Dec 2023 05:39:44 +0300 Subject: [PATCH 003/539] Add OsuModDepth as incompatable to other mods --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index f1197ce0cd..06cb9c3419 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray(); public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index dd2befef4e..6dc0d5d522 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index c8c4cd6a14..befee4af5a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!"; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) }; [SettingSource("Attraction strength", "How strong the pull is.", 0)] public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 6f1206382a..1df344648a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModDepth) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 28d459cedb..91feb33931 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Hit objects run away!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) }; [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index b0533d0cfa..59a1342480 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque, // further implementation will be required for supporting that. - public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModDepth) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index 77cf340b95..a5846efdfe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -47,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModRandom), typeof(OsuModSpunOut), typeof(OsuModStrictTracking), - typeof(OsuModSuddenDeath) + typeof(OsuModSuddenDeath), + typeof(OsuModDepth) }).ToArray(); [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 25d05a88a8..9671f53bea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 92a499e735..b6907af119 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray(); private float theta; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index a45338d91f..d14a821541 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) }; private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles From ebcde63caa4aef0696c977c04bbd5d3e2f2ed5da Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Dec 2023 17:13:47 +0300 Subject: [PATCH 004/539] Don't override hitobjects anchor --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index b2d52aef42..5ae9fc4fac 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -53,25 +53,6 @@ namespace osu.Game.Rulesets.Osu.Mods (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } - public override void ApplyToDrawableHitObject(DrawableHitObject dho) - { - base.ApplyToDrawableHitObject(dho); - - switch (dho) - { - case DrawableSliderHead: - case DrawableSliderTail: - case DrawableSliderTick: - case DrawableSliderRepeat: - return; - - case DrawableHitCircle: - case DrawableSlider: - dho.Anchor = Anchor.Centre; - break; - } - } - private void applyTransform(DrawableHitObject drawable, ArmedState state) { switch (drawable) @@ -160,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static Vector2 positionAtDepth(float mappedDepth, Vector2 positionAtZeroDepth) { - return (positionAtZeroDepth - camera_position.Xy) * mappedDepth; + return (positionAtZeroDepth - camera_position.Xy) * mappedDepth + camera_position.Xy; } } } From b90000f7b70f2840d6ef683aacb6043f78add0e3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Dec 2023 17:15:52 +0300 Subject: [PATCH 005/539] Simplify objects depth calculation --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 5ae9fc4fac..f2ea8b9698 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -126,8 +127,7 @@ namespace osu.Game.Rulesets.Osu.Mods var hitObject = (OsuHitObject)drawable.HitObject; double appearTime = hitObject.StartTime - hitObject.TimePreempt; - double moveDuration = hitObject.TimePreempt; - float z = time > appearTime + moveDuration ? 0 : (MaxDepth.Value - (float)((time - appearTime) / moveDuration * MaxDepth.Value)); + float z = Interpolation.ValueAt(Math.Clamp(time, appearTime, hitObject.StartTime), MaxDepth.Value, 0f, appearTime, hitObject.StartTime); float d = mappedDepth(z); drawable.Position = positionAtDepth(d, hitObject.Position); From ec5c7d7830dfac2f37a8e3d6cbc14967c8473f21 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 4 Dec 2023 09:43:09 +0300 Subject: [PATCH 006/539] Add deceleration and rework depth handling --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 105 ++++++++++------------ 1 file changed, 49 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index f2ea8b9698..5054c75e97 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Utils; @@ -30,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray(); private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -100); + private readonly float minDepth = depthForScale(1.5f); [SettingSource("Maximum depth", "How far away objects appear.", 0)] public BindableFloat MaxDepth { get; } = new BindableFloat(100) @@ -58,25 +58,7 @@ namespace osu.Game.Rulesets.Osu.Mods { switch (drawable) { - case DrawableSliderHead head: - if (!ShowApproachCircles.Value) - { - var hitObject = (OsuHitObject)drawable.HitObject; - double appearTime = hitObject.StartTime - hitObject.TimePreempt; - - using (head.BeginAbsoluteSequence(appearTime)) - head.ApproachCircle.Hide(); - } - - break; - - case DrawableSliderTail: - case DrawableSliderTick: - case DrawableSliderRepeat: - return; - case DrawableHitCircle circle: - if (!ShowApproachCircles.Value) { var hitObject = (OsuHitObject)drawable.HitObject; @@ -86,25 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods circle.ApproachCircle.Hide(); } - setStartPosition(drawable); break; - - case DrawableSlider: - setStartPosition(drawable); - break; - } - } - - private void setStartPosition(DrawableHitObject drawable) - { - var hitObject = (OsuHitObject)drawable.HitObject; - - float d = mappedDepth(MaxDepth.Value); - - using (drawable.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) - { - drawable.MoveTo(positionAtDepth(d, hitObject.Position)); - drawable.ScaleTo(new Vector2(d)); } } @@ -114,34 +78,63 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var drawable in playfield.HitObjectContainer.AliveObjects) { - switch (drawable) - { - case DrawableSliderHead: - case DrawableSliderTail: - case DrawableSliderTick: - case DrawableSliderRepeat: - continue; + if (drawable is not DrawableOsuHitObject d) + continue; + switch (d) + { case DrawableHitCircle: case DrawableSlider: - var hitObject = (OsuHitObject)drawable.HitObject; - - double appearTime = hitObject.StartTime - hitObject.TimePreempt; - float z = Interpolation.ValueAt(Math.Clamp(time, appearTime, hitObject.StartTime), MaxDepth.Value, 0f, appearTime, hitObject.StartTime); - - float d = mappedDepth(z); - drawable.Position = positionAtDepth(d, hitObject.Position); - drawable.Scale = new Vector2(d); + processObject(time, d); break; } } } - private static float mappedDepth(float depth) => 100 / (depth - camera_position.Z); - - private static Vector2 positionAtDepth(float mappedDepth, Vector2 positionAtZeroDepth) + private void processObject(double time, DrawableOsuHitObject drawable) { - return (positionAtZeroDepth - camera_position.Xy) * mappedDepth + camera_position.Xy; + var hitObject = drawable.HitObject; + + double baseSpeed = MaxDepth.Value / hitObject.TimePreempt; + double hitObjectDuration = hitObject is Slider s ? s.Duration : 0.0; + double offsetAfterStartTime = hitObjectDuration + hitObject.MaximumJudgementOffset + 500; + double slowSpeed = -minDepth / offsetAfterStartTime; + + float decelerationDistance = MaxDepth.Value * 0.2f; + double decelerationTime = (slowSpeed - baseSpeed) * 2 * decelerationDistance / (slowSpeed * slowSpeed - baseSpeed * baseSpeed); + + float z; + + if (time < hitObject.StartTime - decelerationTime) + { + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime)); + z = Interpolation.ValueAt(Math.Max(time, appearTime), fullDistance, decelerationDistance, appearTime, hitObject.StartTime - decelerationTime); + } + else if (time < hitObject.StartTime) + { + double timeOffset = time - (hitObject.StartTime - decelerationTime); + double deceleration = (slowSpeed - baseSpeed) / decelerationTime; + z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5); + } + else + { + double endTime = hitObject.StartTime + offsetAfterStartTime; + z = Interpolation.ValueAt(Math.Min(time, endTime), 0f, minDepth, hitObject.StartTime, endTime); + } + + float scale = scaleForDepth(z); + drawable.Position = positionAtDepth(scale, hitObject.Position); + drawable.Scale = new Vector2(scale); + } + + private static float scaleForDepth(float depth) => 100 / (depth - camera_position.Z); + + private static float depthForScale(float scale) => 100 / scale + camera_position.Z; + + private static Vector2 positionAtDepth(float scale, Vector2 positionAtZeroDepth) + { + return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy; } } } From 68907fe1ba93fec9b513c1c6e6072567e7913bb5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 5 Dec 2023 02:48:11 +0300 Subject: [PATCH 007/539] Cleanup pass --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 5054c75e97..18d4fef5e8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -78,30 +77,29 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var drawable in playfield.HitObjectContainer.AliveObjects) { - if (drawable is not DrawableOsuHitObject d) - continue; - - switch (d) + switch (drawable) { - case DrawableHitCircle: - case DrawableSlider: - processObject(time, d); + case DrawableHitCircle circle: + processObject(time, circle, 0); + break; + + case DrawableSlider slider: + processObject(time, slider, slider.HitObject.Duration); break; } } } - private void processObject(double time, DrawableOsuHitObject drawable) + private void processObject(double time, DrawableOsuHitObject drawable, double duration) { var hitObject = drawable.HitObject; double baseSpeed = MaxDepth.Value / hitObject.TimePreempt; - double hitObjectDuration = hitObject is Slider s ? s.Duration : 0.0; - double offsetAfterStartTime = hitObjectDuration + hitObject.MaximumJudgementOffset + 500; - double slowSpeed = -minDepth / offsetAfterStartTime; + double offsetAfterStartTime = duration + hitObject.MaximumJudgementOffset + 500; + double slowSpeed = Math.Min(-minDepth / offsetAfterStartTime, baseSpeed); - float decelerationDistance = MaxDepth.Value * 0.2f; - double decelerationTime = (slowSpeed - baseSpeed) * 2 * decelerationDistance / (slowSpeed * slowSpeed - baseSpeed * baseSpeed); + double decelerationTime = hitObject.TimePreempt * 0.2; + float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5); float z; @@ -109,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Mods { double appearTime = hitObject.StartTime - hitObject.TimePreempt; float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime)); - z = Interpolation.ValueAt(Math.Max(time, appearTime), fullDistance, decelerationDistance, appearTime, hitObject.StartTime - decelerationTime); + z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed); } else if (time < hitObject.StartTime) { @@ -120,11 +118,11 @@ namespace osu.Game.Rulesets.Osu.Mods else { double endTime = hitObject.StartTime + offsetAfterStartTime; - z = Interpolation.ValueAt(Math.Min(time, endTime), 0f, minDepth, hitObject.StartTime, endTime); + z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed); } float scale = scaleForDepth(z); - drawable.Position = positionAtDepth(scale, hitObject.Position); + drawable.Position = toPlayfieldPosition(scale, hitObject.Position); drawable.Scale = new Vector2(scale); } @@ -132,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static float depthForScale(float scale) => 100 / scale + camera_position.Z; - private static Vector2 positionAtDepth(float scale, Vector2 positionAtZeroDepth) + private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth) { return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy; } From 594ea4da5f5324bb4cce86b071acff13a9a1cb1d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 5 Dec 2023 16:00:20 +0300 Subject: [PATCH 008/539] Apply suggested behaviour --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 18d4fef5e8..16517a2f36 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -103,13 +103,7 @@ namespace osu.Game.Rulesets.Osu.Mods float z; - if (time < hitObject.StartTime - decelerationTime) - { - double appearTime = hitObject.StartTime - hitObject.TimePreempt; - float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime)); - z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed); - } - else if (time < hitObject.StartTime) + if (time < hitObject.StartTime) { double timeOffset = time - (hitObject.StartTime - decelerationTime); double deceleration = (slowSpeed - baseSpeed) / decelerationTime; From 160edcd2707d64aede30385eb9bf59ef4641fb7b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 6 Dec 2023 07:09:46 +0300 Subject: [PATCH 009/539] Move objects at a constant speed whenever possible --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 54 ++++++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 16517a2f36..0defebc1c3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -27,8 +27,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray(); - private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -100); - private readonly float minDepth = depthForScale(1.5f); + private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200); + private readonly float sliderMinDepth = depthForScale(1.5f); // Depth at which slider's scale will be 1.5f [SettingSource("Maximum depth", "How far away objects appear.", 0)] public BindableFloat MaxDepth { get; } = new BindableFloat(100) @@ -80,30 +80,60 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { case DrawableHitCircle circle: - processObject(time, circle, 0); + processHitObject(time, circle); break; case DrawableSlider slider: - processObject(time, slider, slider.HitObject.Duration); + processSlider(time, slider); break; } } } - private void processObject(double time, DrawableOsuHitObject drawable, double duration) + private void processHitObject(double time, DrawableOsuHitObject drawable) { var hitObject = drawable.HitObject; + // Circles are always moving at the constant speed. They'll fade out before reaching the camera even at extreme conditions (AR 11, max depth). + double speed = MaxDepth.Value / hitObject.TimePreempt; + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed); + + float scale = scaleForDepth(z); + drawable.Position = toPlayfieldPosition(scale, hitObject.Position); + drawable.Scale = new Vector2(scale); + } + + private void processSlider(double time, DrawableSlider drawableSlider) + { + var hitObject = drawableSlider.HitObject; + double baseSpeed = MaxDepth.Value / hitObject.TimePreempt; - double offsetAfterStartTime = duration + hitObject.MaximumJudgementOffset + 500; - double slowSpeed = Math.Min(-minDepth / offsetAfterStartTime, baseSpeed); + double appearTime = hitObject.StartTime - hitObject.TimePreempt; + + // Allow slider to move at a constant speed if its scale at the end time will be lower than 1.5f + float zEnd = MaxDepth.Value - (float)((Math.Max(hitObject.StartTime + hitObject.Duration, appearTime) - appearTime) * baseSpeed); + + if (zEnd > sliderMinDepth) + { + processHitObject(time, drawableSlider); + return; + } + + double offsetAfterStartTime = hitObject.Duration + 500; + double slowSpeed = Math.Min(-sliderMinDepth / offsetAfterStartTime, baseSpeed); double decelerationTime = hitObject.TimePreempt * 0.2; float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5); float z; - if (time < hitObject.StartTime) + if (time < hitObject.StartTime - decelerationTime) + { + float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime)); + z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed); + } + else if (time < hitObject.StartTime) { double timeOffset = time - (hitObject.StartTime - decelerationTime); double deceleration = (slowSpeed - baseSpeed) / decelerationTime; @@ -116,13 +146,13 @@ namespace osu.Game.Rulesets.Osu.Mods } float scale = scaleForDepth(z); - drawable.Position = toPlayfieldPosition(scale, hitObject.Position); - drawable.Scale = new Vector2(scale); + drawableSlider.Position = toPlayfieldPosition(scale, hitObject.Position); + drawableSlider.Scale = new Vector2(scale); } - private static float scaleForDepth(float depth) => 100 / (depth - camera_position.Z); + private static float scaleForDepth(float depth) => -camera_position.Z / Math.Max(1f, depth - camera_position.Z); - private static float depthForScale(float scale) => 100 / scale + camera_position.Z; + private static float depthForScale(float scale) => -camera_position.Z / scale + camera_position.Z; private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth) { From b0878e36cf39baeb92da9b646e2a47c62acf7891 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 6 Dec 2023 10:30:21 +0300 Subject: [PATCH 010/539] Fix stacks having incorrect position --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 0defebc1c3..f71acf95b8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Mods float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed); float scale = scaleForDepth(z); - drawable.Position = toPlayfieldPosition(scale, hitObject.Position); + drawable.Position = toPlayfieldPosition(scale, hitObject.StackedPosition); drawable.Scale = new Vector2(scale); } @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Mods } float scale = scaleForDepth(z); - drawableSlider.Position = toPlayfieldPosition(scale, hitObject.Position); + drawableSlider.Position = toPlayfieldPosition(scale, hitObject.StackedPosition); drawableSlider.Scale = new Vector2(scale); } From 3f6dad5502d6aec67d9e7522fdb69d21299d5058 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 01:33:50 +0900 Subject: [PATCH 011/539] Use classic HP values for non-classic osu! HP drain --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 + .../Scoring/OsuHealthProcessor.cs | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index e53f20277b..35cbfa3790 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs new file mode 100644 index 0000000000..784d3b934d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public partial class OsuHealthProcessor : DrainingHealthProcessor + { + public OsuHealthProcessor(double drainStartTime, double drainLenience = 0) + : base(drainStartTime, drainLenience) + { + } + + protected override double GetHealthIncreaseFor(JudgementResult result) + { + switch (result.Type) + { + case HitResult.SmallTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.LargeTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + // When classic slider mechanics are enabled, this result comes from the tail. + return 0.02; + + case HitResult.LargeTickHit: + switch (result.HitObject) + { + case SliderTick: + return 0.015; + + case SliderHeadCircle: + case SliderTailCircle: + case SliderRepeat: + return 0.02; + } + + break; + + case HitResult.Meh: + return 0.002; + + case HitResult.Ok: + return 0.011; + + case HitResult.Great: + return 0.03; + + case HitResult.SmallBonus: + return 0.0085; + + case HitResult.LargeBonus: + return 0.01; + } + + return base.GetHealthIncreaseFor(result); + } + } +} From b31b9e96d0cf016e4f7b892116fd6951977a7853 Mon Sep 17 00:00:00 2001 From: Simon G <45692977+jeenyuhs@users.noreply.github.com> Date: Fri, 22 Dec 2023 03:04:48 +0100 Subject: [PATCH 012/539] adjust beatmap length and drain based on rate changing mods --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2613857998..c69cd6ead6 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -161,6 +161,7 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString artistBinding; private FillFlowContainer infoLabelContainer; private Container bpmLabelContainer; + private Container lengthLabelContainer; private readonly WorkingBeatmap working; private readonly RulesetInfo ruleset; @@ -341,10 +342,10 @@ namespace osu.Game.Screens.Select { settingChangeTracker?.Dispose(); - refreshBPMLabel(); + refreshBPMAndLengthLabel(); settingChangeTracker = new ModSettingChangeTracker(m.NewValue); - settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); + settingChangeTracker.SettingChanged += _ => refreshBPMAndLengthLabel(); }, true); } @@ -370,12 +371,10 @@ namespace osu.Game.Screens.Select infoLabelContainer.Children = new Drawable[] { - new InfoLabel(new BeatmapStatistic + lengthLabelContainer = new Container { - Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()), - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(), - }), + AutoSizeAxes = Axes.Both, + }, bpmLabelContainer = new Container { AutoSizeAxes = Axes.Both, @@ -394,7 +393,7 @@ namespace osu.Game.Screens.Select } } - private void refreshBPMLabel() + private void refreshBPMAndLengthLabel() { var beatmap = working.Beatmap; @@ -420,6 +419,16 @@ namespace osu.Game.Screens.Select CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = labelText }); + + double drainLength = Math.Round(beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmap.BeatmapInfo.Length / rate); + + lengthLabelContainer.Child = new InfoLabel(new BeatmapStatistic + { + Name = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), + Content = hitLength.ToFormattedDuration().ToString(), + }); } private Drawable getMapper(BeatmapMetadata metadata) From 9c35e250368ca3d777e1b2f4394310008839d3b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 13:38:52 +0900 Subject: [PATCH 013/539] Add failing tests --- .../Mods/TestSceneManiaModPerfect.cs | 29 +++++++++++++++++++ .../Mods/TestSceneOsuModPerfect.cs | 29 +++++++++++++++++++ osu.Game/Tests/Visual/ModPerfectTestScene.cs | 8 ++--- osu.Game/Tests/Visual/ModTestScene.cs | 18 ++++++------ 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 97a6ee28f4..a734979bf6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -1,9 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Mods @@ -24,5 +29,29 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + [Test] + public void TestBreakOnHoldNote() => CreateModTest(new ModTestData + { + Mod = new ManiaModPerfect(), + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 3000, + }, + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index 26c4133bc4..7030061e0e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -1,11 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; using osu.Game.Tests.Visual; using osuTK; @@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } + + [Test] + public void TestMissSliderTail() => CreateModTest(new ModTestData + { + Mod = new OsuModPerfect(), + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(true), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); } } diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 164faa16aa..c6f4a27ec4 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -29,12 +29,12 @@ namespace osu.Game.Tests.Visual PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) }); - protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(CurrentTestData, AllowFail); - private partial class PerfectModTestPlayer : TestPlayer + protected partial class PerfectModTestPlayer : ModTestPlayer { - public PerfectModTestPlayer() - : base(showResults: false) + public PerfectModTestPlayer(ModTestData data, bool allowFail) + : base(data, allowFail) { } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index aa5b506343..c2ebcdefac 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -20,35 +20,35 @@ namespace osu.Game.Tests.Visual { protected sealed override bool HasCustomSteps => true; - private ModTestData currentTestData; + protected ModTestData CurrentTestData { get; private set; } protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTestData = testData); + AddStep("set test data", () => CurrentTestData = testData); }); public override void TearDownSteps() { AddUntilStep("test passed", () => { - if (currentTestData == null) + if (CurrentTestData == null) return true; - return currentTestData.PassCondition?.Invoke() ?? false; + return CurrentTestData.PassCondition?.Invoke() ?? false; }); base.TearDownSteps(); } - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => CurrentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); - if (currentTestData.Mods != null) - mods.AddRange(currentTestData.Mods); - if (currentTestData.Autoplay) + if (CurrentTestData.Mods != null) + mods.AddRange(CurrentTestData.Mods); + if (CurrentTestData.Autoplay) mods.Add(ruleset.GetAutoplayMod()); SelectedMods.Value = mods; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual return CreateModPlayer(ruleset); } - protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail); + protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(CurrentTestData, AllowFail); protected partial class ModTestPlayer : TestPlayer { From ea778c6e0aa6bf52ae5e12f287b0fb7f131348ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 13:39:41 +0900 Subject: [PATCH 014/539] Fix perfect/sudden death not working on slider tails --- osu.Game/Rulesets/Mods/ModPerfect.cs | 4 +++- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 6f0bb7ad3b..0ba40ba070 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mods } protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => result.Type.AffectsAccuracy() + => (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type)) && result.Type != result.Judgement.MaxResult; + + private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 4e4e8662e8..56420e5ff4 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => result.Type.AffectsCombo() + => (result.Judgement.MinResult.AffectsCombo() || result.Judgement.MaxResult.AffectsCombo() || result.Type.AffectsCombo()) && !result.IsHit; } } From 93efa98d9b8c1ce647a5baca6770f3c0c3e39755 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 16:19:36 +0900 Subject: [PATCH 015/539] Fix mania "Great" hits failing with perfect mod --- .../Mods/TestSceneManiaModPerfect.cs | 23 +++++++++++++++++++ .../Mods/ManiaModPerfect.cs | 15 ++++++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index a734979bf6..73d6fe357d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -30,6 +30,29 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(true)] public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + [Test] + public void TestGreatHit() => CreateModTest(new ModTestData + { + Mod = new ManiaModPerfect(), + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Note + { + StartTime = 1000, + } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1020, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + [Test] public void TestBreakOnHoldNote() => CreateModTest(new ModTestData { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs index 2e22e23dbd..b02a18c9f4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs @@ -1,11 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModPerfect : ModPerfect { + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type)) + return false; + + // Mania allows imperfect "Great" hits without failing. + if (result.Judgement.MaxResult == HitResult.Perfect) + return result.Type < HitResult.Great; + + return result.Type != result.Judgement.MaxResult; + } + + private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); } } From 88a5ba8167facffe4c7c401ca32ddf9f003bf4df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 16:43:09 +0900 Subject: [PATCH 016/539] Add mania/osu sudden death mod tests --- .../Mods/TestSceneCatchModPerfect.cs | 2 +- .../Mods/TestSceneManiaModPerfect.cs | 6 +- .../Mods/TestSceneManiaModSuddenDeath.cs | 72 +++++++++++++++++ .../Mods/TestSceneOsuModPerfect.cs | 4 +- .../Mods/TestSceneOsuModSuddenDeath.cs | 77 +++++++++++++++++++ .../Mods/TestSceneTaikoModPerfect.cs | 2 +- ...tScene.cs => ModFailConditionTestScene.cs} | 14 ++-- 7 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs rename osu.Game/Tests/Visual/{ModPerfectTestScene.cs => ModFailConditionTestScene.cs} (74%) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 45e7d7aa28..7d539f91e4 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Tests.Mods { - public partial class TestSceneCatchModPerfect : ModPerfectTestScene + public partial class TestSceneCatchModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 73d6fe357d..51730e2b43 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -13,7 +13,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Mods { - public partial class TestSceneManiaModPerfect : ModPerfectTestScene + public partial class TestSceneManiaModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestGreatHit() => CreateModTest(new ModTestData { Mod = new ManiaModPerfect(), - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(false), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), Autoplay = false, Beatmap = new Beatmap { @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestBreakOnHoldNote() => CreateModTest(new ModTestData { Mod = new ManiaModPerfect(), - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, Autoplay = false, Beatmap = new Beatmap { diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs new file mode 100644 index 0000000000..619816a815 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModSuddenDeath.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + public TestSceneManiaModSuddenDeath() + : base(new ManiaModSuddenDeath()) + { + } + + [Test] + public void TestGreatHit() => CreateModTest(new ModTestData + { + Mod = new ManiaModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Note + { + StartTime = 1000, + } + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1020, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + + [Test] + public void TestBreakOnHoldNote() => CreateModTest(new ModTestData + { + Mod = new ManiaModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2, + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 3000, + }, + }, + }, + ReplayFrames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(2000) + } + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index 7030061e0e..b01bbbfca1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public partial class TestSceneOsuModPerfect : ModPerfectTestScene + public partial class TestSceneOsuModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods public void TestMissSliderTail() => CreateModTest(new ModTestData { Mod = new OsuModPerfect(), - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(true), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true), Autoplay = false, Beatmap = new Beatmap { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs new file mode 100644 index 0000000000..ea048aaa6e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + public TestSceneOsuModSuddenDeath() + : base(new OsuModSuddenDeath()) + { + } + + [Test] + public void TestMissTail() => CreateModTest(new ModTestData + { + Mod = new OsuModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); + + [Test] + public void TestMissTick() => CreateModTest(new ModTestData + { + Mod = new OsuModSuddenDeath(), + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + Position = new Vector2(256, 192), + StartTime = 1000, + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), }) + }, + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton), + new OsuReplayFrame(1001, new Vector2(256, 192)), + } + }); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index aed08f33e0..8a1157a7f8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -10,7 +10,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Mods { - public partial class TestSceneTaikoModPerfect : ModPerfectTestScene + public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene { protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset(); diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModFailConditionTestScene.cs similarity index 74% rename from osu.Game/Tests/Visual/ModPerfectTestScene.cs rename to osu.Game/Tests/Visual/ModFailConditionTestScene.cs index c6f4a27ec4..8f0dff055d 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModFailConditionTestScene.cs @@ -8,11 +8,11 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Visual { - public abstract partial class ModPerfectTestScene : ModTestScene + public abstract partial class ModFailConditionTestScene : ModTestScene { - private readonly ModPerfect mod; + private readonly ModFailCondition mod; - protected ModPerfectTestScene(ModPerfect mod) + protected ModFailConditionTestScene(ModFailCondition mod) { this.mod = mod; } @@ -26,14 +26,14 @@ namespace osu.Game.Tests.Visual HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) }); - protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(CurrentTestData, AllowFail); + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModFailConditionTestPlayer(CurrentTestData, AllowFail); - protected partial class PerfectModTestPlayer : ModTestPlayer + protected partial class ModFailConditionTestPlayer : ModTestPlayer { - public PerfectModTestPlayer(ModTestData data, bool allowFail) + public ModFailConditionTestPlayer(ModTestData data, bool allowFail) : base(data, allowFail) { } From 5703546d715687d58b832c25082386d35abadb09 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 16:43:17 +0900 Subject: [PATCH 017/539] Revert change to ModSuddenDeath --- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 56420e5ff4..4e4e8662e8 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => (result.Judgement.MinResult.AffectsCombo() || result.Judgement.MaxResult.AffectsCombo() || result.Type.AffectsCombo()) + => result.Type.AffectsCombo() && !result.IsHit; } } From a0185508b76af5f650534d575226d27fe3fc1f21 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Dec 2023 17:55:06 +0900 Subject: [PATCH 018/539] Add basic consideration of density for HP drain --- .../Scoring/OsuHealthProcessor.cs | 4 ++ .../Scoring/DrainingHealthProcessor.cs | 55 +++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 784d3b934d..5ae5766cda 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + protected override int? GetDensityGroup(HitObject hitObject) => (hitObject as IHasComboInformation)?.ComboIndex; + protected override double GetHealthIncreaseFor(JudgementResult result) { switch (result.Type) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index a2fc23ac2e..4f6f8598fb 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -61,7 +61,9 @@ namespace osu.Game.Rulesets.Scoring /// protected readonly double DrainLenience; - private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); + private readonly List healthIncreases = new List(); + private readonly Dictionary densityMultiplierByGroup = new Dictionary(); + private double gameplayEndTime; private double targetMinimumHealth; @@ -133,14 +135,33 @@ namespace osu.Game.Rulesets.Scoring { base.ApplyResultInternal(result); - if (!result.Type.IsBonus()) - healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + if (IsSimulating && !result.Type.IsBonus()) + { + healthIncreases.Add(new HealthIncrease( + result.HitObject.GetEndTime() + result.TimeOffset, + GetHealthIncreaseFor(result), + GetDensityGroup(result.HitObject))); + } } + protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * getDensityMultiplier(GetDensityGroup(result.HitObject)); + + private double getDensityMultiplier(int? group) + { + if (group == null) + return 1; + + return densityMultiplierByGroup.TryGetValue(group.Value, out double multiplier) ? multiplier : 1; + } + + protected virtual int? GetDensityGroup(HitObject hitObject) => null; + protected override void Reset(bool storeResults) { base.Reset(storeResults); + densityMultiplierByGroup.Clear(); + if (storeResults) DrainRate = ComputeDrainRate(); @@ -152,6 +173,24 @@ namespace osu.Game.Rulesets.Scoring if (healthIncreases.Count <= 1) return 0; + // Normalise the health gain during sections with higher densities. + (int group, double avgIncrease)[] avgIncreasesByGroup = healthIncreases + .Where(i => i.Group != null) + .GroupBy(i => i.Group) + .Select(g => ((int)g.Key!, g.Sum(i => i.Amount) / (g.Max(i => i.Time) - g.Min(i => i.Time) + 1))) + .ToArray(); + + if (avgIncreasesByGroup.Length > 1) + { + double overallAverageIncrease = avgIncreasesByGroup.Average(g => g.avgIncrease); + + foreach ((int group, double avgIncrease) in avgIncreasesByGroup) + { + // Reduce the health increase for groups that return more health than average. + densityMultiplierByGroup[group] = Math.Min(1, overallAverageIncrease / avgIncrease); + } + } + int adjustment = 1; double result = 1; @@ -165,8 +204,8 @@ namespace osu.Game.Rulesets.Scoring for (int i = 0; i < healthIncreases.Count; i++) { - double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime; + double currentTime = healthIncreases[i].Time; + double lastTime = i > 0 ? healthIncreases[i - 1].Time : DrainStartTime; while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime) { @@ -177,10 +216,12 @@ namespace osu.Game.Rulesets.Scoring currentBreak++; } + double multiplier = getDensityMultiplier(healthIncreases[i].Group); + // Apply health adjustments currentHealth -= (currentTime - lastTime) * result; lowestHealth = Math.Min(lowestHealth, currentHealth); - currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].Amount * multiplier); // Common scenario for when the drain rate is definitely too harsh if (lowestHealth < 0) @@ -198,5 +239,7 @@ namespace osu.Game.Rulesets.Scoring return result; } + + private record struct HealthIncrease(double Time, double Amount, int? Group); } } From 7e557152fb430219ab484107d592311e983094f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Dec 2023 12:40:24 +0100 Subject: [PATCH 019/539] Fix relax mod not considering full follow area radius when automatically holding sliders Closes https://github.com/ppy/osu/issues/25947. Regressed in https://github.com/ppy/osu/pull/25776 with the changes to `DrawableSliderBall`. I would have liked to include tests, but relax mod is a bit untestable, because it disengages completely in the presence of a replay: https://github.com/ppy/osu/blob/7e09164d7084265536570ac7036d7244934b651c/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs#L49-L58 Additionally, `RulesetInputManager` disengages completely from parent inputs when there is a replay active: https://github.com/ppy/osu/blob/7e09164d7084265536570ac7036d7244934b651c/osu.Game/Rulesets/UI/RulesetInputManager.cs#L116 which means there is really no easy way to control positional input while still having relax logic work. So I'm hoping the fix could be considered obvious enough to not require test coverage. --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- .../Objects/Drawables/SliderInputManager.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index aaa7c70a8d..40fadfb77e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (!slider.HeadCircle.IsHit) handleHitCircle(slider.HeadCircle); - requiresHold |= slider.Ball.IsHovered || h.IsHovered; + requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true); break; case DrawableSpinner spinner: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs index 8aa982783e..95896c7c91 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { base.Update(); - updateTracking(isMouseInFollowArea(Tracking)); + updateTracking(IsMouseInFollowArea(Tracking)); } public void PostProcessHeadJudgement(DrawableSliderHead head) @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!head.Judged || !head.Result.IsHit) return; - if (!isMouseInFollowArea(true)) + if (!IsMouseInFollowArea(true)) return; Debug.Assert(screenSpaceMousePosition != null); @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // If all ticks were hit so far, enable tracking the full extent. // If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball. // For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update(). - updateTracking(allTicksInRange || isMouseInFollowArea(false)); + updateTracking(allTicksInRange || IsMouseInFollowArea(false)); } public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset) @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether the mouse is currently in the follow area. /// /// Whether to test against the maximum area of the follow circle. - private bool isMouseInFollowArea(bool expanded) + public bool IsMouseInFollowArea(bool expanded) { if (screenSpaceMousePosition is not Vector2 pos) return false; From 6cb82310549a73ca2657efe65c9528da8367f1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Dec 2023 13:22:26 +0100 Subject: [PATCH 020/539] Add test coverage for failure case --- .../Mods/TestSceneOsuModStrictTracking.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs new file mode 100644 index 0000000000..726b415977 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModStrictTracking : OsuModTestScene + { + [Test] + public void TestSliderInput() => CreateModTest(new ModTestData + { + Mod = new OsuModStrictTracking(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(0, 100)) + } + } + } + } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton), + new OsuReplayFrame(501, new Vector2(200, 0)), + new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton), + new OsuReplayFrame(1751, new Vector2(0, 100)), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 2 + }); + } +} From 30553dc7b839600145ac57bd789b7e9992c34dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Dec 2023 13:38:07 +0100 Subject: [PATCH 021/539] Fix strict tracking mod forcefully missing tail before slider start time Closes https://github.com/ppy/osu/issues/25816. Regressed in https://github.com/ppy/osu/pull/25748. The reason this broke is that allowing the state of `Tracking` to change before the slider's start time to support the early hit scenario causes strict tracking to consider loss of tracking before the slider's start time as an actual miss, and thus forcefully miss the tail (see test case in 6cb82310549a73ca2657efe65c9528da8367f1ab). --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index c465ab8732..2c9292c58b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods { if (e.NewValue || slider.Judged) return; + if (slider.Time.Current < slider.HitObject.StartTime) + return; + var tail = slider.NestedHitObjects.OfType().First(); if (!tail.Judged) From 01cf4ee15a8456ccc56381f138dfdf7f210de121 Mon Sep 17 00:00:00 2001 From: Simon G <45692977+jeenyuhs@users.noreply.github.com> Date: Fri, 22 Dec 2023 18:11:37 +0100 Subject: [PATCH 022/539] add test for length updates --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 7cd4f06bce..4e100a37dc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -194,6 +195,36 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + [TestCase] + public void TestLengthUpdates() + { + IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + double drain = beatmap.CalculateDrainLength(); + beatmap.BeatmapInfo.Length = drain; + + OsuModDoubleTime doubleTime = null; + + selectBeatmap(beatmap); + checkDisplayedLength(drain); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedLength(Math.Round(drain / 1.5f)); + + AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); + checkDisplayedLength(Math.Round(drain / 2)); + } + + private void checkDisplayedLength(double drain) + { + var displayedLength = drain.ToFormattedDuration(); + + AddUntilStep($"check map drain ({displayedLength})", () => + { + var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsTotalLength(displayedLength)); + return label.Statistic.Content == displayedLength.ToString(); + }); + } + private void setRuleset(RulesetInfo rulesetInfo) { Container containerBefore = null; From c5893f245ce7a89d1900dbb620390823702481fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 14:03:45 +0900 Subject: [PATCH 023/539] Change legacy version checks to account for users specifying incorrect versions --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs | 2 +- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index d8d86d1802..ef616ae964 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; - if (legacyVersion >= 2.0m) + if (legacyVersion > 1.0m) // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. hitCircleText.FadeOut(legacy_fade_duration / 4); else diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 68274ffa2d..b8fe2f8d06 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -67,7 +67,7 @@ namespace osu.Game.Skinning decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; - if (legacyVersion >= 2.0m) + if (legacyVersion > 1.0m) { this.MoveTo(new Vector2(0, -5)); this.MoveToOffset(new Vector2(0, 80), fade_out_delay + fade_out_length, Easing.In); From 9f34dfa2baa7e649d43ad74a444fa32df7b172e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 16:25:52 +0900 Subject: [PATCH 024/539] Add missing using statement --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 4e100a37dc..fd102da026 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; From 8349cb7bbe4f30777f73cbf7c18f071997bd185c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 17:03:57 +0900 Subject: [PATCH 025/539] Fix hard crash when attempting to change folder location during a large import Closes https://github.com/ppy/osu/issues/26067. --- osu.Game/OsuGameBase.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 2d8024a45a..48548dc1ef 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -527,14 +527,21 @@ namespace osu.Game { ManualResetEventSlim readyToRun = new ManualResetEventSlim(); + bool success = false; + Scheduler.Add(() => { - realmBlocker = realm.BlockAllOperations("migration"); + try + { + realmBlocker = realm.BlockAllOperations("migration"); + success = true; + } + catch { } readyToRun.Set(); }, false); - if (!readyToRun.Wait(30000)) + if (!readyToRun.Wait(30000) || !success) throw new TimeoutException("Attempting to block for migration took too long."); bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); From 27a9dcc5a1a375e7d0591c90880ccb09fd8c0042 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 19:55:05 +0900 Subject: [PATCH 026/539] Add basic hotkey offset adjust support (via existing offset control) --- .../Input/Bindings/GlobalActionContainer.cs | 8 ++ .../GlobalActionKeyBindingStrings.cs | 10 +++ .../PlayerSettings/BeatmapOffsetControl.cs | 73 +++++++++++++------ 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 947cd5f54f..5a39c02185 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -160,6 +160,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus), new KeyBinding(InputKey.F1, GlobalAction.SaveReplay), new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), + new KeyBinding(InputKey.Plus, GlobalAction.IncreaseOffset), + new KeyBinding(InputKey.Minus, GlobalAction.DecreaseOffset), }; private static IEnumerable replayKeyBindings => new[] @@ -404,6 +406,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] + IncreaseOffset, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))] + DecreaseOffset } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 8356c480dd..ca27d0ff95 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -344,6 +344,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); + /// + /// "Increase offset" + /// + public static LocalisableString IncreaseOffset => new TranslatableString(getKey(@"increase_offset"), @"Increase offset"); + + /// + /// "Decrease offset" + /// + public static LocalisableString DecreaseOffset => new TranslatableString(getKey(@"decrease_offset"), @"Decrease offset"); + /// /// "Toggle rotate control" /// diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 840077eb7f..b4bb35377d 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -9,6 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -16,6 +18,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; @@ -26,7 +29,7 @@ using osuTK; namespace osu.Game.Screens.Play.PlayerSettings { - public partial class BeatmapOffsetControl : CompositeDrawable + public partial class BeatmapOffsetControl : CompositeDrawable, IKeyBindingHandler { public Bindable ReferenceScore { get; } = new Bindable(); @@ -88,28 +91,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } - public partial class OffsetSliderBar : PlayerSliderBar - { - protected override Drawable CreateControl() => new CustomSliderBar(); - - protected partial class CustomSliderBar : SliderBar - { - public override LocalisableString TooltipText => - Current.Value == 0 - ? LocalisableString.Interpolate($@"{base.TooltipText} ms") - : LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}"); - - private LocalisableString getEarlyLateText(double value) - { - Debug.Assert(value != 0); - - return value > 0 - ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier - : BeatmapOffsetControlStrings.HitObjectsAppearLater; - } - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -243,5 +224,51 @@ namespace osu.Game.Screens.Play.PlayerSettings base.Dispose(isDisposing); beatmapOffsetSubscription?.Dispose(); } + + public bool OnPressed(KeyBindingPressEvent e) + { + double amount = e.AltPressed ? 1 : 5; + + switch (e.Action) + { + case GlobalAction.IncreaseOffset: + Current.Value += amount; + + return true; + + case GlobalAction.DecreaseOffset: + Current.Value -= amount; + + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + public partial class OffsetSliderBar : PlayerSliderBar + { + protected override Drawable CreateControl() => new CustomSliderBar(); + + protected partial class CustomSliderBar : SliderBar + { + public override LocalisableString TooltipText => + Current.Value == 0 + ? LocalisableString.Interpolate($@"{base.TooltipText} ms") + : LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}"); + + private LocalisableString getEarlyLateText(double value) + { + Debug.Assert(value != 0); + + return value > 0 + ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier + : BeatmapOffsetControlStrings.HitObjectsAppearLater; + } + } + } } } From 7e9522a722fd01ca872877e1e3b4bcfeb61c8a2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 20:46:12 +0900 Subject: [PATCH 027/539] Allow external use of offset text explanation --- .../PlayerSettings/BeatmapOffsetControl.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b4bb35377d..80549343a5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -233,12 +233,10 @@ namespace osu.Game.Screens.Play.PlayerSettings { case GlobalAction.IncreaseOffset: Current.Value += amount; - return true; case GlobalAction.DecreaseOffset: Current.Value -= amount; - return true; } @@ -249,25 +247,29 @@ namespace osu.Game.Screens.Play.PlayerSettings { } + public static LocalisableString GetOffsetExplanatoryText(double offset) + { + return offset == 0 + ? LocalisableString.Interpolate($@"{offset:0.0} ms") + : LocalisableString.Interpolate($@"{offset:0.0} ms {getEarlyLateText(offset)}"); + + LocalisableString getEarlyLateText(double value) + { + Debug.Assert(value != 0); + + return value > 0 + ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier + : BeatmapOffsetControlStrings.HitObjectsAppearLater; + } + } + public partial class OffsetSliderBar : PlayerSliderBar { protected override Drawable CreateControl() => new CustomSliderBar(); protected partial class CustomSliderBar : SliderBar { - public override LocalisableString TooltipText => - Current.Value == 0 - ? LocalisableString.Interpolate($@"{base.TooltipText} ms") - : LocalisableString.Interpolate($@"{base.TooltipText} ms {getEarlyLateText(Current.Value)}"); - - private LocalisableString getEarlyLateText(double value) - { - Debug.Assert(value != 0); - - return value > 0 - ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier - : BeatmapOffsetControlStrings.HitObjectsAppearLater; - } + public override LocalisableString TooltipText => GetOffsetExplanatoryText(Current.Value); } } } From 6f11885d4bedc303859964ab1415a2f1959e9bda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 20:46:22 +0900 Subject: [PATCH 028/539] Add control to allow changing offset from gameplay --- .../Screens/Play/GameplayOffsetControl.cs | 104 ++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 6 + 2 files changed, 110 insertions(+) create mode 100644 osu.Game/Screens/Play/GameplayOffsetControl.cs diff --git a/osu.Game/Screens/Play/GameplayOffsetControl.cs b/osu.Game/Screens/Play/GameplayOffsetControl.cs new file mode 100644 index 0000000000..3f5a5bef2a --- /dev/null +++ b/osu.Game/Screens/Play/GameplayOffsetControl.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Play.PlayerSettings; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + /// + /// This provides the ability to change the offset while in gameplay. + /// Eventually this should be replaced with all settings from PlayerLoader being accessible from the game. + /// + internal partial class GameplayOffsetControl : VisibilityContainer + { + protected override bool StartHidden => true; + + public override bool PropagateNonPositionalInputSubTree => true; + + private BeatmapOffsetControl offsetControl = null!; + + private OsuTextFlowContainer text = null!; + + private ScheduledDelegate? hideOp; + + public GameplayOffsetControl() + { + AutoSizeAxes = Axes.Y; + Width = SettingsToolboxGroup.CONTAINER_WIDTH; + + Masking = true; + CornerRadius = 5; + + // Allow BeatmapOffsetControl to handle keyboard input. + AlwaysPresent = true; + + Anchor = Anchor.CentreRight; + Origin = Anchor.CentreRight; + + X = 100; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.8f, + Colour = colourProvider?.Background4 ?? Color4.Black, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Spacing = new Vector2(5), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl(), + text = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + } + } + }, + }; + + offsetControl.Current.BindValueChanged(val => + { + text.Text = BeatmapOffsetControl.GetOffsetExplanatoryText(val.NewValue); + Show(); + + hideOp?.Cancel(); + hideOp = Scheduler.AddDelayed(Hide, 500); + }); + } + + protected override void PopIn() + { + this.FadeIn(500, Easing.OutQuint) + .MoveToX(0, 500, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(500, Easing.InQuint) + .MoveToX(100, 500, Easing.InQuint); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c9251b0a78..c960ac357f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -461,6 +461,12 @@ namespace osu.Game.Screens.Play OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, + new GameplayOffsetControl + { + Margin = new MarginPadding(20), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } }, }; From a2e5f624789d852a23d555e12b5dd962ad6f7712 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 21:07:17 +0900 Subject: [PATCH 029/539] Fix user profile cover showing 1px line when contracted Addresses https://github.com/ppy/osu/discussions/26068. --- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 36bd8a5af5..c9e5068b2a 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -232,6 +232,14 @@ namespace osu.Game.Overlays.Profile.Header bool expanded = coverToggle.CoverExpanded.Value; cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint); + + // Without this a very tiny slither of the cover will be visible even with a size of zero. + // Integer masking woes, no doubt. + if (expanded) + cover.FadeIn(transition_duration, Easing.OutQuint); + else + cover.FadeOut(transition_duration, Easing.InQuint); + avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint); avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint); flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint); From 007ea51e202d546e813725991221436066c3db74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Dec 2023 13:07:29 +0100 Subject: [PATCH 030/539] Add extra safety against returning negative total score in conversion operation --- .../StandardisedScoreMigrationTools.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 380bf63e63..5cd2a2f29c 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -321,6 +321,8 @@ namespace osu.Game.Database double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); + long convertedTotalScore; + switch (score.Ruleset.OnlineID) { case 0: @@ -417,32 +419,42 @@ namespace osu.Game.Database double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; - return (long)Math.Round(( + convertedTotalScore = (long)Math.Round(( 500000 * newComboScoreProportion * score.Accuracy + 500000 * Math.Pow(score.Accuracy, 5) + bonusProportion) * modMultiplier); + break; case 1: - return (long)Math.Round(( + convertedTotalScore = (long)Math.Round(( 250000 * comboProportion + 750000 * Math.Pow(score.Accuracy, 3.6) + bonusProportion) * modMultiplier); + break; case 2: - return (long)Math.Round(( + convertedTotalScore = (long)Math.Round(( 600000 * comboProportion + 400000 * score.Accuracy + bonusProportion) * modMultiplier); + break; case 3: - return (long)Math.Round(( + convertedTotalScore = (long)Math.Round(( 850000 * comboProportion + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + bonusProportion) * modMultiplier); + break; default: - return score.TotalScore; + convertedTotalScore = score.TotalScore; + break; } + + if (convertedTotalScore < 0) + throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); + + return convertedTotalScore; } public static double ComputeAccuracy(ScoreInfo scoreInfo) From 15a9740eb65f39dbc3e58c55ad146e3103d483a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 21:12:47 +0900 Subject: [PATCH 031/539] Change "cinema" mod to never fail Addresses https://github.com/ppy/osu/discussions/26032. --- osu.Game/Rulesets/Mods/ModCinema.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index ae661c5f25..a942bcc678 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mods } } - public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer + public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer, IApplicableFailOverride { public override string Name => "Cinema"; public override string Acronym => "CN"; @@ -45,5 +45,9 @@ namespace osu.Game.Rulesets.Mods player.BreakOverlay.Hide(); } + + public bool PerformFail() => false; + + public bool RestartOnFail => false; } } From 644c9816739b2aadfcdb9b5e14d438a0cae81313 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Dec 2023 21:21:34 +0900 Subject: [PATCH 032/539] Fix "spectate" button not always being clickable in online users list --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index fe3151398f..37ea3f9c38 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -218,6 +219,7 @@ namespace osu.Game.Overlays.Dashboard { panel.Anchor = Anchor.TopCentre; panel.Origin = Anchor.TopCentre; + panel.CanSpectate.Value = playingUsers.Contains(user.Id); }); public partial class OnlineUserPanel : CompositeDrawable, IFilterable From 3a2ed3677b75bbbf8ec1683be7ecd8eb6e0a3e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Dec 2023 13:25:20 +0100 Subject: [PATCH 033/539] Fix standardised score conversion failing for scores set with 0.0x mod mutliplier Closes https://github.com/ppy/osu/issues/26073. --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 8 ++++++-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 5cd2a2f29c..66c816e796 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -312,8 +312,12 @@ namespace osu.Game.Database double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy; // We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio. - double comboProportion = - ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore); + // Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0 + // when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot). + // In such cases, just assume 0. + double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0 + ? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore) + : 0; // We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore. long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index dbd9a106df..cf0a7bd54f 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -35,9 +35,10 @@ namespace osu.Game.Scoring.Legacy /// 30000006: Fix edge cases in conversion after combo exponent introduction that lead to NaNs. Reconvert all scores. /// 30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores. /// 30000008: Add accuracy conversion. Reconvert all scores. + /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000008; + public const int LATEST_VERSION = 30000009; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 32d8ee2d0c30b4922ac96bb172959ea375cd2e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Dec 2023 13:42:19 +0100 Subject: [PATCH 034/539] Add test coverage --- .../Online/TestSceneCurrentlyOnlineDisplay.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index 7687cd195d..b696c5d8ca 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -81,6 +81,21 @@ namespace osu.Game.Tests.Visual.Online AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); } + [Test] + public void TestUserWasPlayingBeforeWatchingUserPresence() + { + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); + AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); + AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); + + AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); + AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); + AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); + AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + } + internal partial class TestUserLookupCache : UserLookupCache { private static readonly string[] usernames = From 0cbf594a8c151e3699ca15431e29d3dac96f2016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Dec 2023 15:10:31 +0100 Subject: [PATCH 035/539] Make cinema mod incompatible with no fail --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 +- osu.Game/Rulesets/Mods/ModNoFail.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index a942bcc678..dbb37e0af6 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModCinema; public override LocalisableString Description => "Watch the video without visual distractions."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail) }).ToArray(); public void ApplyToHUD(HUDOverlay overlay) { diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 0cf81bf4c9..cc451772b2 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; - public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition) }; + public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) }; private readonly Bindable showHealthBar = new Bindable(); From d1000b2e6c527f170e9fd1dd525d2ba296c31f17 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 23 Dec 2023 23:36:15 +0900 Subject: [PATCH 036/539] remove HP density --- .../Scoring/OsuHealthProcessor.cs | 2 - .../Scoring/DrainingHealthProcessor.cs | 42 ++----------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 5ae5766cda..7463bb565c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Osu.Scoring { } - protected override int? GetDensityGroup(HitObject hitObject) => (hitObject as IHasComboInformation)?.ComboIndex; - protected override double GetHealthIncreaseFor(JudgementResult result) { switch (result.Type) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 4f6f8598fb..629a84ea62 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring protected readonly double DrainLenience; private readonly List healthIncreases = new List(); - private readonly Dictionary densityMultiplierByGroup = new Dictionary(); private double gameplayEndTime; private double targetMinimumHealth; @@ -139,29 +138,14 @@ namespace osu.Game.Rulesets.Scoring { healthIncreases.Add(new HealthIncrease( result.HitObject.GetEndTime() + result.TimeOffset, - GetHealthIncreaseFor(result), - GetDensityGroup(result.HitObject))); + GetHealthIncreaseFor(result))); } } - protected override double GetHealthIncreaseFor(JudgementResult result) => base.GetHealthIncreaseFor(result) * getDensityMultiplier(GetDensityGroup(result.HitObject)); - - private double getDensityMultiplier(int? group) - { - if (group == null) - return 1; - - return densityMultiplierByGroup.TryGetValue(group.Value, out double multiplier) ? multiplier : 1; - } - - protected virtual int? GetDensityGroup(HitObject hitObject) => null; - protected override void Reset(bool storeResults) { base.Reset(storeResults); - densityMultiplierByGroup.Clear(); - if (storeResults) DrainRate = ComputeDrainRate(); @@ -173,24 +157,6 @@ namespace osu.Game.Rulesets.Scoring if (healthIncreases.Count <= 1) return 0; - // Normalise the health gain during sections with higher densities. - (int group, double avgIncrease)[] avgIncreasesByGroup = healthIncreases - .Where(i => i.Group != null) - .GroupBy(i => i.Group) - .Select(g => ((int)g.Key!, g.Sum(i => i.Amount) / (g.Max(i => i.Time) - g.Min(i => i.Time) + 1))) - .ToArray(); - - if (avgIncreasesByGroup.Length > 1) - { - double overallAverageIncrease = avgIncreasesByGroup.Average(g => g.avgIncrease); - - foreach ((int group, double avgIncrease) in avgIncreasesByGroup) - { - // Reduce the health increase for groups that return more health than average. - densityMultiplierByGroup[group] = Math.Min(1, overallAverageIncrease / avgIncrease); - } - } - int adjustment = 1; double result = 1; @@ -216,12 +182,10 @@ namespace osu.Game.Rulesets.Scoring currentBreak++; } - double multiplier = getDensityMultiplier(healthIncreases[i].Group); - // Apply health adjustments currentHealth -= (currentTime - lastTime) * result; lowestHealth = Math.Min(lowestHealth, currentHealth); - currentHealth = Math.Min(1, currentHealth + healthIncreases[i].Amount * multiplier); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].Amount); // Common scenario for when the drain rate is definitely too harsh if (lowestHealth < 0) @@ -240,6 +204,6 @@ namespace osu.Game.Rulesets.Scoring return result; } - private record struct HealthIncrease(double Time, double Amount, int? Group); + private record struct HealthIncrease(double Time, double Amount); } } From 00090bc527989b6caa094a40caf67023aeb352b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 23 Dec 2023 23:36:24 +0900 Subject: [PATCH 037/539] Add combo end bonus to HP --- .../Scoring/OsuHealthProcessor.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 7463bb565c..672a8c91ba 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -1,10 +1,12 @@ // 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 osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -12,12 +14,68 @@ namespace osu.Game.Rulesets.Osu.Scoring { public partial class OsuHealthProcessor : DrainingHealthProcessor { + private ComboResult currentComboResult = ComboResult.Perfect; + public OsuHealthProcessor(double drainStartTime, double drainLenience = 0) : base(drainStartTime, drainLenience) { } protected override double GetHealthIncreaseFor(JudgementResult result) + { + if (IsSimulating) + return getHealthIncreaseFor(result); + + if (result.HitObject is not IHasComboInformation combo) + return getHealthIncreaseFor(result); + + if (combo.NewCombo) + currentComboResult = ComboResult.Perfect; + + switch (result.Type) + { + case HitResult.LargeTickMiss: + case HitResult.Ok: + setComboResult(ComboResult.Good); + break; + + case HitResult.Meh: + case HitResult.Miss: + setComboResult(ComboResult.None); + break; + } + + // The tail has a special IgnoreMiss judgement + if (result.HitObject is SliderTailCircle && !result.IsHit) + setComboResult(ComboResult.Good); + + if (combo.LastInCombo && result.Type.IsHit()) + { + switch (currentComboResult) + { + case ComboResult.Perfect: + return getHealthIncreaseFor(result) + 0.07; + + case ComboResult.Good: + return getHealthIncreaseFor(result) + 0.05; + + default: + return getHealthIncreaseFor(result) + 0.03; + } + } + + return getHealthIncreaseFor(result); + + void setComboResult(ComboResult comboResult) => currentComboResult = (ComboResult)Math.Min((int)currentComboResult, (int)comboResult); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + currentComboResult = ComboResult.Perfect; + } + + private double getHealthIncreaseFor(JudgementResult result) { switch (result.Type) { From 8b11bcc6ea617bbb689b793443a90b977ae2d5e9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 24 Dec 2023 00:08:15 +0900 Subject: [PATCH 038/539] Remove unused using --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 672a8c91ba..bd6281a7d3 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -4,7 +4,6 @@ using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; From 7437d21f498cca28084c7ed70341e714392e0690 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 24 Dec 2023 00:45:22 +0900 Subject: [PATCH 039/539] Adjust comment regarding slider tail --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index bd6281a7d3..2eb257b3e6 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - // The tail has a special IgnoreMiss judgement + // The slider tail has a special judgement that can't accurately be described above. if (result.HitObject is SliderTailCircle && !result.IsHit) setComboResult(ComboResult.Good); From 92b490f2e79d850980fa9e681f7c62c1ba1d5692 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Dec 2023 01:59:48 +0900 Subject: [PATCH 040/539] Don't bother with alt support for now --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 80549343a5..80003aeaba 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -227,7 +227,10 @@ namespace osu.Game.Screens.Play.PlayerSettings public bool OnPressed(KeyBindingPressEvent e) { - double amount = e.AltPressed ? 1 : 5; + // To match stable, this should adjust by 5 ms, or 1 ms when holding alt. + // But that is hard to make work with global actions due to the operating mode. + // Let's use the more precise as a default for now. + const double amount = 1; switch (e.Action) { From 72bec527fd06714bd0b88f56ff1d7538bbf37710 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Dec 2023 02:36:27 +0900 Subject: [PATCH 041/539] Add conditions to match stable offset adjust limitations --- .../PlayerSettings/BeatmapOffsetControl.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 80003aeaba..b0e7d08699 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -51,6 +51,12 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private Player? player { get; set; } + + [Resolved] + private IGameplayClock? gameplayClock { get; set; } + private double lastPlayAverage; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; @@ -227,6 +233,18 @@ namespace osu.Game.Screens.Play.PlayerSettings public bool OnPressed(KeyBindingPressEvent e) { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + if (player is SubmittingPlayer) + { + // TODO: the blocking conditions should probably display a message. + if (player?.IsBreakTime.Value == false && gameplayClock?.CurrentTime - gameplayClock?.StartTime > 10000) + return false; + + if (gameplayClock?.IsPaused.Value == true) + return false; + } + // To match stable, this should adjust by 5 ms, or 1 ms when holding alt. // But that is hard to make work with global actions due to the operating mode. // Let's use the more precise as a default for now. From 686b2a4394ac735ef309976910fed859f3f3b779 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Dec 2023 03:00:51 +0900 Subject: [PATCH 042/539] Disable positional interaction for now --- osu.Game/Screens/Play/GameplayOffsetControl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayOffsetControl.cs b/osu.Game/Screens/Play/GameplayOffsetControl.cs index 3f5a5bef2a..2f0cb821ec 100644 --- a/osu.Game/Screens/Play/GameplayOffsetControl.cs +++ b/osu.Game/Screens/Play/GameplayOffsetControl.cs @@ -25,6 +25,9 @@ namespace osu.Game.Screens.Play public override bool PropagateNonPositionalInputSubTree => true; + // Disable interaction for now to avoid any funny business with slider bar dragging. + public override bool PropagatePositionalInputSubTree => false; + private BeatmapOffsetControl offsetControl = null!; private OsuTextFlowContainer text = null!; From 5b03dc8d0bb1d189dc68103971fccf41bfc2a008 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Dec 2023 03:20:42 +0900 Subject: [PATCH 043/539] Use a realm subscription to avoid overhead when hovering a toolbar button Addresses https://github.com/ppy/osu/discussions/26018. --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 08bcb6bd8a..344f7b2018 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -157,6 +157,15 @@ namespace osu.Game.Overlays.Toolbar }; } + [BackgroundDependencyLoader] + private void load() + { + if (Hotkey != null) + { + realm.SubscribeToPropertyChanged(r => r.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip); + } + } + protected override bool OnMouseDown(MouseDownEvent e) => false; protected override bool OnClick(ClickEvent e) @@ -168,8 +177,6 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnHover(HoverEvent e) { - updateKeyBindingTooltip(); - HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); @@ -197,19 +204,12 @@ namespace osu.Game.Overlays.Toolbar { } - private void updateKeyBindingTooltip() + private void updateKeyBindingTooltip(string keyCombination) { - if (Hotkey == null) return; + string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination); - var realmKeyBinding = realm.Realm.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) - { - string keyBindingString = keyCombinationProvider.GetReadableString(realmKeyBinding.KeyCombination); - - if (!string.IsNullOrEmpty(keyBindingString)) - keyBindingTooltip.Text = $" ({keyBindingString})"; - } + if (!string.IsNullOrEmpty(keyBindingString)) + keyBindingTooltip.Text = $" ({keyBindingString})"; } } From 68430d6ecdbf7810285a818e4f762c07477f6de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Dec 2023 19:39:17 +0100 Subject: [PATCH 044/539] Fix toolbar keybinding hint not clearing after unbinding the keybinding --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 344f7b2018..81d2de5acb 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -208,8 +208,9 @@ namespace osu.Game.Overlays.Toolbar { string keyBindingString = keyCombinationProvider.GetReadableString(keyCombination); - if (!string.IsNullOrEmpty(keyBindingString)) - keyBindingTooltip.Text = $" ({keyBindingString})"; + keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) + ? $" ({keyBindingString})" + : string.Empty; } } From 19d02364185b72e97c157b42f1d668c2fa794dd8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 23 Dec 2023 22:11:00 +0300 Subject: [PATCH 045/539] Change mod acronym --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index f71acf95b8..b70d607ca1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Name => "Depth"; - public override string Acronym => "DH"; + public override string Acronym => "DP"; public override IconUsage? Icon => FontAwesome.Solid.Cube; public override ModType Type => ModType.Fun; public override LocalisableString Description => "3D. Almost."; From b1d994b6ffb617f12a09aca20cf0482a3b5db2b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 17:17:23 +0900 Subject: [PATCH 046/539] Add classic skin sprites for slider tick and slider end misses --- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 16 +++++++++++++--- osu.Game/Skinning/LegacySkin.cs | 13 ++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index b8fe2f8d06..1834a17279 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -58,14 +58,24 @@ namespace osu.Game.Skinning if (result.IsMiss()) { + decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; + + // missed ticks / slider end don't get the normal animation. if (isMissedTick()) - applyMissedTickScaling(); - else { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - decimal? legacyVersion = skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value; + if (legacyVersion > 1.0m) + { + this.MoveTo(new Vector2(0, -2f)); + this.MoveToOffset(new Vector2(0, 10), fade_out_delay + fade_out_length, Easing.In); + } + } + else + { + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); if (legacyVersion > 1.0m) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index a37a386889..270abe2849 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -453,11 +453,18 @@ namespace osu.Game.Skinning private Drawable? getJudgementAnimation(HitResult result) { - if (result.IsMiss()) - return this.GetAnimation("hit0", true, false); - switch (result) { + case HitResult.Miss: + return this.GetAnimation("hit0", true, false); + + case HitResult.LargeTickMiss: + return this.GetAnimation("slidertickmiss", true, false); + + case HitResult.ComboBreak: + case HitResult.IgnoreMiss: + return this.GetAnimation("sliderendmiss", true, false); + case HitResult.Meh: return this.GetAnimation("hit50", true, false); From 02c771f540489eedaf56719632f500981b20da6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 17:27:15 +0900 Subject: [PATCH 047/539] Add warning for linux users about to report a non-bug as a bug --- .github/ISSUE_TEMPLATE/bug-issue.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 00a873f9c8..dfdcf8d320 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -11,6 +11,10 @@ body: - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful. + # ATTENTION LINUX USERS + + If you are having an issue and it is hardware related, **please open a [q&a discussion](https://github.com/ppy/osu/discussions/categories/q-a)** instead of an issue. There's a high chance your issue is due to your system configuration, and not our software. + - type: dropdown attributes: label: Type From 8e6ea2dd9b098aa41f32d54a3c953218aee459fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 17:32:30 +0900 Subject: [PATCH 048/539] Update argon and triangles to match display style --- .../Skinning/Argon/ArgonJudgementPiece.cs | 11 ++++++++++- .../Rulesets/Judgements/DefaultJudgementPiece.cs | 15 ++++++++++++++- osu.Game/Rulesets/Scoring/HitResult.cs | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs index edeece0293..bb61bd37c1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs @@ -62,7 +62,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon /// public virtual void PlayAnimation() { - if (Result.IsMiss()) + if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) + { + this.RotateTo(-45); + this.ScaleTo(1.8f); + this.ScaleTo(1.2f, 100, Easing.In); + + this.MoveTo(Vector2.Zero); + this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + } + else if (Result.IsMiss()) { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 3c5e37f91c..ada651b60e 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -38,7 +38,20 @@ namespace osu.Game.Rulesets.Judgements /// public virtual void PlayAnimation() { - if (Result != HitResult.None && !Result.IsHit()) + // TODO: make these better. currently they are using a text `-` and it's not centered properly. + // Should be an explicit drawable. + // + // When this is done, remove the [Description] attributes from HitResults which were added for this purpose. + if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) + { + this.RotateTo(-45); + this.ScaleTo(1.8f); + this.ScaleTo(1.2f, 100, Easing.In); + + this.MoveTo(Vector2.Zero); + this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + } + else if (Result.IsMiss()) { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index e174ebd00f..2d55f1a649 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick miss. /// [EnumMember(Value = "large_tick_miss")] - [Description(@"x")] + [Description("-")] [Order(10)] LargeTickMiss, @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a miss that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_miss")] - [Description("x")] + [Description("-")] [Order(13)] IgnoreMiss, From 8142a7cb7e82d7053004cd6e4b2f62e74254ed57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Dec 2023 14:00:32 +0100 Subject: [PATCH 049/539] Remove incorrect spec --- osu.Game/Skinning/LegacySkin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 270abe2849..8f0cd59b68 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -461,7 +461,6 @@ namespace osu.Game.Skinning case HitResult.LargeTickMiss: return this.GetAnimation("slidertickmiss", true, false); - case HitResult.ComboBreak: case HitResult.IgnoreMiss: return this.GetAnimation("sliderendmiss", true, false); From 4fa35d709cb163a1a93bc17c6772f2b034614e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Dec 2023 14:56:40 +0100 Subject: [PATCH 050/539] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2ec954b3a5..fbcdb00cdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From f84b07e71a3298a5093eab2806a74c0ef2066679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Dec 2023 19:01:06 +0100 Subject: [PATCH 051/539] Do not attempt to stop preview tracks when arriving from a "track completed" sync This fixes an issue identified with the WASAPI implementation in https://github.com/ppy/osu-framework/pull/6088. It has no real effect on current `master`, but fixes a deadlock that occurs with the aforementioned framework branch when one lets a preview track play out to the end - at this point all audio will stop and an attempt to perform any synchronous BASS operation (playing another track, seeking) will result in a deadlock. It isn't terribly clear as to why this is happening precisely, but there does not appear to be any need to stop and seek at that point, so this feels like a decent workaround even if the actual issue is upstream (and will unblock pushing out WASAPI support to users). --- osu.Game/Audio/PreviewTrack.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs index d625566ee7..6184ff85dd 100644 --- a/osu.Game/Audio/PreviewTrack.cs +++ b/osu.Game/Audio/PreviewTrack.cs @@ -96,10 +96,13 @@ namespace osu.Game.Audio hasStarted = false; - Track.Stop(); + if (!Track.HasCompleted) + { + Track.Stop(); - // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value. - Track.Seek(0); + // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value. + Track.Seek(0); + } Stopped?.Invoke(); } From 060bf8beff4f58060e9d60f41dce5850a20748ba Mon Sep 17 00:00:00 2001 From: Nathan Tran Date: Mon, 25 Dec 2023 15:09:39 -0800 Subject: [PATCH 052/539] Fix rewind backtracking --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 370b559897..89911c9a69 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -643,7 +643,7 @@ namespace osu.Game.Screens.Select while (randomSelectedBeatmaps.Any()) { var beatmap = randomSelectedBeatmaps[^1]; - randomSelectedBeatmaps.Remove(beatmap); + randomSelectedBeatmaps.RemoveAt(randomSelectedBeatmaps.Count - 1); if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true) { From b18b5b99773bffbd5cea8f2b536db763ae983f80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 12:06:56 +0900 Subject: [PATCH 053/539] Add inline note about deadlock --- osu.Game/Audio/PreviewTrack.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs index 6184ff85dd..961990a1bd 100644 --- a/osu.Game/Audio/PreviewTrack.cs +++ b/osu.Game/Audio/PreviewTrack.cs @@ -96,6 +96,7 @@ namespace osu.Game.Audio hasStarted = false; + // This pre-check is important, fixes a BASS deadlock in some scenarios. if (!Track.HasCompleted) { Track.Stop(); From 2ec6aa7fbb92aa286f1281c4043274f6d007e98b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 26 Dec 2023 12:46:21 +0900 Subject: [PATCH 054/539] Make mania scroll speed independent of hit position --- .../UI/DrawableManiaRuleset.cs | 20 ++++++++++++++++++- .../Skinning/LegacyManiaSkinConfiguration.cs | 4 +++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 9169599798..bea536e4af 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -19,12 +19,14 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { @@ -57,6 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(0); + [Resolved] + private ISkinSource skin { get; set; } + public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { @@ -104,7 +109,20 @@ namespace osu.Game.Rulesets.Mania.UI updateTimeRange(); } - private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; + private void updateTimeRange() + { + float hitPosition = skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + ?? Stage.HIT_TARGET_POSITION; + + const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION; + float lengthToHitPosition = 768 - hitPosition; + + // This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position. + float scale = lengthToHitPosition / length_to_default_hit_position; + + TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; + } /// /// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40. diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 9acb29a793..042836984a 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -21,6 +21,8 @@ namespace osu.Game.Skinning /// public const float DEFAULT_COLUMN_SIZE = 30 * POSITION_SCALE_FACTOR; + public const float DEFAULT_HIT_POSITION = (480 - 402) * POSITION_SCALE_FACTOR; + public readonly int Keys; public Dictionary CustomColours { get; } = new Dictionary(); @@ -35,7 +37,7 @@ namespace osu.Game.Skinning public readonly float[] ExplosionWidth; public readonly float[] HoldNoteLightWidth; - public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; + public float HitPosition = DEFAULT_HIT_POSITION; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; From f9e47242db508d596fe92afef5e15ab5f6583c1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 17:44:49 +0900 Subject: [PATCH 055/539] Add visual offset to better align editor waveforms with expectations --- .../Edit/Compose/Components/Timeline/Timeline.cs | 7 ++++++- osu.Game/Screens/Edit/Editor.cs | 13 +++++++++++++ .../Edit/Timing/WaveformComparisonDisplay.cs | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 75de15fe56..83d34ab61a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -141,7 +141,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); track.BindTo(editorClock.Track); - track.BindValueChanged(_ => waveform.Waveform = beatmap.Value.Waveform, true); + track.BindValueChanged(_ => + { + waveform.Waveform = beatmap.Value.Waveform; + waveform.RelativePositionAxes = Axes.X; + waveform.X = -(float)(Editor.WAVEFORM_VISUAL_OFFSET / beatmap.Value.Track.Length); + }, true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a6c18bdf0e..c1f6c02301 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -60,6 +60,19 @@ namespace osu.Game.Screens.Edit [Cached] public partial class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider { + /// + /// An offset applied to waveform visuals to align them with expectations. + /// + /// + /// Historically, osu! beatmaps have an assumption of full system latency baked in. + /// This comes from a culmination of stable's platform offset, average hardware playback + /// latency, and users having their universal offsets tweaked to previous beatmaps. + /// + /// Coming to this value involved running various tests with existing users / beatmaps. + /// This included both visual and audible comparisons. Ballpark confidence is ≈2 ms. + /// + public const float WAVEFORM_VISUAL_OFFSET = 20; + public override float BackgroundParallaxAmount => 0.1f; public override bool AllowBackButton => false; diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 856bc7c303..b5315feccb 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -219,7 +219,7 @@ namespace osu.Game.Screens.Edit.Timing // offset to the required beat index. double time = selectedGroupStartTime + index * timingPoint.BeatLength; - float offset = (float)(time - visible_width / 2) / trackLength * scale; + float offset = (float)(time - visible_width / 2 + Editor.WAVEFORM_VISUAL_OFFSET) / trackLength * scale; row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; row.WaveformOffsetTo(-offset, animated); From 4e3bdb2b56bd0d8430ffeeb988498d42412d72ee Mon Sep 17 00:00:00 2001 From: Nathan Tran Date: Tue, 26 Dec 2023 00:57:06 -0800 Subject: [PATCH 056/539] Add test coverage --- .../SongSelect/TestSceneBeatmapCarousel.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c509d40e07..41ea347ef3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -454,6 +454,23 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); } + [Test] + public void TestRewind() + { + const int local_set_count = 3; + const int random_select_count = local_set_count * 3; + loadBeatmaps(setCount: local_set_count); + + for (int i = 0; i < random_select_count; i++) + nextRandom(); + + for (int i = 0; i < random_select_count; i++) + { + prevRandom(); + AddAssert("correct random last selected", () => selectedSets.Peek() == carousel.SelectedBeatmapSet); + } + } + [Test] public void TestRewindToDeletedBeatmap() { From 225528d519cf65420cf54055ff0602edc3c211d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 19:20:41 +0900 Subject: [PATCH 057/539] Bail from score submission if audio playback rate is too far from reality Closes https://github.com/ppy/osu/issues/23149. --- .../Play/MasterGameplayClockContainer.cs | 55 +++++++++++++++++++ osu.Game/Screens/Play/SubmittingPlayer.cs | 8 +++ 2 files changed, 63 insertions(+) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 54ed7ba626..2844d84f31 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -39,6 +40,14 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; + /// + /// Whether the audio playback is within acceptable ranges. + /// Will become false if audio playback is not going as expected. + /// + public IBindable PlaybackRateValid => playbackRateValid; + + private readonly Bindable playbackRateValid = new Bindable(true); + private readonly WorkingBeatmap beatmap; private Track track; @@ -128,6 +137,7 @@ namespace osu.Game.Screens.Play { // Safety in case the clock is seeked while stopped. LastStopTime = null; + elapsedValidationTime = null; base.Seek(time); } @@ -197,6 +207,51 @@ namespace osu.Game.Screens.Play addAdjustmentsToTrack(); } + protected override void Update() + { + base.Update(); + checkPlaybackValidity(); + } + + #region Clock validation (ensure things are running correctly for local gameplay) + + private double elapsedGameplayClockTime; + private double? elapsedValidationTime; + private int playbackDiscrepancyCount; + + private const int allowed_playback_discrepancies = 5; + + private void checkPlaybackValidity() + { + if (GameplayClock.IsRunning) + { + elapsedGameplayClockTime += GameplayClock.ElapsedFrameTime; + + elapsedValidationTime ??= elapsedGameplayClockTime; + elapsedValidationTime += GameplayClock.Rate * Time.Elapsed; + + if (Math.Abs(elapsedGameplayClockTime - elapsedValidationTime!.Value) > 300) + { + if (playbackDiscrepancyCount++ > allowed_playback_discrepancies) + { + if (playbackRateValid.Value) + { + playbackRateValid.Value = false; + Logger.Log("System audio playback is not working as expected. Some online functionality will not work.\n\nPlease check your audio drivers.", level: LogLevel.Important); + } + } + else + { + Logger.Log($"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); + } + + elapsedValidationTime = null; + } + } + } + + #endregion + private bool speedAdjustmentsApplied; private void addAdjustmentsToTrack() diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 785164178a..f88526b8f9 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -208,6 +208,14 @@ namespace osu.Game.Screens.Play private Task submitScore(Score score) { + var masterClock = GameplayClockContainer as MasterGameplayClockContainer; + + if (masterClock?.PlaybackRateValid.Value != true) + { + Logger.Log("Score submission cancelled due to audio playback rate discrepancy."); + return Task.CompletedTask; + } + // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). if (token == null) { From 30b5b36f1d72f9285971287d6d78c4628c3dde94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 12:19:04 +0100 Subject: [PATCH 058/539] Fix code quality inspection --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 41ea347ef3..aa4c879468 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -467,7 +467,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < random_select_count; i++) { prevRandom(); - AddAssert("correct random last selected", () => selectedSets.Peek() == carousel.SelectedBeatmapSet); + AddAssert("correct random last selected", () => selectedSets.Peek(), () => Is.EqualTo(carousel.SelectedBeatmapSet)); } } From f2c0e7cf2ecc04d5f3f01bd945623a61a8ab7f04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 20:31:34 +0900 Subject: [PATCH 059/539] Fix editor's control point list refreshing multiple times for a single change --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 22e37b9efb..7cd1dbc630 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -109,8 +109,13 @@ namespace osu.Game.Screens.Edit.Timing controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((_, _) => { - table.ControlGroups = controlPointGroups; - changeHandler?.SaveState(); + // This callback can happen many times in a change operation. It gets expensive. + // We really should be handling the `CollectionChanged` event properly. + Scheduler.AddOnce(() => + { + table.ControlGroups = controlPointGroups; + changeHandler?.SaveState(); + }); }, true); table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); From 1f2f749db68fe3a541150abb7acdae7e0aabf79e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 20:42:04 +0900 Subject: [PATCH 060/539] Fix selection not being retained in control point list when undoing / redoing --- osu.Game/Screens/Edit/EditorTable.cs | 30 ++++++++++++++++++- .../Screens/Edit/Timing/ControlPointTable.cs | 14 +++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index b79d71b42b..5ccb21cf59 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -46,15 +47,42 @@ namespace osu.Game.Screens.Edit }); } - protected void SetSelectedRow(object? item) + protected int GetIndexForObject(object? item) { + for (int i = 0; i < BackgroundFlow.Count; i++) + { + if (BackgroundFlow[i].Item == item) + return i; + } + + return -1; + } + + protected bool SetSelectedRow(object? item) + { + bool foundSelection = false; + foreach (var b in BackgroundFlow) { b.Selected = ReferenceEquals(b.Item, item); if (b.Selected) + { + Debug.Assert(!foundSelection); OnRowSelected?.Invoke(b); + foundSelection = true; + } } + + return foundSelection; + } + + protected object? GetObjectAtIndex(int index) + { + if (index < 0 || index > BackgroundFlow.Count - 1) + return null; + + return BackgroundFlow[index].Item; } protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index b078e3fa44..335077c6f0 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit.Timing public partial class ControlPointTable : EditorTable { [Resolved] - private Bindable selectedGroup { get; set; } = null!; + private Bindable selectedGroup { get; set; } = null!; [Resolved] private EditorClock clock { get; set; } = null!; @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Edit.Timing { set { + int selectedIndex = GetIndexForObject(selectedGroup.Value); + Content = null; BackgroundFlow.Clear(); @@ -53,7 +55,11 @@ namespace osu.Game.Screens.Edit.Timing Columns = createHeaders(); Content = value.Select(createContent).ToArray().ToRectangular(); - updateSelectedGroup(); + if (!SetSelectedRow(selectedGroup.Value)) + { + // Some operations completely obliterate references, so best-effort reselect based on index. + selectedGroup.Value = GetObjectAtIndex(selectedIndex) as ControlPointGroup; + } } } @@ -61,11 +67,9 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(_ => updateSelectedGroup(), true); + selectedGroup.BindValueChanged(_ => SetSelectedRow(selectedGroup.Value), true); } - private void updateSelectedGroup() => SetSelectedRow(selectedGroup.Value); - private TableColumn[] createHeaders() { var columns = new List From 03e2463b06b14b66529627fc76941d901231001c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Dec 2023 21:20:18 +0900 Subject: [PATCH 061/539] Add test coverage and refactor to better handle equality edge case --- .../Visual/Editing/TestSceneTimingScreen.cs | 43 +++++++++++++++++++ osu.Game/Screens/Edit/EditorTable.cs | 2 +- .../Screens/Edit/Timing/ControlPointTable.cs | 29 ++++++++++--- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 216c35de65..40aadc8164 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing.RowAttributes; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -69,6 +70,48 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType().Any()); } + [Test] + public void TestSelectedRetainedOverUndo() + { + AddStep("Select first timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170); + + AddStep("Adjust offset", () => + { + InputManager.MoveMouseTo(timingScreen.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for offset changed", () => + { + return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + }); + + AddStep("simulate undo", () => + { + var clone = editorBeatmap.ControlPointInfo.DeepClone(); + + editorBeatmap.ControlPointInfo.Clear(); + + foreach (var group in clone.Groups) + { + foreach (var cp in group.ControlPoints) + editorBeatmap.ControlPointInfo.Add(group.Time, cp); + } + }); + + AddUntilStep("selection retained", () => + { + return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + }); + } + [Test] public void TestTrackingCurrentTimeWhileRunning() { diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index 5ccb21cf59..e5dc540b06 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit return -1; } - protected bool SetSelectedRow(object? item) + protected virtual bool SetSelectedRow(object? item) { bool foundSelection = false; diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 335077c6f0..7a27056da3 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Edit.Timing { Action = () => { - selectedGroup.Value = group; + SetSelectedRow(group); clock.SeekSmoothlyTo(group.Time); } }); @@ -55,11 +55,16 @@ namespace osu.Game.Screens.Edit.Timing Columns = createHeaders(); Content = value.Select(createContent).ToArray().ToRectangular(); - if (!SetSelectedRow(selectedGroup.Value)) - { - // Some operations completely obliterate references, so best-effort reselect based on index. - selectedGroup.Value = GetObjectAtIndex(selectedIndex) as ControlPointGroup; - } + // Attempt to retain selection. + if (SetSelectedRow(selectedGroup.Value)) + return; + + // Some operations completely obliterate references, so best-effort reselect based on index. + if (SetSelectedRow(GetObjectAtIndex(selectedIndex))) + return; + + // Selection could not be retained. + selectedGroup.Value = null; } } @@ -67,7 +72,17 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(_ => SetSelectedRow(selectedGroup.Value), true); + // Handle external selections. + selectedGroup.BindValueChanged(g => SetSelectedRow(g.NewValue), true); + } + + protected override bool SetSelectedRow(object? item) + { + if (!base.SetSelectedRow(item)) + return false; + + selectedGroup.Value = item as ControlPointGroup; + return true; } private TableColumn[] createHeaders() From d70fddb6fde18de0ef092c047509ee0a2efd2d16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 00:11:22 +0900 Subject: [PATCH 062/539] Fix elapsed time being counted twice on first frame --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 2844d84f31..a475f4823f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -227,8 +227,10 @@ namespace osu.Game.Screens.Play { elapsedGameplayClockTime += GameplayClock.ElapsedFrameTime; - elapsedValidationTime ??= elapsedGameplayClockTime; - elapsedValidationTime += GameplayClock.Rate * Time.Elapsed; + if (elapsedValidationTime == null) + elapsedValidationTime = elapsedGameplayClockTime; + else + elapsedValidationTime += GameplayClock.Rate * Time.Elapsed; if (Math.Abs(elapsedGameplayClockTime - elapsedValidationTime!.Value) > 300) { From c55458e49c6b5747a1a84e00ac05787829c81a2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:32:47 +0900 Subject: [PATCH 063/539] Remove pointless intermediary class --- .../Containers/ExpandingButtonContainer.cs | 21 ------------------- osu.Game/Overlays/Settings/SettingsSidebar.cs | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 osu.Game/Graphics/Containers/ExpandingButtonContainer.cs diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs deleted file mode 100644 index 5abb4096ac..0000000000 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Graphics.Containers -{ - /// - /// An with a long hover expansion delay. - /// - /// - /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". - /// - public partial class ExpandingButtonContainer : ExpandingContainer - { - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) - : base(contractedWidth, expandedWidth) - { - } - - protected override double HoverExpansionDelay => 400; - } -} diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 06bc2fd788..7baf9a58ff 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -8,7 +8,7 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { - public partial class SettingsSidebar : ExpandingButtonContainer + public partial class SettingsSidebar : ExpandingContainer { public const float DEFAULT_WIDTH = 70; public const int EXPANDED_WIDTH = 200; From 58476d5429985109a13240c481da362449489167 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:33:02 +0900 Subject: [PATCH 064/539] Allow `ExpandingContainer` to not auto expand on hover --- osu.Game/Graphics/Containers/ExpandingContainer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs index 60b9e6a167..2abdb508ae 100644 --- a/osu.Game/Graphics/Containers/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,6 +24,8 @@ namespace osu.Game.Graphics.Containers /// protected virtual double HoverExpansionDelay => 0; + protected virtual bool ExpandOnHover => true; + protected override Container Content => FillFlow; protected FillFlowContainer FillFlow { get; } @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Containers }; } - private ScheduledDelegate hoverExpandEvent; + private ScheduledDelegate? hoverExpandEvent; protected override void LoadComplete() { @@ -93,6 +93,9 @@ namespace osu.Game.Graphics.Containers private void updateHoverExpansion() { + if (!ExpandOnHover) + return; + hoverExpandEvent?.Cancel(); if (IsHovered && !Expanded.Value) From 9a1a97180d7938bbc1dced8d887cc226f28deb46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:33:17 +0900 Subject: [PATCH 065/539] Change settings overlay to always show expanded buttons --- osu.Game/Overlays/Settings/SettingsSidebar.cs | 3 +++ osu.Game/Overlays/SettingsOverlay.cs | 7 +++++-- osu.Game/Overlays/SettingsPanel.cs | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 7baf9a58ff..fc5c6b07bb 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -13,9 +13,12 @@ namespace osu.Game.Overlays.Settings public const float DEFAULT_WIDTH = 70; public const int EXPANDED_WIDTH = 200; + protected override bool ExpandOnHover => false; + public SettingsSidebar() : base(DEFAULT_WIDTH, EXPANDED_WIDTH) { + Expanded.Value = true; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 746d451343..a779c3c263 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -72,16 +72,19 @@ namespace osu.Game.Overlays switch (state.NewValue) { case Visibility.Visible: - Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); + Sidebar.Expanded.Value = false; + Sidebar.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint); ContentContainer.MoveToX(-PANEL_WIDTH, 500, Easing.OutQuint); lastOpenedSubPanel = panel; + break; case Visibility.Hidden: - Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); + Sidebar.Expanded.Value = true; + Sidebar.FadeColour(Color4.White, 300, Easing.OutQuint); SectionsContainer.FadeIn(500, Easing.OutQuint); ContentContainer.MoveToX(0, 500, Easing.OutQuint); diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 3bac6c400f..339120fd84 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -285,7 +285,6 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.Expanded.Value = false; }, }; } From 5de8307918216ed5f18edb42bfc8eb28c6f310b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:38:23 +0900 Subject: [PATCH 066/539] Reduce width of sidebar buttons --- osu.Game/Overlays/Settings/SettingsSidebar.cs | 2 +- osu.Game/Overlays/Settings/SidebarIconButton.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index fc5c6b07bb..c751f12003 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Settings public partial class SettingsSidebar : ExpandingContainer { public const float DEFAULT_WIDTH = 70; - public const int EXPANDED_WIDTH = 200; + public const int EXPANDED_WIDTH = 170; protected override bool ExpandOnHover => false; diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index 4e5b361460..bd9ac3cf97 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -69,18 +69,18 @@ namespace osu.Game.Overlays.Settings Colour = OsuColour.Gray(0.6f), Children = new Drawable[] { - headerText = new OsuSpriteText - { - Position = new Vector2(SettingsSidebar.DEFAULT_WIDTH + 10, 0), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, iconContainer = new ConstrainedIconContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(20), }, + headerText = new OsuSpriteText + { + Position = new Vector2(60, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, } }, selectionIndicator = new CircularContainer From 5b1deb7c4b1139aa55543094dc652b6763ddfcd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:40:50 +0900 Subject: [PATCH 067/539] Give buttons a touch of padding to make click effect feel better --- osu.Game/Overlays/Settings/SidebarIconButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index bd9ac3cf97..a8d27eb792 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -60,6 +60,8 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.X; Height = 46; + Padding = new MarginPadding(5); + AddRange(new Drawable[] { textIconContent = new Container From 8e13f65c5d388ba0494367dfedcb2cccdc066b84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:46:21 +0900 Subject: [PATCH 068/539] Adjust hover effect slightly --- osu.Game/Overlays/Settings/SidebarIconButton.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index a8d27eb792..041d19e8bf 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -111,10 +111,13 @@ namespace osu.Game.Overlays.Settings private void load() { selectionIndicator.Colour = ColourProvider.Highlight1; + Hover.Colour = ColourProvider.Light4; } protected override void UpdateState() { + Hover.FadeTo(IsHovered ? 0.1f : 0, FADE_DURATION, Easing.OutQuint); + if (Selected) { textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint); From 5d0b5247946e758fd28f8bad58ec16cf7c15ee18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:54:24 +0900 Subject: [PATCH 069/539] Adjust back button to match style better --- osu.Game/Overlays/Settings/SidebarButton.cs | 7 ++++++- osu.Game/Overlays/Settings/SidebarIconButton.cs | 3 +-- osu.Game/Overlays/SettingsSubPanel.cs | 13 +++++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index a63688762d..f58c2f41ef 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Settings private void load() { BackgroundColour = ColourProvider.Background5; + Hover.Colour = ColourProvider.Light4; } protected override void LoadComplete() @@ -40,6 +42,9 @@ namespace osu.Game.Overlays.Settings protected override void OnHoverLost(HoverLostEvent e) => UpdateState(); - protected abstract void UpdateState(); + protected virtual void UpdateState() + { + Hover.FadeTo(IsHovered ? 0.1f : 0, FADE_DURATION, Easing.OutQuint); + } } } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index 041d19e8bf..1bb3aa2921 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -111,12 +111,11 @@ namespace osu.Game.Overlays.Settings private void load() { selectionIndicator.Colour = ColourProvider.Highlight1; - Hover.Colour = ColourProvider.Light4; } protected override void UpdateState() { - Hover.FadeTo(IsHovered ? 0.1f : 0, FADE_DURATION, Easing.OutQuint); + base.UpdateState(); if (Selected) { diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 1651975a74..1b9edeebc2 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -47,7 +47,9 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - Size = new Vector2(SettingsSidebar.DEFAULT_WIDTH); + Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); + + Padding = new MarginPadding(5); AddRange(new Drawable[] { @@ -61,7 +63,8 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(15), + Y = -5, + Size = new Vector2(30), Shadow = true, Icon = FontAwesome.Solid.ChevronLeft }, @@ -69,8 +72,8 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Y = 15, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Y = 30, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), Text = @"back", }, } @@ -80,6 +83,8 @@ namespace osu.Game.Overlays protected override void UpdateState() { + base.UpdateState(); + content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); } } From c087578e011c6ade4411dbccfcfce80da407dba3 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Tue, 26 Dec 2023 10:07:21 -0800 Subject: [PATCH 070/539] Force minimum cursor size for `OsuResumeOverlay` On cursor sizes below 0.3x it becomes exceedingly difficult to quickly locate and then accurately click the resume cursor on the pause overlay as it could as big as a handful of pixels. This clamps the minimum cursor size to 1x for the resume overlay, which is way more comfortable and more closely resembles stable. --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 10 +++++----- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index ba9fda25e4..18351c20ce 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }; userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); - userCursorScale.ValueChanged += _ => calculateCursorScale(); + userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale(); autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); - autoCursorScale.ValueChanged += _ => calculateCursorScale(); + autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale(); cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true); } @@ -81,10 +81,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override void LoadComplete() { base.LoadComplete(); - calculateCursorScale(); + cursorScale.Value = CalculateCursorScale(); } - private void calculateCursorScale() + protected virtual float CalculateCursorScale() { float scale = userCursorScale.Value; @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); } - cursorScale.Value = scale; + return scale; } protected override void SkinChanged(ISkinSource skin) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index ea49836772..f5e83f46f2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Osu.UI RelativePositionAxes = Axes.Both; } + protected override float CalculateCursorScale() + { + // Force minimum cursor size so it's easily clickable + return Math.Max(1f, base.CalculateCursorScale()); + } + protected override bool OnHover(HoverEvent e) { updateColour(); From c107bfcd3112cc8b1589d84f4634be1688cc684d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 20:09:34 +0100 Subject: [PATCH 071/539] Fix `TestSceneSideOverlays` test failure --- osu.Game/Overlays/Settings/SettingsSidebar.cs | 4 ++-- osu.Game/Overlays/Settings/SidebarIconButton.cs | 2 +- osu.Game/Overlays/SettingsPanel.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index c751f12003..302c52fbd2 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -10,13 +10,13 @@ namespace osu.Game.Overlays.Settings { public partial class SettingsSidebar : ExpandingContainer { - public const float DEFAULT_WIDTH = 70; + public const float CONTRACTED_WIDTH = 70; public const int EXPANDED_WIDTH = 170; protected override bool ExpandOnHover => false; public SettingsSidebar() - : base(DEFAULT_WIDTH, EXPANDED_WIDTH) + : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) { Expanded.Value = true; } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index 1bb3aa2921..e7ae4cc81d 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Settings { textIconContent = new Container { - Width = SettingsSidebar.DEFAULT_WIDTH, + Width = SettingsSidebar.CONTRACTED_WIDTH, RelativeSizeAxes = Axes.Y, Colour = OsuColour.Gray(0.6f), Children = new Drawable[] diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 339120fd84..f4dfc7fa27 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; - private const float sidebar_width = SettingsSidebar.DEFAULT_WIDTH; + private const float sidebar_width = SettingsSidebar.EXPANDED_WIDTH; /// /// The width of the settings panel content, excluding the sidebar. From 9ac79782d25f3625d79d1d9096cb3eecacbdad21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 20:21:15 +0100 Subject: [PATCH 072/539] Add visibility toggle step to settings panel test scene --- osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 5d9c2f890c..8c4ca47fc3 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Settings public void TestBasic() { AddStep("do nothing", () => { }); + AddToggleStep("toggle visibility", visible => settings.State.Value = visible ? Visibility.Visible : Visibility.Hidden); } [Test] From af47f9fd701897a3096c785a5c2e31c2b87ee6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 20:24:43 +0100 Subject: [PATCH 073/539] Fix sidebar button text becoming masked away during fadeout --- osu.Game/Overlays/Settings/SidebarIconButton.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index e7ae4cc81d..f4b71207e3 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -66,16 +66,16 @@ namespace osu.Game.Overlays.Settings { textIconContent = new Container { - Width = SettingsSidebar.CONTRACTED_WIDTH, - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.6f), Children = new Drawable[] { iconContainer = new ConstrainedIconContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Size = new Vector2(20), + Margin = new MarginPadding { Left = 25 } }, headerText = new OsuSpriteText { From 6f672b8cb302fb7aa5034b07a2b6f0f351df54aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 20:36:12 +0100 Subject: [PATCH 074/539] Fix `TestSceneKeyBindingPanel` failures --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 1c4e89e1a2..57c9770c9a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays; -using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Rulesets.Taiko; using osuTK.Input; @@ -152,7 +151,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click first row with two bindings", () => { multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); - InputManager.MoveMouseTo(multiBindingRow); + InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); @@ -256,7 +255,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click first row with two bindings", () => { multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); - InputManager.MoveMouseTo(multiBindingRow); + InputManager.MoveMouseTo(multiBindingRow.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); @@ -305,7 +304,6 @@ namespace osu.Game.Tests.Visual.Settings section.ChildrenOfType().Single().TriggerClick(); }); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); scrollToAndStartBinding("Left (rim)"); AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); @@ -325,7 +323,6 @@ namespace osu.Game.Tests.Visual.Settings section.ChildrenOfType().Single().TriggerClick(); }); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); scrollToAndStartBinding("Left (rim)"); AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); @@ -345,7 +342,6 @@ namespace osu.Game.Tests.Visual.Settings section.ChildrenOfType().Single().TriggerClick(); }); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); scrollToAndStartBinding("Left (centre)"); AddStep("clear binding", () => { @@ -377,7 +373,6 @@ namespace osu.Game.Tests.Visual.Settings section.ChildrenOfType().Single().TriggerClick(); }); AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for collapsed", () => panel.ChildrenOfType().Single().Expanded.Value, () => Is.False); scrollToAndStartBinding("Left (centre)"); AddStep("clear binding", () => { From 8cd240fbecebf7bd37ba2cc504fe751c5afe727a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 13:20:19 +0900 Subject: [PATCH 075/539] Reduce size and fix alignment of back button --- osu.Game/Overlays/SettingsSubPanel.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 1b9edeebc2..4d1f8f45cc 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays public partial class BackButton : SidebarButton { - private Container content; + private Drawable content; public BackButton() : base(HoverSampleSet.Default) @@ -49,30 +49,31 @@ namespace osu.Game.Overlays { Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); - Padding = new MarginPadding(5); + Padding = new MarginPadding(40); AddRange(new Drawable[] { - content = new Container + content = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), Children = new Drawable[] { new SpriteIcon { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = -5, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Size = new Vector2(30), Shadow = true, Icon = FontAwesome.Solid.ChevronLeft }, new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = 30, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), Text = @"back", }, From 901674a1303a349185d5123f013a3d2e3d2d4fad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 13:39:13 +0900 Subject: [PATCH 076/539] Move back button inside sidebar to fix weird animation --- .../Visual/Settings/TestSceneSettingsPanel.cs | 2 +- osu.Game/Overlays/Settings/SettingsSidebar.cs | 79 ++++++++++++++++++- osu.Game/Overlays/SettingsOverlay.cs | 2 +- osu.Game/Overlays/SettingsPanel.cs | 13 +-- osu.Game/Overlays/SettingsSubPanel.cs | 71 ----------------- 5 files changed, 87 insertions(+), 80 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 8c4ca47fc3..df0fc8de57 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("Press back", () => settings .ChildrenOfType().FirstOrDefault()? - .ChildrenOfType().FirstOrDefault()?.TriggerClick()); + .ChildrenOfType().FirstOrDefault()?.TriggerClick()); AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index 302c52fbd2..ddbcd60ef6 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -1,10 +1,17 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -13,11 +20,16 @@ namespace osu.Game.Overlays.Settings public const float CONTRACTED_WIDTH = 70; public const int EXPANDED_WIDTH = 170; + public Action? BackButtonAction; + protected override bool ExpandOnHover => false; - public SettingsSidebar() + private readonly bool showBackButton; + + public SettingsSidebar(bool showBackButton) : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) { + this.showBackButton = showBackButton; Expanded.Value = true; } @@ -30,6 +42,71 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Both, Depth = float.MaxValue }); + + if (showBackButton) + { + AddInternal(new BackButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Action = () => BackButtonAction?.Invoke(), + }); + } + } + + public partial class BackButton : SidebarButton + { + private Drawable content = null!; + + public BackButton() + : base(HoverSampleSet.Default) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); + + Padding = new MarginPadding(40); + + AddRange(new[] + { + content = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(30), + Shadow = true, + Icon = FontAwesome.Solid.ChevronLeft + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), + Text = @"back", + }, + } + } + }); + } + + protected override void UpdateState() + { + base.UpdateState(); + + content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + } } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index a779c3c263..5735b1515a 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays protected override Drawable CreateFooter() => new SettingsFooter(); public SettingsOverlay() - : base(true) + : base(false) { } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index f4dfc7fa27..3861c5abc7 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; - private readonly bool showSidebar; + private readonly bool showBackButton; private LoadingLayer loading; @@ -72,9 +72,9 @@ namespace osu.Game.Overlays [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - protected SettingsPanel(bool showSidebar) + protected SettingsPanel(bool showBackButton) { - this.showSidebar = showSidebar; + this.showBackButton = showBackButton; RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; } @@ -146,10 +146,11 @@ namespace osu.Game.Overlays } }); - if (showSidebar) + AddInternal(Sidebar = new SettingsSidebar(showBackButton) { - AddInternal(Sidebar = new SettingsSidebar { Width = sidebar_width }); - } + BackButtonAction = Hide, + Width = sidebar_width + }); CreateSections()?.ForEach(AddSection); } diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 4d1f8f45cc..440639f06b 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -1,17 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; -using osuTK; namespace osu.Game.Overlays { @@ -25,69 +15,8 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - AddInternal(new BackButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = Hide - }); } protected override bool DimMainContent => false; // dimming is handled by main overlay - - public partial class BackButton : SidebarButton - { - private Drawable content; - - public BackButton() - : base(HoverSampleSet.Default) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); - - Padding = new MarginPadding(40); - - AddRange(new Drawable[] - { - content = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(30), - Shadow = true, - Icon = FontAwesome.Solid.ChevronLeft - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - Text = @"back", - }, - } - } - }); - } - - protected override void UpdateState() - { - base.UpdateState(); - - content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); - } - } } } From 81ba46216f0951e30329c3e2779a5380ee25e743 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 13:49:00 +0900 Subject: [PATCH 077/539] Speed up fades in transition to avoid ugliness --- osu.Game/Overlays/SettingsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 3861c5abc7..748673035b 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -181,7 +181,7 @@ namespace osu.Game.Overlays Scheduler.AddDelayed(loadSections, TRANSITION_LENGTH / 3); Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); searchTextBox.TakeFocus(); searchTextBox.HoldFocus = true; @@ -197,7 +197,7 @@ namespace osu.Game.Overlays ContentContainer.MoveToX(-WIDTH + ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) From 768e10d55f5e3cc144856b47906b03187f162e13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 13:50:47 +0900 Subject: [PATCH 078/539] Adjust `NotificationOverlay` fades to match --- osu.Game/Overlays/NotificationOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index c3ddb228ea..f56d09f2b2 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays protected override void PopIn() { this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); toastTray.FlushAllToasts(); @@ -240,7 +240,7 @@ namespace osu.Game.Overlays markAllRead(); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint); - mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In); } From 8f7e0571f0693cfd8fefcc41f41a70239db1f21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 10:51:05 +0100 Subject: [PATCH 079/539] Add failing test coverage of handling out-of-bounds catch objects --- .../TestSceneOutOfBoundsObjects.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs new file mode 100644 index 0000000000..951f5d1ca1 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneOutOfBoundsObjects.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public partial class TestSceneOutOfBoundsObjects : TestSceneCatchPlayer + { + protected override bool Autoplay => true; + + [Test] + public void TestNoOutOfBoundsObjects() + { + bool anyObjectOutOfBounds = false; + + AddStep("reset flag", () => anyObjectOutOfBounds = false); + + AddUntilStep("check for out-of-bounds objects", + () => + { + anyObjectOutOfBounds |= Player.ChildrenOfType().Any(dho => dho.X < 0 || dho.X > CatchPlayfield.WIDTH); + return Player.ScoreProcessor.HasCompleted.Value; + }); + + AddAssert("no out of bound objects found", () => !anyObjectOutOfBounds); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Ruleset = ruleset, + }, + HitObjects = new List + { + new Fruit { StartTime = 1000, X = -50 }, + new Fruit { StartTime = 1200, X = CatchPlayfield.WIDTH + 50 }, + new JuiceStream + { + StartTime = 1500, + X = 10, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(-200, 0) + }) + }, + new JuiceStream + { + StartTime = 3000, + X = CatchPlayfield.WIDTH - 10, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(200, 0) + }) + }, + } + }; + } +} From 2e8b49b93a7ec12ace3ea9e3f0713f8ba20545e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 10:55:29 +0100 Subject: [PATCH 080/539] Fix catch drawable objects not being clamped to playfield bounds --- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 4a9661f108..ade00918ab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -1,10 +1,12 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.UI; using osuTK; using osuTK.Graphics; @@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private void updateXPosition(ValueChangedEvent _) { - X = OriginalXBindable.Value + XOffsetBindable.Value; + // same as `CatchHitObject.EffectiveX`. + // not using that property directly to support scenarios where `HitObject` may not necessarily be present + // for this pooled drawable. + X = Math.Clamp(OriginalXBindable.Value + XOffsetBindable.Value, 0, CatchPlayfield.WIDTH); } protected override void OnApply() From 1233533fb967390c2c433ea0f7feb0dadd682635 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 22:14:15 +0900 Subject: [PATCH 081/539] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index cf01f2f99b..b179b8b837 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index c7b9d02b26..7e03ab50e2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 14b37db3dd17d3be07a9dce5b088b59205d33815 Mon Sep 17 00:00:00 2001 From: Gabriel Del Nero <43073074+Gabixel@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:35:19 +0100 Subject: [PATCH 082/539] Make `ModDaycore` sequential for `ModHalfTime` --- osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 59a631a7b5..bf58efc339 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Mods.Input { [Key.Q] = new[] { typeof(ModEasy) }, [Key.W] = new[] { typeof(ModNoFail) }, - [Key.E] = new[] { typeof(ModHalfTime) }, + [Key.E] = new[] { typeof(ModHalfTime), typeof(ModDaycore) }, [Key.A] = new[] { typeof(ModHardRock) }, [Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) }, [Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) }, From 13333f75756e587572411971524a65cda09eedab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:02:19 +0100 Subject: [PATCH 083/539] Make room for `OsuIcon` to accept new icons --- osu.Game/Graphics/OsuIcon.cs | 132 ++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 15af8f000b..d1ad818300 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -7,90 +7,94 @@ namespace osu.Game.Graphics { public static class OsuIcon { - public static IconUsage Get(int icon) => new IconUsage((char)icon, "osuFont"); + #region Legacy spritesheet-based icons + + private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont"); // ruleset icons in circles - public static IconUsage RulesetOsu => Get(0xe000); - public static IconUsage RulesetMania => Get(0xe001); - public static IconUsage RulesetCatch => Get(0xe002); - public static IconUsage RulesetTaiko => Get(0xe003); + public static IconUsage RulesetOsu => get(0xe000); + public static IconUsage RulesetMania => get(0xe001); + public static IconUsage RulesetCatch => get(0xe002); + public static IconUsage RulesetTaiko => get(0xe003); // ruleset icons without circles - public static IconUsage FilledCircle => Get(0xe004); - public static IconUsage CrossCircle => Get(0xe005); - public static IconUsage Logo => Get(0xe006); - public static IconUsage ChevronDownCircle => Get(0xe007); - public static IconUsage EditCircle => Get(0xe033); - public static IconUsage LeftCircle => Get(0xe034); - public static IconUsage RightCircle => Get(0xe035); - public static IconUsage Charts => Get(0xe036); - public static IconUsage Solo => Get(0xe037); - public static IconUsage Multi => Get(0xe038); - public static IconUsage Gear => Get(0xe039); + public static IconUsage FilledCircle => get(0xe004); + public static IconUsage CrossCircle => get(0xe005); + public static IconUsage Logo => get(0xe006); + public static IconUsage ChevronDownCircle => get(0xe007); + public static IconUsage EditCircle => get(0xe033); + public static IconUsage LeftCircle => get(0xe034); + public static IconUsage RightCircle => get(0xe035); + public static IconUsage Charts => get(0xe036); + public static IconUsage Solo => get(0xe037); + public static IconUsage Multi => get(0xe038); + public static IconUsage Gear => get(0xe039); // misc icons - public static IconUsage Bat => Get(0xe008); - public static IconUsage Bubble => Get(0xe009); - public static IconUsage BubblePop => Get(0xe02e); - public static IconUsage Dice => Get(0xe011); - public static IconUsage Heart => Get(0xe02f); - public static IconUsage HeartBreak => Get(0xe030); - public static IconUsage Hot => Get(0xe031); - public static IconUsage ListSearch => Get(0xe032); + public static IconUsage Bat => get(0xe008); + public static IconUsage Bubble => get(0xe009); + public static IconUsage BubblePop => get(0xe02e); + public static IconUsage Dice => get(0xe011); + public static IconUsage Heart => get(0xe02f); + public static IconUsage HeartBreak => get(0xe030); + public static IconUsage Hot => get(0xe031); + public static IconUsage ListSearch => get(0xe032); //osu! playstyles - public static IconUsage PlayStyleTablet => Get(0xe02a); - public static IconUsage PlayStyleMouse => Get(0xe029); - public static IconUsage PlayStyleKeyboard => Get(0xe02b); - public static IconUsage PlayStyleTouch => Get(0xe02c); + public static IconUsage PlayStyleTablet => get(0xe02a); + public static IconUsage PlayStyleMouse => get(0xe029); + public static IconUsage PlayStyleKeyboard => get(0xe02b); + public static IconUsage PlayStyleTouch => get(0xe02c); // osu! difficulties - public static IconUsage EasyOsu => Get(0xe015); - public static IconUsage NormalOsu => Get(0xe016); - public static IconUsage HardOsu => Get(0xe017); - public static IconUsage InsaneOsu => Get(0xe018); - public static IconUsage ExpertOsu => Get(0xe019); + public static IconUsage EasyOsu => get(0xe015); + public static IconUsage NormalOsu => get(0xe016); + public static IconUsage HardOsu => get(0xe017); + public static IconUsage InsaneOsu => get(0xe018); + public static IconUsage ExpertOsu => get(0xe019); // taiko difficulties - public static IconUsage EasyTaiko => Get(0xe01a); - public static IconUsage NormalTaiko => Get(0xe01b); - public static IconUsage HardTaiko => Get(0xe01c); - public static IconUsage InsaneTaiko => Get(0xe01d); - public static IconUsage ExpertTaiko => Get(0xe01e); + public static IconUsage EasyTaiko => get(0xe01a); + public static IconUsage NormalTaiko => get(0xe01b); + public static IconUsage HardTaiko => get(0xe01c); + public static IconUsage InsaneTaiko => get(0xe01d); + public static IconUsage ExpertTaiko => get(0xe01e); // fruits difficulties - public static IconUsage EasyFruits => Get(0xe01f); - public static IconUsage NormalFruits => Get(0xe020); - public static IconUsage HardFruits => Get(0xe021); - public static IconUsage InsaneFruits => Get(0xe022); - public static IconUsage ExpertFruits => Get(0xe023); + public static IconUsage EasyFruits => get(0xe01f); + public static IconUsage NormalFruits => get(0xe020); + public static IconUsage HardFruits => get(0xe021); + public static IconUsage InsaneFruits => get(0xe022); + public static IconUsage ExpertFruits => get(0xe023); // mania difficulties - public static IconUsage EasyMania => Get(0xe024); - public static IconUsage NormalMania => Get(0xe025); - public static IconUsage HardMania => Get(0xe026); - public static IconUsage InsaneMania => Get(0xe027); - public static IconUsage ExpertMania => Get(0xe028); + public static IconUsage EasyMania => get(0xe024); + public static IconUsage NormalMania => get(0xe025); + public static IconUsage HardMania => get(0xe026); + public static IconUsage InsaneMania => get(0xe027); + public static IconUsage ExpertMania => get(0xe028); // mod icons - public static IconUsage ModPerfect => Get(0xe049); - public static IconUsage ModAutopilot => Get(0xe03a); - public static IconUsage ModAuto => Get(0xe03b); - public static IconUsage ModCinema => Get(0xe03c); - public static IconUsage ModDoubleTime => Get(0xe03d); - public static IconUsage ModEasy => Get(0xe03e); - public static IconUsage ModFlashlight => Get(0xe03f); - public static IconUsage ModHalftime => Get(0xe040); - public static IconUsage ModHardRock => Get(0xe041); - public static IconUsage ModHidden => Get(0xe042); - public static IconUsage ModNightcore => Get(0xe043); - public static IconUsage ModNoFail => Get(0xe044); - public static IconUsage ModRelax => Get(0xe045); - public static IconUsage ModSpunOut => Get(0xe046); - public static IconUsage ModSuddenDeath => Get(0xe047); - public static IconUsage ModTarget => Get(0xe048); + public static IconUsage ModPerfect => get(0xe049); + public static IconUsage ModAutopilot => get(0xe03a); + public static IconUsage ModAuto => get(0xe03b); + public static IconUsage ModCinema => get(0xe03c); + public static IconUsage ModDoubleTime => get(0xe03d); + public static IconUsage ModEasy => get(0xe03e); + public static IconUsage ModFlashlight => get(0xe03f); + public static IconUsage ModHalftime => get(0xe040); + public static IconUsage ModHardRock => get(0xe041); + public static IconUsage ModHidden => get(0xe042); + public static IconUsage ModNightcore => get(0xe043); + public static IconUsage ModNoFail => get(0xe044); + public static IconUsage ModRelax => get(0xe045); + public static IconUsage ModSpunOut => get(0xe046); + public static IconUsage ModSuddenDeath => get(0xe047); + public static IconUsage ModTarget => get(0xe048); // Use "Icons/BeatmapDetails/mod-icon" instead // public static IconUsage ModBg => Get(0xe04a); + + #endregion } } From c9de6a383d71829eb3ec23c4a7571b72480459c1 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Dec 2023 16:21:54 +0100 Subject: [PATCH 084/539] Add settings checkbox for `MinimiseOnFocusLossInFullscreen` Shown only on desktop platforms in fullscreen. "alt-tab" keyword also matches "alt" and "tab". --- osu.Game/Localisation/GraphicsSettingsStrings.cs | 5 +++++ .../Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 422704514f..fc29352ae2 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -155,6 +155,11 @@ namespace osu.Game.Localisation public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again."); + /// + /// "Minimise on focus loss" + /// + public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise on focus loss"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index a3290bc81c..adb572e245 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -51,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private SettingsDropdown resolutionDropdown = null!; private SettingsDropdown displayDropdown = null!; private SettingsDropdown windowModeDropdown = null!; + private SettingsCheckbox minimiseOnFocusLossCheckbox = null!; private SettingsCheckbox safeAreaConsiderationsCheckbox = null!; private Bindable scalingPositionX = null!; @@ -106,6 +107,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics ItemSource = resolutions, Current = sizeFullscreen }, + minimiseOnFocusLossCheckbox = new SettingsCheckbox + { + LabelText = GraphicsSettingsStrings.MinimiseOnFocusLoss, + Current = config.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen), + Keywords = new[] { "alt-tab" }, + }, safeAreaConsiderationsCheckbox = new SettingsCheckbox { LabelText = "Shrink game to avoid cameras and notches", @@ -255,6 +262,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen; displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1; + minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen; safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero; } From 0fde9cd6ae676c3bef39cd34182fbe0cfe338526 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Dec 2023 16:40:52 +0100 Subject: [PATCH 085/539] Override confine mouse mode only when clicking outside the window would minimise it --- osu.Game/Input/ConfineMouseTracker.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index de8660dbce..926f68df45 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -22,6 +22,7 @@ namespace osu.Game.Input { private Bindable frameworkConfineMode; private Bindable frameworkWindowMode; + private Bindable frameworkMinimiseOnFocusLossInFullscreen; private Bindable osuConfineMode; private IBindable localUserPlaying; @@ -31,7 +32,9 @@ namespace osu.Game.Input { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); + frameworkMinimiseOnFocusLossInFullscreen = frameworkConfigManager.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen); frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); + frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode()); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); @@ -46,7 +49,8 @@ namespace osu.Game.Input if (frameworkConfineMode.Disabled) return; - if (frameworkWindowMode.Value == WindowMode.Fullscreen) + // override confine mode only when clicking outside the window minimises it. + if (frameworkWindowMode.Value == WindowMode.Fullscreen && frameworkMinimiseOnFocusLossInFullscreen.Value) { frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; return; From a6bed04e50b6bcc7845ca85157bb2c6672c991d5 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Dec 2023 16:54:01 +0100 Subject: [PATCH 086/539] Update confine mode settings checkbox to match the new `ConfineMouseTracker` logic Not too happy with the duplicated logic, but settings can't depend on `ConfineMouseTracker` for testability reasons. --- .../Settings/Sections/Input/MouseSettings.cs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 6bf06f4f98..7805ed5834 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable localSensitivity; private Bindable windowMode; + private Bindable minimiseOnFocusLoss; private SettingsEnumDropdown confineMouseModeSetting; private Bindable relativeMode; @@ -47,6 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); windowMode = config.GetBindable(FrameworkSetting.WindowMode); + minimiseOnFocusLoss = config.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen); Children = new Drawable[] { @@ -98,21 +100,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); - windowMode.BindValueChanged(mode => - { - bool isFullscreen = mode.NewValue == WindowMode.Fullscreen; - - if (isFullscreen) - { - confineMouseModeSetting.Current.Disabled = true; - confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen; - } - else - { - confineMouseModeSetting.Current.Disabled = false; - confineMouseModeSetting.TooltipText = string.Empty; - } - }, true); + windowMode.BindValueChanged(_ => updateConfineMouseModeSettingVisibility()); + minimiseOnFocusLoss.BindValueChanged(_ => updateConfineMouseModeSettingVisibility(), true); highPrecisionMouse.Current.BindValueChanged(highPrecision => { @@ -126,6 +115,25 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, true); } + /// + /// Updates disabled state and tooltip of to match when is overriding the confine mode. + /// + private void updateConfineMouseModeSettingVisibility() + { + bool confineModeOverriden = windowMode.Value == WindowMode.Fullscreen && minimiseOnFocusLoss.Value; + + if (confineModeOverriden) + { + confineMouseModeSetting.Current.Disabled = true; + confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen; + } + else + { + confineMouseModeSetting.Current.Disabled = false; + confineMouseModeSetting.TooltipText = string.Empty; + } + } + public partial class SensitivitySetting : SettingsSlider { public SensitivitySetting() From 45143a6c17e4574aab8a910a7be27e568f91c34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:17:39 +0100 Subject: [PATCH 087/539] Implement new icon store --- osu.Game/Graphics/OsuIcon.cs | 348 ++++++++++++++++++++++++++++++++++- osu.Game/OsuGameBase.cs | 1 + 2 files changed, 347 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index d1ad818300..3cd10b1315 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -1,7 +1,16 @@ // 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.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Text; namespace osu.Game.Graphics { @@ -19,7 +28,6 @@ namespace osu.Game.Graphics // ruleset icons without circles public static IconUsage FilledCircle => get(0xe004); - public static IconUsage CrossCircle => get(0xe005); public static IconUsage Logo => get(0xe006); public static IconUsage ChevronDownCircle => get(0xe007); public static IconUsage EditCircle => get(0xe033); @@ -35,7 +43,6 @@ namespace osu.Game.Graphics public static IconUsage Bubble => get(0xe009); public static IconUsage BubblePop => get(0xe02e); public static IconUsage Dice => get(0xe011); - public static IconUsage Heart => get(0xe02f); public static IconUsage HeartBreak => get(0xe030); public static IconUsage Hot => get(0xe031); public static IconUsage ListSearch => get(0xe032); @@ -96,5 +103,342 @@ namespace osu.Game.Graphics // public static IconUsage ModBg => Get(0xe04a); #endregion + + #region New single-file-based icons + + public const string FONT_NAME = @"Icons"; + + public static IconUsage Audio => get(OsuIconMapping.Audio); + public static IconUsage Beatmap => get(OsuIconMapping.Beatmap); + public static IconUsage Calendar => get(OsuIconMapping.Calendar); + public static IconUsage ChangelogA => get(OsuIconMapping.ChangelogA); + public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB); + public static IconUsage Chat => get(OsuIconMapping.Chat); + public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle); + public static IconUsage CollapseA => get(OsuIconMapping.CollapseA); + public static IconUsage Collections => get(OsuIconMapping.Collections); + public static IconUsage Cross => get(OsuIconMapping.Cross); + public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle); + public static IconUsage Crown => get(OsuIconMapping.Crown); + public static IconUsage Debug => get(OsuIconMapping.Debug); + public static IconUsage Delete => get(OsuIconMapping.Delete); + public static IconUsage Details => get(OsuIconMapping.Details); + public static IconUsage Discord => get(OsuIconMapping.Discord); + public static IconUsage EllipsisHorizontal => get(OsuIconMapping.EllipsisHorizontal); + public static IconUsage EllipsisVertical => get(OsuIconMapping.EllipsisVertical); + public static IconUsage ExpandA => get(OsuIconMapping.ExpandA); + public static IconUsage ExpandB => get(OsuIconMapping.ExpandB); + public static IconUsage FeaturedArtist => get(OsuIconMapping.FeaturedArtist); + public static IconUsage FeaturedArtistCircle => get(OsuIconMapping.FeaturedArtistCircle); + public static IconUsage GameplayA => get(OsuIconMapping.GameplayA); + public static IconUsage GameplayB => get(OsuIconMapping.GameplayB); + public static IconUsage GameplayC => get(OsuIconMapping.GameplayC); + public static IconUsage Global => get(OsuIconMapping.Global); + public static IconUsage Graphics => get(OsuIconMapping.Graphics); + public static IconUsage Heart => get(OsuIconMapping.Heart); + public static IconUsage Home => get(OsuIconMapping.Home); + public static IconUsage Input => get(OsuIconMapping.Input); + public static IconUsage Maintenance => get(OsuIconMapping.Maintenance); + public static IconUsage Megaphone => get(OsuIconMapping.Megaphone); + public static IconUsage Music => get(OsuIconMapping.Music); + public static IconUsage News => get(OsuIconMapping.News); + public static IconUsage Next => get(OsuIconMapping.Next); + public static IconUsage NextCircle => get(OsuIconMapping.NextCircle); + public static IconUsage Notification => get(OsuIconMapping.Notification); + public static IconUsage Online => get(OsuIconMapping.Online); + public static IconUsage Play => get(OsuIconMapping.Play); + public static IconUsage Player => get(OsuIconMapping.Player); + public static IconUsage PlayerFollow => get(OsuIconMapping.PlayerFollow); + public static IconUsage Prev => get(OsuIconMapping.Prev); + public static IconUsage PrevCircle => get(OsuIconMapping.PrevCircle); + public static IconUsage Ranking => get(OsuIconMapping.Ranking); + public static IconUsage Rulesets => get(OsuIconMapping.Rulesets); + public static IconUsage Search => get(OsuIconMapping.Search); + public static IconUsage Settings => get(OsuIconMapping.Settings); + public static IconUsage SkinA => get(OsuIconMapping.SkinA); + public static IconUsage SkinB => get(OsuIconMapping.SkinB); + public static IconUsage Star => get(OsuIconMapping.Star); + public static IconUsage Storyboard => get(OsuIconMapping.Storyboard); + public static IconUsage Team => get(OsuIconMapping.Team); + public static IconUsage ThumbsUp => get(OsuIconMapping.ThumbsUp); + public static IconUsage Tournament => get(OsuIconMapping.Tournament); + public static IconUsage Twitter => get(OsuIconMapping.Twitter); + public static IconUsage UserInterface => get(OsuIconMapping.UserInterface); + public static IconUsage Wiki => get(OsuIconMapping.Wiki); + public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint); + public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream); + public static IconUsage EditorDistanceSnap => get(OsuIconMapping.EditorDistanceSnap); + public static IconUsage EditorFinish => get(OsuIconMapping.EditorFinish); + public static IconUsage EditorGridSnap => get(OsuIconMapping.EditorGridSnap); + public static IconUsage EditorNewComboA => get(OsuIconMapping.EditorNewComboA); + public static IconUsage EditorNewComboB => get(OsuIconMapping.EditorNewComboB); + public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect); + public static IconUsage EditorSound => get(OsuIconMapping.EditorSound); + public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle); + + private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME); + + private enum OsuIconMapping + { + [Description(@"audio")] + Audio, + + [Description(@"beatmap")] + Beatmap, + + [Description(@"calendar")] + Calendar, + + [Description(@"changelog-a")] + ChangelogA, + + [Description(@"changelog-b")] + ChangelogB, + + [Description(@"chat")] + Chat, + + [Description(@"check-circle")] + CheckCircle, + + [Description(@"collapse-a")] + CollapseA, + + [Description(@"collections")] + Collections, + + [Description(@"cross")] + Cross, + + [Description(@"cross-circle")] + CrossCircle, + + [Description(@"crown")] + Crown, + + [Description(@"debug")] + Debug, + + [Description(@"delete")] + Delete, + + [Description(@"details")] + Details, + + [Description(@"discord")] + Discord, + + [Description(@"ellipsis-horizontal")] + EllipsisHorizontal, + + [Description(@"ellipsis-vertical")] + EllipsisVertical, + + [Description(@"expand-a")] + ExpandA, + + [Description(@"expand-b")] + ExpandB, + + [Description(@"featured-artist")] + FeaturedArtist, + + [Description(@"featured-artist-circle")] + FeaturedArtistCircle, + + [Description(@"gameplay-a")] + GameplayA, + + [Description(@"gameplay-b")] + GameplayB, + + [Description(@"gameplay-c")] + GameplayC, + + [Description(@"global")] + Global, + + [Description(@"graphics")] + Graphics, + + [Description(@"heart")] + Heart, + + [Description(@"home")] + Home, + + [Description(@"input")] + Input, + + [Description(@"maintenance")] + Maintenance, + + [Description(@"megaphone")] + Megaphone, + + [Description(@"music")] + Music, + + [Description(@"news")] + News, + + [Description(@"next")] + Next, + + [Description(@"next-circle")] + NextCircle, + + [Description(@"notification")] + Notification, + + [Description(@"online")] + Online, + + [Description(@"play")] + Play, + + [Description(@"player")] + Player, + + [Description(@"player-follow")] + PlayerFollow, + + [Description(@"prev")] + Prev, + + [Description(@"prev-circle")] + PrevCircle, + + [Description(@"ranking")] + Ranking, + + [Description(@"rulesets")] + Rulesets, + + [Description(@"search")] + Search, + + [Description(@"settings")] + Settings, + + [Description(@"skin-a")] + SkinA, + + [Description(@"skin-b")] + SkinB, + + [Description(@"star")] + Star, + + [Description(@"storyboard")] + Storyboard, + + [Description(@"team")] + Team, + + [Description(@"thumbs-up")] + ThumbsUp, + + [Description(@"tournament")] + Tournament, + + [Description(@"twitter")] + Twitter, + + [Description(@"user-interface")] + UserInterface, + + [Description(@"wiki")] + Wiki, + + [Description(@"Editor/add-control-point")] + EditorAddControlPoint = 1000, + + [Description(@"Editor/convert-to-stream")] + EditorConvertToStream, + + [Description(@"Editor/distance-snap")] + EditorDistanceSnap, + + [Description(@"Editor/finish")] + EditorFinish, + + [Description(@"Editor/grid-snap")] + EditorGridSnap, + + [Description(@"Editor/new-combo-a")] + EditorNewComboA, + + [Description(@"Editor/new-combo-b")] + EditorNewComboB, + + [Description(@"Editor/select")] + EditorSelect, + + [Description(@"Editor/sound")] + EditorSound, + + [Description(@"Editor/whistle")] + EditorWhistle, + } + + public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore + { + private readonly TextureStore textures; + + public OsuIconStore(TextureStore textures) + { + this.textures = textures; + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + if (fontName == FONT_NAME) + return new Glyph(textures.Get($@"{fontName}/{((OsuIconMapping)character).GetDescription()}")); + + return null; + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + + public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public Texture Get(string name) => throw new NotImplementedException(); + + public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public Task GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public class Glyph : ITexturedCharacterGlyph + { + public float XOffset => default; + public float YOffset => default; + public float XAdvance => default; + public float Baseline => default; + public char Character => default; + + public float GetKerning(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException(); + + public Texture Texture { get; } + public float Width => Texture.Width; + public float Height => Texture.Height; + + public Glyph(Texture texture) + { + Texture = texture; + } + } + + public void Dispose() + { + textures.Dispose(); + } + } + + #endregion } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 48548dc1ef..b4ad21f045 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -479,6 +479,7 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Black"); Fonts.AddStore(new HexaconsIcons.HexaconsStore(Textures)); + Fonts.AddStore(new OsuIcon.OsuIconStore(Textures)); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => From 69baabee627f55d87f6ba61f361e1b370bbfdd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:35:03 +0100 Subject: [PATCH 088/539] Replace hexacons in toolbar with new icons --- osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs | 2 +- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 2 +- osu.Game/Overlays/ChatOverlay.cs | 2 +- osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs | 2 +- osu.Game/Overlays/News/NewsHeader.cs | 2 +- osu.Game/Overlays/NotificationOverlay.cs | 2 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs | 2 +- osu.Game/Overlays/SettingsOverlay.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs | 2 +- osu.Game/Overlays/Wiki/WikiHeader.cs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 27fab82bf3..075dfd02b0 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing { Title = PageTitleStrings.MainBeatmapsetsControllerIndex; Description = NamedOverlayComponentStrings.BeatmapListingDescription; - Icon = HexaconsIcons.Beatmap; + Icon = OsuIcon.Beatmap; } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 61ea9dc4db..f738d70370 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Changelog { Title = PageTitleStrings.MainChangelogControllerDefault; Description = NamedOverlayComponentStrings.ChangelogDescription; - Icon = HexaconsIcons.Devtools; + Icon = OsuIcon.ChangelogB; } } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 4aa4c59471..8f3b7031c2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays { public partial class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler { - public IconUsage Icon => HexaconsIcons.Messaging; + public IconUsage Icon => OsuIcon.Chat; public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Description => ChatStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 104f0943dc..8fd8f6b332 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard { Title = PageTitleStrings.MainHomeControllerIndex; Description = NamedOverlayComponentStrings.DashboardDescription; - Icon = HexaconsIcons.Social; + Icon = OsuIcon.Global; } } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index f237ed66f2..92d71a21ef 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays.News { Title = PageTitleStrings.MainNewsControllerDefault; Description = NamedOverlayComponentStrings.NewsDescription; - Icon = HexaconsIcons.News; + Icon = OsuIcon.News; } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f56d09f2b2..18a487a312 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, INotificationOverlay { - public IconUsage Icon => HexaconsIcons.Notification; + public IconUsage Icon => OsuIcon.Notification; public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Description => NotificationsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 425ff0935d..7ec52364d4 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public IconUsage Icon => HexaconsIcons.Music; + public IconUsage Icon => OsuIcon.Music; public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 63128fb73d..a23ec18afe 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Rankings { Title = PageTitleStrings.MainRankingControllerDefault; Description = NamedOverlayComponentStrings.RankingsDescription; - Icon = HexaconsIcons.Rankings; + Icon = OsuIcon.Ranking; } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 5735b1515a..9efd848035 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays { public partial class SettingsOverlay : SettingsPanel, INamedOverlayComponent { - public IconUsage Icon => HexaconsIcons.Settings; + public IconUsage Icon => OsuIcon.Settings; public LocalisableString Title => SettingsStrings.HeaderTitle; public LocalisableString Description => SettingsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index ded0229d67..70675c1b92 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = ToolbarStrings.HomeHeaderTitle; TooltipSub = ToolbarStrings.HomeHeaderDescription; - SetIcon(HexaconsIcons.Home); + SetIcon(OsuIcon.Home); } } } diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 9e9e565684..24eddeb0c2 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -82,7 +82,7 @@ namespace osu.Game.Overlays.Wiki { Title = PageTitleStrings.MainWikiControllerDefault; Description = NamedOverlayComponentStrings.WikiDescription; - Icon = HexaconsIcons.Wiki; + Icon = OsuIcon.Wiki; } } } From 28d9145a4c182ecb66029e0b1e52f24f7cc16acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:47:29 +0100 Subject: [PATCH 089/539] Add more spacing to toolbar icons --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 81d2de5acb..a547fda814 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -113,7 +113,7 @@ namespace osu.Game.Overlays.Toolbar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(26), + Size = new Vector2(20), Alpha = 0, }, DrawableText = new OsuSpriteText From c45477bd1fff8447755f7afd54bf5156e1352833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:40:48 +0100 Subject: [PATCH 090/539] Use new icons in main menu wherever feasible --- osu.Game/Screens/Menu/ButtonSystem.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index ca5bef985e..b2b3fbd626 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -13,7 +13,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -103,8 +102,8 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { - new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, + new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { VisibleStateMin = ButtonSystemState.Play, @@ -128,18 +127,18 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", HexaconsIcons.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); - buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", HexaconsIcons.Editor, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); From 2857322a8bc3257ae26ea35567b8f1741b7a45ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:53:01 +0100 Subject: [PATCH 091/539] Use new icons in settings --- osu.Game/Overlays/Settings/Sections/AudioSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/DebugSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/GameplaySection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 2 +- osu.Game/Overlays/Settings/Sections/GraphicsSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/InputSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/OnlineSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/RulesetSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 ++- osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs | 3 ++- 11 files changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index fb3d486776..1ab0d6c886 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Localisation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Audio; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.VolumeUp + Icon = OsuIcon.Audio }; public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "sound" }); diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 33a6f4c673..b84c441057 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -5,6 +5,7 @@ using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.DebugSettings; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Bug + Icon = OsuIcon.Debug }; public DebugSection() diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index b60689b611..463b3d1d09 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Gameplay; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Regular.DotCircle + Icon = OsuIcon.GameplayC }; public GameplaySection() diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index 2b043d40bc..2aa1008b1d 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Cog + Icon = OsuIcon.Settings }; [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 98f6908512..e1fa1eef9c 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Graphics; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Laptop + Icon = OsuIcon.Graphics }; public GraphicsSection() diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index a8f19cc91d..0204aa5e64 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Handlers; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Input; @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Keyboard + Icon = OsuIcon.Input }; public InputSection(KeyBindingPanel keyConfig) diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index bb0a952164..bd90e4c35d 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Maintenance; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Wrench + Icon = OsuIcon.Maintenance }; public MaintenanceSection() diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index c8faa3b697..1484f2c756 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Online; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.GlobeAsia + Icon = OsuIcon.Online }; public OnlineSection() diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs index aaad1ec4e2..626264151f 100644 --- a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs +++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Rulesets; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.Chess + Icon = OsuIcon.Rulesets }; [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1d057f42c0..9b04f208a7 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays.SkinEditor; @@ -31,7 +32,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.PaintBrush + Icon = OsuIcon.SkinB }; private static readonly Live random_skin_info = new SkinInfo diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs index 2ec9e32ea9..953ede25e1 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.UserInterface; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Solid.LayerGroup + Icon = OsuIcon.UserInterface }; public UserInterfaceSection() From 288ac930e44bd8dc5b5ab8d5f5f342228625fc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 16:03:57 +0100 Subject: [PATCH 092/539] Use new icons in editor Some that exist on figma are purposefully not used due to an editorial request from @peppy. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 3 ++- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 3 ++- osu.Game/Rulesets/Edit/Tools/SelectTool.cs | 3 ++- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8382317d70..448cfaf84c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit .Concat(DistanceSnapProvider.CreateTernaryButtons()) .Concat(new[] { - new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) + new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }) }); private BindableList selectedHitObjects; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index ddf539771d..b3ca59a5b0 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -169,7 +170,7 @@ namespace osu.Game.Rulesets.Edit public IEnumerable CreateTernaryButtons() => new[] { - new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) + new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) }; protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs index 9640830a09..a272e9f480 100644 --- a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Edit.Tools { @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Edit.Tools { } - public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.MousePointer }; + public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect }; public override PlacementBlueprint CreatePlacementBlueprint() => null; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c7c7c4aa83..4fba798a26 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -225,7 +225,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); + yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }); foreach (var kvp in SelectionHandler.SelectionSampleStates) yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); @@ -272,10 +272,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return new SpriteIcon { Icon = FontAwesome.Solid.Hands }; case HitSampleInfo.HIT_WHISTLE: - return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn }; + return new SpriteIcon { Icon = OsuIcon.EditorWhistle }; case HitSampleInfo.HIT_FINISH: - return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan }; + return new SpriteIcon { Icon = OsuIcon.EditorFinish }; } return null; From 53766285ce6cd5b227e933fe45da8f32bc2f6cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:55:19 +0100 Subject: [PATCH 093/539] Remove remaining hexacons usages --- osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs | 2 +- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 +- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 55a04b129c..5db7223bdf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface public TestTitle() { Title = "title"; - Icon = HexaconsIcons.Devtools; + Icon = OsuIcon.ChangelogB; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index eced27f35e..1df246ae77 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = PageTitleStrings.MainBeatmapsetsControllerShow; - Icon = HexaconsIcons.Beatmap; + Icon = OsuIcon.Beatmap; } } } diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 4fc9fbb6d5..3ecdb09976 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = HexaconsIcons.Messaging, + Icon = OsuIcon.Chat, Size = new Vector2(24), }, // Placeholder text diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 78343d08f1..42bec50022 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = PageTitleStrings.MainUsersControllerDefault; - Icon = HexaconsIcons.Profile; + Icon = OsuIcon.Player; } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 5c77672d90..0e125d0ec0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Components.Menus Size = new Vector2(26), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Icon = HexaconsIcons.Editor, + Icon = OsuIcon.EditCircle, }, text = new TextFlowContainer { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 93448c4394..022da36abc 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Setup { Title = EditorSetupStrings.BeatmapSetup.ToLower(); Description = EditorSetupStrings.BeatmapSetupDescription; - Icon = HexaconsIcons.Social; + Icon = OsuIcon.Beatmap; } } From 89e2b6358a2937ee46246f3b05b4122c7528ae95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 15:55:37 +0100 Subject: [PATCH 094/539] Remove hexacons --- osu.Game/Graphics/HexaconsIcons.cs | 131 ----------------------------- osu.Game/OsuGameBase.cs | 1 - 2 files changed, 132 deletions(-) delete mode 100644 osu.Game/Graphics/HexaconsIcons.cs diff --git a/osu.Game/Graphics/HexaconsIcons.cs b/osu.Game/Graphics/HexaconsIcons.cs deleted file mode 100644 index 3eee5d7197..0000000000 --- a/osu.Game/Graphics/HexaconsIcons.cs +++ /dev/null @@ -1,131 +0,0 @@ -// 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.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Text; - -namespace osu.Game.Graphics -{ - public static class HexaconsIcons - { - public const string FONT_NAME = "Icons/Hexacons"; - - public static IconUsage BeatmapPacks => get(HexaconsMapping.beatmap_packs); - public static IconUsage Beatmap => get(HexaconsMapping.beatmap); - public static IconUsage Calendar => get(HexaconsMapping.calendar); - public static IconUsage Chart => get(HexaconsMapping.chart); - public static IconUsage Community => get(HexaconsMapping.community); - public static IconUsage Contests => get(HexaconsMapping.contests); - public static IconUsage Devtools => get(HexaconsMapping.devtools); - public static IconUsage Download => get(HexaconsMapping.download); - public static IconUsage Editor => get(HexaconsMapping.editor); - public static IconUsage FeaturedArtist => get(HexaconsMapping.featured_artist); - public static IconUsage Home => get(HexaconsMapping.home); - public static IconUsage Messaging => get(HexaconsMapping.messaging); - public static IconUsage Music => get(HexaconsMapping.music); - public static IconUsage News => get(HexaconsMapping.news); - public static IconUsage Notification => get(HexaconsMapping.notification); - public static IconUsage Profile => get(HexaconsMapping.profile); - public static IconUsage Rankings => get(HexaconsMapping.rankings); - public static IconUsage Search => get(HexaconsMapping.search); - public static IconUsage Settings => get(HexaconsMapping.settings); - public static IconUsage Social => get(HexaconsMapping.social); - public static IconUsage Store => get(HexaconsMapping.store); - public static IconUsage Tournament => get(HexaconsMapping.tournament); - public static IconUsage Wiki => get(HexaconsMapping.wiki); - - private static IconUsage get(HexaconsMapping icon) => new IconUsage((char)icon, FONT_NAME); - - // Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility. - // Names should match filenames in resources. - private enum HexaconsMapping - { - beatmap_packs, - beatmap, - calendar, - chart, - community, - contests, - devtools, - download, - editor, - featured_artist, - home, - messaging, - music, - news, - notification, - profile, - rankings, - search, - settings, - social, - store, - tournament, - wiki, - } - - public class HexaconsStore : ITextureStore, ITexturedGlyphLookupStore - { - private readonly TextureStore textures; - - public HexaconsStore(TextureStore textures) - { - this.textures = textures; - } - - public void Dispose() - { - textures.Dispose(); - } - - public ITexturedCharacterGlyph? Get(string? fontName, char character) - { - if (fontName == FONT_NAME) - return new Glyph(textures.Get($"{fontName}/{((HexaconsMapping)character).ToString().Replace("_", "-")}")); - - return null; - } - - public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); - - public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; - - public Texture Get(string name) => throw new NotImplementedException(); - - public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public Stream GetStream(string name) => throw new NotImplementedException(); - - public IEnumerable GetAvailableResources() => throw new NotImplementedException(); - - public Task GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - - public class Glyph : ITexturedCharacterGlyph - { - public float XOffset => default; - public float YOffset => default; - public float XAdvance => default; - public float Baseline => default; - public char Character => default; - - public float GetKerning(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException(); - - public Texture Texture { get; } - public float Width => Texture.Width; - public float Height => Texture.Height; - - public Glyph(Texture texture) - { - Texture = texture; - } - } - } - } -} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b4ad21f045..5b17dc13c2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -478,7 +478,6 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Bold"); AddFont(Resources, @"Fonts/Venera/Venera-Black"); - Fonts.AddStore(new HexaconsIcons.HexaconsStore(Textures)); Fonts.AddStore(new OsuIcon.OsuIconStore(Textures)); } From 655528a5370fe83b05452e220d22103824bfecc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 03:04:13 +0900 Subject: [PATCH 095/539] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index dd2393ff21..c7e0cc3808 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 5f7f1f771d2404e05a7d152d73e893e3d1c54cc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 03:09:20 +0900 Subject: [PATCH 096/539] Reword tooltip text for dashboard --- osu.Game/Localisation/NamedOverlayComponentStrings.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/NamedOverlayComponentStrings.cs b/osu.Game/Localisation/NamedOverlayComponentStrings.cs index 475bea2a4a..72e63d699a 100644 --- a/osu.Game/Localisation/NamedOverlayComponentStrings.cs +++ b/osu.Game/Localisation/NamedOverlayComponentStrings.cs @@ -20,12 +20,12 @@ namespace osu.Game.Localisation public static LocalisableString ChangelogDescription => new TranslatableString(getKey(@"changelog_description"), @"track recent dev updates in the osu! ecosystem"); /// - /// "view your friends and other information" + /// "view your friends and spectate other players" /// - public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and other information"); + public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and spectate other players"); /// - /// "find out who's the best right now" + /// "find out who's the best right now" /// public static LocalisableString RankingsDescription => new TranslatableString(getKey(@"rankings_description"), @"find out who's the best right now"); @@ -39,6 +39,6 @@ namespace osu.Game.Localisation /// public static LocalisableString WikiDescription => new TranslatableString(getKey(@"wiki_description"), @"knowledge base"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 1f55ef211eda02b867327746313a32d599645d17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 03:11:27 +0900 Subject: [PATCH 097/539] Rearrange buttons --- osu.Game/Overlays/Toolbar/Toolbar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 93294a9d30..ec1238ad1f 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -164,11 +164,11 @@ namespace osu.Game.Overlays.Toolbar { new ToolbarNewsButton(), new ToolbarChangelogButton(), + new ToolbarWikiButton(), new ToolbarRankingsButton(), new ToolbarBeatmapListingButton(), new ToolbarChatButton(), new ToolbarSocialButton(), - new ToolbarWikiButton(), new ToolbarMusicButton(), //new ToolbarButton //{ From d4423d493364065baa7984acda63884e26e8a98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 18:38:29 +0100 Subject: [PATCH 098/539] Store last set score to a `SessionStatic` --- osu.Game/Configuration/SessionStatics.cs | 7 +++++++ osu.Game/Scoring/ScoreInfo.cs | 1 + osu.Game/Screens/Play/SubmittingPlayer.cs | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 8f0a60b23d..1548b781a7 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -9,6 +9,7 @@ using osu.Game.Input; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Scoring; namespace osu.Game.Configuration { @@ -27,6 +28,7 @@ namespace osu.Game.Configuration SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); + SetDefault(Static.LastLocalUserScore, null); } /// @@ -73,5 +75,10 @@ namespace osu.Game.Configuration /// Used in touchscreen detection scenarios (). /// TouchInputActive, + + /// + /// Stores the local user's last score (can be completed or aborted). + /// + LastLocalUserScore, } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7071bd380e..44795c6fa7 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -207,6 +207,7 @@ namespace osu.Game.Scoring clone.Statistics = new Dictionary(clone.Statistics); clone.MaximumStatistics = new Dictionary(clone.MaximumStatistics); + clone.HitEvents = new List(clone.HitEvents); // Ensure we have fresh mods to avoid any references (ie. after gameplay). clone.clearAllMods(); diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index f88526b8f9..ff2c57bf35 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -37,6 +38,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } + [Resolved] + private SessionStatics statics { get; set; } + private TaskCompletionSource scoreSubmissionSource; protected SubmittingPlayer(PlayerConfiguration configuration = null) @@ -176,6 +180,7 @@ namespace osu.Game.Screens.Play { bool exiting = base.OnExiting(e); submitFromFailOrQuit(); + statics.SetValue(Static.LastLocalUserScore, Score.ScoreInfo.DeepClone()); return exiting; } From 1b7af989ec68e06b10c1a014e37c4a56f47cd1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 19:14:17 +0100 Subject: [PATCH 099/539] Migrate `BeatmapOffsetControl` to use session static directly --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 28 ++++++++++++++++--- osu.Game/Screens/Play/PlayerLoader.cs | 4 --- .../Play/PlayerSettings/AudioSettings.cs | 7 +++-- .../PlayerSettings/BeatmapOffsetControl.cs | 4 +++ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index f3701b664c..83fc5c2013 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Ranking; namespace osu.Game.Tests.Visual.Gameplay @@ -44,7 +45,23 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }; + }); + + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + } + + [Test] + public void TestScoreFromDifferentBeatmap() + { + AddStep("Set short reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10), + BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First(), }; }); @@ -59,7 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay offsetControl.ReferenceScore.Value = new ScoreInfo { HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10), - Mods = new Mod[] { new OsuModRelax() } + Mods = new Mod[] { new OsuModRelax() }, + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); @@ -77,7 +95,8 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); @@ -105,7 +124,8 @@ namespace osu.Game.Tests.Visual.Gameplay { offsetControl.ReferenceScore.Value = new ScoreInfo { - HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, }; }); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 681189d184..232de53ac3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -263,10 +263,6 @@ namespace osu.Game.Screens.Play Debug.Assert(CurrentPlayer != null); - var lastScore = CurrentPlayer.Score; - - AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo; - // prepare for a retry. CurrentPlayer = null; playerConsumed = false; diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs index 010d8115fa..3c79721590 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class AudioSettings : PlayerSettingsGroup { - public Bindable ReferenceScore { get; } = new Bindable(); + private Bindable referenceScore { get; } = new Bindable(); private readonly PlayerCheckbox beatmapHitsoundsToggle; @@ -26,15 +26,16 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapHitsounds }, new BeatmapOffsetControl { - ReferenceScore = { BindTarget = ReferenceScore }, + ReferenceScore = { BindTarget = referenceScore }, }, }; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, SessionStatics statics) { beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); + statics.BindWith(Static.LastLocalUserScore, referenceScore); } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index b0e7d08699..3f0f0fd1df 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -174,6 +175,9 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; + if (!score.NewValue.BeatmapInfo.AsNonNull().Equals(beatmap.Value.BeatmapInfo)) + return; + if (score.NewValue.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) return; From 70aa067eb166477f29b31495da6b1a8121a0bf8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 03:23:04 +0900 Subject: [PATCH 100/539] Adjust gradient visibility and transition --- osu.Game/Overlays/Toolbar/Toolbar.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index ec1238ad1f..52fad2ba3b 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -224,9 +224,9 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.X, Anchor = Anchor.BottomLeft, Alpha = 0, - Height = 100, + Height = 80, Colour = ColourInfo.GradientVertical( - OsuColour.Gray(0).Opacity(0.9f), OsuColour.Gray(0).Opacity(0)), + OsuColour.Gray(0f).Opacity(0.7f), OsuColour.Gray(0).Opacity(0)), }, }; } @@ -241,9 +241,9 @@ namespace osu.Game.Overlays.Toolbar private void updateState() { if (ShowGradient.Value) - gradientBackground.FadeIn(transition_time, Easing.OutQuint); + gradientBackground.FadeIn(2500, Easing.OutQuint); else - gradientBackground.FadeOut(transition_time, Easing.OutQuint); + gradientBackground.FadeOut(200, Easing.OutQuint); } } From 92c4c20a51e544a27b745932334e6442d5d5d994 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 03:43:38 +0900 Subject: [PATCH 101/539] Adjust paddings and fills of toolbar buttons --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 45 ++++++++++--------- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 34 +++++++++----- .../Toolbar/ToolbarOverlayToggleButton.cs | 2 +- .../Toolbar/ToolbarRulesetSelector.cs | 1 + .../Overlays/Toolbar/ToolbarUserButton.cs | 2 +- 5 files changed, 51 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index a547fda814..bd5faf1588 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -7,7 +7,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; @@ -74,6 +73,8 @@ namespace osu.Game.Overlays.Toolbar private readonly SpriteText keyBindingTooltip; protected FillFlowContainer Flow; + protected readonly Container BackgroundContent; + [Resolved] private RealmAccess realm { get; set; } = null!; @@ -82,21 +83,33 @@ namespace osu.Game.Overlays.Toolbar Width = Toolbar.HEIGHT; RelativeSizeAxes = Axes.Y; + Padding = new MarginPadding(3); + Children = new Drawable[] { - HoverBackground = new Box + BackgroundContent = new Container { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + HoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, Flow = new FillFlowContainer { @@ -219,14 +232,6 @@ namespace osu.Game.Overlays.Toolbar public OpaqueBackground() { RelativeSizeAxes = Axes.Both; - Masking = true; - MaskingSmoothness = 0; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }; Children = new Drawable[] { diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index f1310d8535..67688155ae 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -42,21 +42,33 @@ namespace osu.Game.Overlays.Toolbar clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); prefer24HourTime = config.GetBindable(OsuSetting.Prefer24HourTime); + Padding = new MarginPadding(3); + Children = new Drawable[] { - hoverBackground = new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, new FillFlowContainer { diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 78c976111b..37038161e1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarOverlayToggleButton() { - Add(stateBackground = new Box + BackgroundContent.Add(stateBackground = new Box { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(150).Opacity(180), diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 715076b368..63d11c1b9e 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Toolbar new OpaqueBackground { Depth = 1, + Masking = true, }, ModeButtonLine = new Container { diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 028decea1e..7d1b6c7404 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login) { - Add(new OpaqueBackground { Depth = 1 }); + BackgroundContent.Add(new OpaqueBackground { Depth = 1 }); Flow.Add(new Container { From cf5e3e886386737385fe75cf1642e75279c7ff16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 04:06:29 +0900 Subject: [PATCH 102/539] Breathe some colour and life into the toolbar --- .../Toolbar/ToolbarOverlayToggleButton.cs | 8 ++-- .../Toolbar/ToolbarRulesetSelector.cs | 43 +++++++++---------- .../Toolbar/ToolbarRulesetTabButton.cs | 30 +++++++------ 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 37038161e1..09b8df14a6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class ToolbarOverlayToggleButton : ToolbarButton { - private readonly Box stateBackground; + private Box stateBackground; private OverlayContainer stateContainer; @@ -44,12 +45,13 @@ namespace osu.Game.Overlays.Toolbar } } - public ToolbarOverlayToggleButton() + [BackgroundDependencyLoader] + private void load(OsuColour colours) { BackgroundContent.Add(stateBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(150).Opacity(180), + Colour = colours.Carmine.Opacity(180), Blending = BlendingParameters.Additive, Depth = 2, Alpha = 0, diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 63d11c1b9e..723c24597a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -4,20 +4,19 @@ #nullable disable using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osuTK.Input; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays.Toolbar { @@ -47,20 +46,18 @@ namespace osu.Game.Overlays.Toolbar { Size = new Vector2(Toolbar.HEIGHT, 3), Anchor = Anchor.BottomLeft, - Origin = Anchor.TopLeft, - Masking = true, - EdgeEffect = new EdgeEffectParameters + Origin = Anchor.BottomLeft, + Y = -1, + Children = new Drawable[] { - Type = EdgeEffectType.Glow, - Colour = new Color4(255, 194, 224, 100), - Radius = 15, - Roundness = 15, - }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(18, 3), + } } - } + }, }); foreach (var ruleset in Rulesets.AvailableRulesets) @@ -90,7 +87,7 @@ namespace osu.Game.Overlays.Toolbar { if (SelectedTab != null) { - ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); + ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter); if (hasInitialPosition) selectionSamples[SelectedTab.Value.ShortName]?.Play(); diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 74f76c7c89..5500f1c879 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -1,14 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets; -using osuTK.Graphics; namespace osu.Game.Overlays.Toolbar { @@ -41,27 +43,31 @@ namespace osu.Game.Overlays.Toolbar { protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); + [Resolved] + private OsuColour colours { get; set; } = null!; + + public RulesetButton() + { + Padding = new MarginPadding(3) + { + Bottom = 5 + }; + } + public bool Active { - set + set => Scheduler.AddOnce(() => { if (value) { - IconContainer.Colour = Color4.White; - IconContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(255, 194, 224, 100), - Radius = 15, - Roundness = 15, - }; + IconContainer.Colour = Color4Extensions.FromHex("#00FFAA"); } else { - IconContainer.Colour = new Color4(255, 194, 224, 255); + IconContainer.Colour = colours.GrayF; IconContainer.EdgeEffect = new EdgeEffectParameters(); } - } + }); } protected override bool OnClick(ClickEvent e) From f51b5f5487e770b783cc12513865eb1d5a3462d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 19:43:14 +0100 Subject: [PATCH 103/539] Add components to track average hit errors across session --- osu.Game/OsuGameBase.cs | 2 + .../Audio/AudioOffsetAdjustControl.cs | 23 ++++++++ .../Audio/SessionAverageHitErrorTracker.cs | 55 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 48548dc1ef..64f15efb15 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -55,6 +55,7 @@ using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; +using osu.Game.Overlays.Settings.Sections.Audio; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Resources; using osu.Game.Rulesets; @@ -349,6 +350,7 @@ namespace osu.Game dependencies.CacheAs(powerStatus); dependencies.Cache(SessionStatics = new SessionStatics()); + dependencies.Cache(new SessionAverageHitErrorTracker()); dependencies.Cache(Colours = new OsuColour()); RegisterImportHandler(BeatmapManager); diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs new file mode 100644 index 0000000000..045cd24fc0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays.Settings.Sections.Audio +{ + public partial class AudioOffsetAdjustControl : SettingsItem + { + [BackgroundDependencyLoader] + private void load() + { + } + + protected override Drawable CreateControl() => new AudioOffsetPreview(); + + private partial class AudioOffsetPreview : CompositeDrawable + { + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs b/osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs new file mode 100644 index 0000000000..b714d49b89 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Overlays.Settings.Sections.Audio +{ + /// + /// Tracks the local user's average hit error during the ongoing play session. + /// + [Cached] + public partial class SessionAverageHitErrorTracker : Component + { + public IBindableList AverageHitErrorHistory => averageHitErrorHistory; + private readonly BindableList averageHitErrorHistory = new BindableList(); + + private readonly Bindable latestScore = new Bindable(); + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + statics.BindWith(Static.LastLocalUserScore, latestScore); + latestScore.BindValueChanged(score => calculateAverageHitError(score.NewValue), true); + } + + private void calculateAverageHitError(ScoreInfo? newScore) + { + if (newScore == null) + return; + + if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) + return; + + if (newScore.HitEvents.Count < 10) + return; + + if (newScore.HitEvents.CalculateAverageHitError() is not double averageError) + return; + + // keep a sane maximum number of entries. + if (averageHitErrorHistory.Count >= 50) + averageHitErrorHistory.RemoveAt(0); + averageHitErrorHistory.Add(averageError); + } + + public void ClearHistory() => averageHitErrorHistory.Clear(); + } +} From 160342ceed4824a3214157c049ff981e2710568f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 21:14:37 +0100 Subject: [PATCH 104/539] Implement automatic suggestion of audio offset based on last plays --- .../TestSceneAudioOffsetAdjustControl.cs | 63 ++++++++ osu.Game/OsuGameBase.cs | 5 +- .../Audio/AudioOffsetAdjustControl.cs | 138 +++++++++++++++++- .../Settings/Sections/Audio/OffsetSettings.cs | 5 +- 4 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs new file mode 100644 index 0000000000..efb65bb0a8 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings.Sections.Audio; +using osu.Game.Scoring; +using osu.Game.Tests.Visual.Ranking; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneAudioOffsetAdjustControl : OsuTestScene + { + [Resolved] + private SessionStatics statics { get; set; } = null!; + + [Cached] + private SessionAverageHitErrorTracker tracker = new SessionAverageHitErrorTracker(); + + private Container content = null!; + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + tracker, + content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + AutoSizeAxes = Axes.Y + } + }); + } + + [Test] + public void TestBehaviour() + { + AddStep("create control", () => Child = new AudioOffsetAdjustControl + { + Current = new BindableDouble + { + MinValue = -500, + MaxValue = 500 + } + }); + AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + })); + AddStep("clear history", () => tracker.ClearHistory()); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 64f15efb15..0d8a2fbb97 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -201,6 +201,8 @@ namespace osu.Game private RulesetConfigCache rulesetConfigCache; + private SessionAverageHitErrorTracker hitErrorTracker; + protected SpectatorClient SpectatorClient { get; private set; } protected MultiplayerClient MultiplayerClient { get; private set; } @@ -350,7 +352,7 @@ namespace osu.Game dependencies.CacheAs(powerStatus); dependencies.Cache(SessionStatics = new SessionStatics()); - dependencies.Cache(new SessionAverageHitErrorTracker()); + dependencies.Cache(hitErrorTracker = new SessionAverageHitErrorTracker()); dependencies.Cache(Colours = new OsuColour()); RegisterImportHandler(BeatmapManager); @@ -410,6 +412,7 @@ namespace osu.Game }); base.Content.Add(new TouchInputInterceptor()); + base.Content.Add(hitErrorTracker); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 045cd24fc0..0023c60c61 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -1,9 +1,23 @@ // 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.Collections.Specialized; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays.Settings.Sections.Audio { @@ -12,12 +26,134 @@ namespace osu.Game.Overlays.Settings.Sections.Audio [BackgroundDependencyLoader] private void load() { + LabelText = AudioSettingsStrings.AudioOffset; } protected override Drawable CreateControl() => new AudioOffsetPreview(); - private partial class AudioOffsetPreview : CompositeDrawable + private partial class AudioOffsetPreview : CompositeDrawable, IHasCurrentValue { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + private readonly IBindableList averageHitErrorHistory = new BindableList(); + + private readonly Bindable suggestedOffset = new Bindable(); + + private Container notchContainer = null!; + private TextFlowContainer hintText = null!; + private RoundedButton applySuggestion = null!; + + [BackgroundDependencyLoader] + private void load(SessionAverageHitErrorTracker hitErrorTracker) + { + averageHitErrorHistory.BindTo(hitErrorTracker.AverageHitErrorHistory); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TimeSlider + { + RelativeSizeAxes = Axes.X, + Current = { BindTarget = Current }, + KeyboardStep = 1, + }, + notchContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 10, + Padding = new MarginPadding { Horizontal = Nub.DEFAULT_EXPANDED_SIZE / 2 }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + hintText = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 16)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + applySuggestion = new RoundedButton + { + RelativeSizeAxes = Axes.X, + Text = "Apply suggested offset", + Action = () => + { + if (suggestedOffset.Value.HasValue) + current.Value = suggestedOffset.Value.Value; + hitErrorTracker.ClearHistory(); + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); + suggestedOffset.BindValueChanged(_ => updateHintText(), true); + } + + private void updateDisplay(object? _, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (double average in e.NewItems!) + { + notchContainer.ForEach(n => n.Alpha *= 0.95f); + notchContainer.Add(new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + RelativePositionAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = getXPositionForAverage(average) + }); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (double average in e.OldItems!) + { + var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForAverage(average)); + Debug.Assert(notch != null); + notchContainer.Remove(notch, true); + } + + break; + + case NotifyCollectionChangedAction.Reset: + notchContainer.Clear(); + break; + } + + suggestedOffset.Value = averageHitErrorHistory.Count < 3 ? null : -averageHitErrorHistory.Average(); + } + + private float getXPositionForAverage(double average) => (float)(Math.Clamp(-average, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); + + private void updateHintText() + { + hintText.Text = suggestedOffset.Value == null + ? @"Play a few beatmaps to receive a suggested offset!" + : $@"Based on the last {averageHitErrorHistory.Count} plays, the suggested offset is {suggestedOffset.Value:N0} ms."; + applySuggestion.Enabled.Value = suggestedOffset.Value != null; + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 6b5c769853..af15d310fc 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -23,11 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Children = new Drawable[] { - new SettingsSlider + new AudioOffsetAdjustControl { - LabelText = AudioSettingsStrings.AudioOffset, Current = config.GetBindable(OsuSetting.AudioOffset), - KeyboardStep = 1f }, new SettingsButton { From cf39bb7a18877055a58324d542b9c4feb83d1611 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:55:20 -0800 Subject: [PATCH 105/539] Fix spinner max bonus not respecting ISamplePlaybackDisabler The spinner max bonus was loaded through SkinnableSound instead of PausableSkinnableSound, leading to it not respecting the case where sample playback is globally disabled through ISamplePlaybackDisabler, and can be easily heard in situations like during the catchup period after seeking using the ArgonSongProgressBar with song volume at 0 --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f7c1437009..bf4b07eaab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - private SkinnableSound maxBonusSample; + private PausableSkinnableSound maxBonusSample; /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Looping = true, Frequency = { Value = spinning_sample_initial_frequency } }, - maxBonusSample = new SkinnableSound + maxBonusSample = new PausableSkinnableSound { MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, } From d9299a8a55bff6757358db2d46b38806817f974a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 23:07:17 +0100 Subject: [PATCH 106/539] Implement visual appearance of "system title" message in main menu --- .../Visual/Menus/TestSceneMainMenu.cs | 30 ++++++ .../API/Requests/Responses/APISystemTitle.cs | 16 +++ osu.Game/OsuGame.cs | 5 +- osu.Game/Screens/Menu/MainMenu.cs | 15 +++ osu.Game/Screens/Menu/SystemTitle.cs | 101 ++++++++++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APISystemTitle.cs create mode 100644 osu.Game/Screens/Menu/SystemTitle.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs new file mode 100644 index 0000000000..85aa14f33e --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneMainMenu : OsuGameTestScene + { + [Test] + public void TestSystemTitle() + { + AddStep("set system title", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }); + AddStep("set another title", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + }); + AddStep("unset system title", () => Game.ChildrenOfType().Single().Current.Value = null); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs new file mode 100644 index 0000000000..3a2342cc4c --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APISystemTitle + { + [JsonProperty(@"image")] + public string Image { get; set; } = string.Empty; + + [JsonProperty(@"url")] + public string Url { get; set; } = string.Empty; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e7ff99ef01..37996e8832 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -995,7 +995,10 @@ namespace osu.Game }, topMostOverlayContent.Add); if (!args?.Any(a => a == @"--no-version-overlay") ?? true) - loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); + { + dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); + loadComponentSingleFile(versionManager, ScreenContainer.Add); + } loadComponentSingleFile(osuLogo, _ => { diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c25e62d69e..0739689daa 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -76,6 +76,9 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + [Resolved(canBeNull: true)] + private VersionManager versionManager { get; set; } + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -91,6 +94,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; + private SystemTitle systemTitle; private Sample reappearSampleSwoosh; @@ -153,6 +157,7 @@ namespace osu.Game.Screens.Menu Margin = new MarginPadding { Right = 15, Top = 5 } }, new KiaiMenuFountains(), + systemTitle = new SystemTitle(), holdToExitGameOverlay?.CreateProxy() ?? Empty() }); @@ -263,6 +268,16 @@ namespace osu.Game.Screens.Menu } } + protected override void Update() + { + base.Update(); + + systemTitle.Margin = new MarginPadding + { + Bottom = (versionManager?.DrawHeight + 5) ?? 0 + }; + } + protected override void LogoSuspending(OsuLogo logo) { var seq = logo.FadeOut(300, Easing.InSine) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs new file mode 100644 index 0000000000..6ca9aab6c7 --- /dev/null +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Framework.Platform; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Menu +{ + public partial class SystemTitle : CompositeDrawable + { + internal Bindable Current { get; } = new Bindable(); + + private Container content = null!; + private CancellationTokenSource? cancellationTokenSource; + private SystemTitleImage? currentImage; + + [BackgroundDependencyLoader] + private void load(GameHost? gameHost) + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + AutoSizeAxes = Axes.Both; + + InternalChild = content = new ClickableContainer + { + AutoSizeAxes = Axes.Both, + Action = () => + { + if (!string.IsNullOrEmpty(Current.Value?.Url)) + gameHost?.OpenUrlExternally(Current.Value.Url); + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + content.ScaleTo(1.1f, 500, Easing.OutBounce); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + content.ScaleTo(1f, 500, Easing.OutBounce); + base.OnHoverLost(e); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => loadNewImage(), true); + } + + private void loadNewImage() + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = null; + currentImage?.FadeOut(500, Easing.OutQuint).Expire(); + + if (string.IsNullOrEmpty(Current.Value?.Image)) + return; + + LoadComponentAsync(new SystemTitleImage(Current.Value), loaded => + { + if (loaded.SystemTitle != Current.Value) + loaded.Dispose(); + + loaded.FadeInFromZero(500, Easing.OutQuint); + content.Add(currentImage = loaded); + }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); + } + + [LongRunningLoad] + private partial class SystemTitleImage : Sprite + { + public readonly APISystemTitle SystemTitle; + + public SystemTitleImage(APISystemTitle systemTitle) + { + SystemTitle = systemTitle; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textureStore) + { + var texture = textureStore.Get(SystemTitle.Image); + if (SystemTitle.Image.Contains(@"@2x")) + texture.ScaleAdjust *= 2; + Texture = texture; + } + } + } +} From a3f720bc627f27d6bcc9fe36c011190010930b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 23:31:14 +0100 Subject: [PATCH 107/539] Retrieve system title from online source --- .../API/Requests/GetSystemTitleRequest.cs | 15 ++++++++++++ .../API/Requests/Responses/APISystemTitle.cs | 2 +- osu.Game/Screens/Menu/SystemTitle.cs | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/API/Requests/GetSystemTitleRequest.cs diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs new file mode 100644 index 0000000000..52ca0c11eb --- /dev/null +++ b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetSystemTitleRequest : OsuJsonWebRequest + { + public GetSystemTitleRequest() + : base(@"https://assets.ppy.sh/lazer-status.json") + { + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs index 3a2342cc4c..97bdc8355a 100644 --- a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs +++ b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses { - public class APISystemTitle + public record APISystemTitle { [JsonProperty(@"image")] public string Image { get; set; } = string.Empty; diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 6ca9aab6c7..0867fc4748 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -1,7 +1,9 @@ // 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.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Framework.Platform; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Screens.Menu @@ -57,6 +60,26 @@ namespace osu.Game.Screens.Menu base.LoadComplete(); Current.BindValueChanged(_ => loadNewImage(), true); + + checkForUpdates(); + Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(15).TotalMilliseconds, true); + } + + private void checkForUpdates() + { + var request = new GetSystemTitleRequest(); + Task.Run(() => request.Perform()) + .ContinueWith(r => + { + if (r.IsCompletedSuccessfully) + Schedule(() => Current.Value = request.ResponseObject); + + // if the request failed, "observe" the exception. + // it isn't very important why this failed, as it's only for display. + // the inner error will be logged by framework mechanisms anyway. + if (r.IsFaulted) + _ = r.Exception; + }); } private void loadNewImage() From ac449131edcee95d9ca61fef538aeea7010c128d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Dec 2023 23:47:37 +0100 Subject: [PATCH 108/539] CodeFileSanity does not like records in standalone files --- .../API/Requests/Responses/APISystemTitle.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs index 97bdc8355a..6d5d91f8f3 100644 --- a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs +++ b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs @@ -1,16 +1,29 @@ // 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 Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses { - public record APISystemTitle + public class APISystemTitle : IEquatable { [JsonProperty(@"image")] public string Image { get; set; } = string.Empty; [JsonProperty(@"url")] public string Url { get; set; } = string.Empty; + + public bool Equals(APISystemTitle? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Image == other.Image && Url == other.Url; + } + + public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Image, Url); } } From ef3975981376e7308f4156024929f404f9240504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 00:09:51 +0100 Subject: [PATCH 109/539] More code quality inspections --- osu.Game/Online/API/Requests/Responses/APISystemTitle.cs | 1 + osu.Game/Screens/Menu/SystemTitle.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs index 6d5d91f8f3..bfa5c1043b 100644 --- a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs +++ b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs @@ -24,6 +24,7 @@ namespace osu.Game.Online.API.Requests.Responses public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other); + // ReSharper disable NonReadonlyMemberInGetHashCode public override int GetHashCode() => HashCode.Combine(Image, Url); } } diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 0867fc4748..6b976a8ed6 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new SystemTitleImage(Current.Value), loaded => { - if (loaded.SystemTitle != Current.Value) + if (!loaded.SystemTitle.Equals(Current.Value)) loaded.Dispose(); loaded.FadeInFromZero(500, Easing.OutQuint); From cbfcda79299e2ed2d42e720b81dcf6cd6f55984f Mon Sep 17 00:00:00 2001 From: Felipe Marins Date: Thu, 28 Dec 2023 00:10:01 -0300 Subject: [PATCH 110/539] Expose `SelectAll()` method on `ShearedSearchTextBox` --- osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 131041e706..c3a9f8a586 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -48,6 +48,8 @@ namespace osu.Game.Graphics.UserInterface public void KillFocus() => textBox.KillFocus(); + public bool SelectAll() => textBox.SelectAll(); + public ShearedSearchTextBox() { Height = 42; From dce9204731972d81aaeedb42002de243b9c0742c Mon Sep 17 00:00:00 2001 From: Felipe Marins Date: Thu, 28 Dec 2023 00:10:44 -0300 Subject: [PATCH 111/539] Select search box text on ModSelectOverlay when mod selection changes --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index baa7e594c1..43f44d682d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -565,6 +565,9 @@ namespace osu.Game.Overlays.Mods .ToArray(); SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); + + if (SearchTextBox.HasFocus) + SearchTextBox.SelectAll(); } #region Transition handling From f8d6b8e347c18e2bf0bec85cdf76aac6f0f5d7a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 14:10:52 +0900 Subject: [PATCH 112/539] Adjust toolbar animations / layering to feel better --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index bd5faf1588..03a1cfc005 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -183,7 +183,7 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { - flashBackground.FadeOutFromOne(800, Easing.OutQuint); + flashBackground.FadeIn(50).Then().FadeOutFromOne(800, Easing.OutQuint); tooltipContainer.FadeOut(100); return base.OnClick(e); } diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 09b8df14a6..06755a9da9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Both, Colour = colours.Carmine.Opacity(180), Blending = BlendingParameters.Additive, - Depth = 2, + Depth = float.MaxValue, Alpha = 0, }); @@ -65,11 +65,11 @@ namespace osu.Game.Overlays.Toolbar switch (state.NewValue) { case Visibility.Hidden: - stateBackground.FadeOut(200); + stateBackground.FadeOut(200, Easing.OutQuint); break; case Visibility.Visible: - stateBackground.FadeIn(200); + stateBackground.FadeIn(200, Easing.OutQuint); break; } } From ffc8778d673dd54a650066b9dc47d6252367da15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 14:13:35 +0900 Subject: [PATCH 113/539] Fix song select leaderboard tab ordering not matching stable --- .../Select/Leaderboards/BeatmapLeaderboardScope.cs | 6 +++--- osu.Game/Screens/Select/PlayBeatmapDetailArea.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index 5bcb4c27a7..e2e3404877 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -12,12 +12,12 @@ namespace osu.Game.Screens.Select.Leaderboards [Description("Local Ranking")] Local, - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardCountry))] - Country, - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardGlobal))] Global, + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardCountry))] + Country, + [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowScoreboardFriend))] Friend, } diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index 8a1b9ef3e1..deb1100dfc 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -80,8 +80,8 @@ namespace osu.Game.Screens.Select protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Concat(new BeatmapDetailAreaTabItem[] { new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local), - new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global), + new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country), new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend), }).ToArray(); @@ -95,12 +95,12 @@ namespace osu.Game.Screens.Select case TabType.Local: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local); - case TabType.Country: - return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country); - case TabType.Global: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global); + case TabType.Country: + return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country); + case TabType.Friends: return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend); From 93a8afe96e101a63f65e6bcca92d9707c1fde767 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 14:40:10 +0900 Subject: [PATCH 114/539] Add very simple cache-busting (30 minutes) --- osu.Game/Online/API/Requests/GetSystemTitleRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs index 52ca0c11eb..659e46bb11 100644 --- a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs +++ b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs @@ -1,6 +1,7 @@ // 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 osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests @@ -8,7 +9,7 @@ namespace osu.Game.Online.API.Requests public class GetSystemTitleRequest : OsuJsonWebRequest { public GetSystemTitleRequest() - : base(@"https://assets.ppy.sh/lazer-status.json") + : base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}") { } } From c70e7d340da905d081eb87863ea9588021816453 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 14:48:17 +0900 Subject: [PATCH 115/539] Adjust animation and add delay to URL open --- osu.Game/Screens/Menu/SystemTitle.cs | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 6b976a8ed6..2c8cb90b96 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -12,6 +12,8 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Framework.Platform; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -25,6 +27,8 @@ namespace osu.Game.Screens.Menu private CancellationTokenSource? cancellationTokenSource; private SystemTitleImage? currentImage; + private ScheduledDelegate? openUrlAction; + [BackgroundDependencyLoader] private void load(GameHost? gameHost) { @@ -32,29 +36,52 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomCentre; AutoSizeAxes = Axes.Both; - InternalChild = content = new ClickableContainer + InternalChild = content = new OsuClickableContainer { AutoSizeAxes = Axes.Both, Action = () => { - if (!string.IsNullOrEmpty(Current.Value?.Url)) - gameHost?.OpenUrlExternally(Current.Value.Url); + // Delay slightly to allow animation to play out. + openUrlAction?.Cancel(); + openUrlAction = Scheduler.AddDelayed(() => + { + if (!string.IsNullOrEmpty(Current.Value?.Url)) + gameHost?.OpenUrlExternally(Current.Value.Url); + }, 250); } }; } protected override bool OnHover(HoverEvent e) { - content.ScaleTo(1.1f, 500, Easing.OutBounce); + content.ScaleTo(1.05f, 2000, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - content.ScaleTo(1f, 500, Easing.OutBounce); + content.ScaleTo(1f, 500, Easing.OutQuint); base.OnHoverLost(e); } + protected override bool OnClick(ClickEvent e) + { + //hover.FlashColour(FlashColour, 800, Easing.OutQuint); + return base.OnClick(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.95f, 500, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + content.ScaleTo(1, 500, Easing.OutElastic); + base.OnMouseUp(e); + } + protected override void LoadComplete() { base.LoadComplete(); From 289e0f00f98ea570fd0a3f6b644b4399c0e4b5ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 14:58:05 +0900 Subject: [PATCH 116/539] Add flash on click --- osu.Game/Screens/Menu/SystemTitle.cs | 37 +++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 2c8cb90b96..667dc6e947 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Action = () => { + currentImage?.Flash(); + // Delay slightly to allow animation to play out. openUrlAction?.Cancel(); openUrlAction = Scheduler.AddDelayed(() => @@ -64,12 +66,6 @@ namespace osu.Game.Screens.Menu base.OnHoverLost(e); } - protected override bool OnClick(ClickEvent e) - { - //hover.FlashColour(FlashColour, 800, Easing.OutQuint); - return base.OnClick(e); - } - protected override bool OnMouseDown(MouseDownEvent e) { content.ScaleTo(0.95f, 500, Easing.OutQuint); @@ -78,7 +74,9 @@ namespace osu.Game.Screens.Menu protected override void OnMouseUp(MouseUpEvent e) { - content.ScaleTo(1, 500, Easing.OutElastic); + content + .ScaleTo(0.95f) + .ScaleTo(1, 500, Easing.OutElastic); base.OnMouseUp(e); } @@ -129,10 +127,12 @@ namespace osu.Game.Screens.Menu } [LongRunningLoad] - private partial class SystemTitleImage : Sprite + private partial class SystemTitleImage : CompositeDrawable { public readonly APISystemTitle SystemTitle; + private Sprite flash = null!; + public SystemTitleImage(APISystemTitle systemTitle) { SystemTitle = systemTitle; @@ -144,7 +144,26 @@ namespace osu.Game.Screens.Menu var texture = textureStore.Get(SystemTitle.Image); if (SystemTitle.Image.Contains(@"@2x")) texture.ScaleAdjust *= 2; - Texture = texture; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Sprite { Texture = texture }, + flash = new Sprite + { + Texture = texture, + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }; + } + + public void Flash() + { + flash.FadeInFromZero(50) + .Then() + .FadeOut(500, Easing.OutQuint); } } } From 481a25178658e80891a59bcdd7ad1720576854fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 15:10:05 +0900 Subject: [PATCH 117/539] Use `HandleLink` to allow potentially opening wiki or otherwise --- osu.Game/Screens/Menu/SystemTitle.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 667dc6e947..967b928b43 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; -using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests; @@ -30,7 +29,7 @@ namespace osu.Game.Screens.Menu private ScheduledDelegate? openUrlAction; [BackgroundDependencyLoader] - private void load(GameHost? gameHost) + private void load(OsuGame? game) { Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; @@ -48,7 +47,7 @@ namespace osu.Game.Screens.Menu openUrlAction = Scheduler.AddDelayed(() => { if (!string.IsNullOrEmpty(Current.Value?.Url)) - gameHost?.OpenUrlExternally(Current.Value.Url); + game?.HandleLink(Current.Value.Url); }, 250); } }; From 972234b1e5c1a505f23fe1491d509ab343636b4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 15:12:44 +0900 Subject: [PATCH 118/539] Move re-schedule inside continuation --- osu.Game/Screens/Menu/SystemTitle.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 967b928b43..b8510dde87 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -86,7 +86,6 @@ namespace osu.Game.Screens.Menu Current.BindValueChanged(_ => loadNewImage(), true); checkForUpdates(); - Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(15).TotalMilliseconds, true); } private void checkForUpdates() @@ -103,6 +102,8 @@ namespace osu.Game.Screens.Menu // the inner error will be logged by framework mechanisms anyway. if (r.IsFaulted) _ = r.Exception; + + Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(15).TotalMilliseconds); }); } From 0ea62d0c5d52f98c19062e8baba5e225daea8c5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 15:16:42 +0900 Subject: [PATCH 119/539] Add initial additive blending on fade in --- osu.Game/Screens/Menu/SystemTitle.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index b8510dde87..8b243e8f7f 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -121,7 +121,6 @@ namespace osu.Game.Screens.Menu if (!loaded.SystemTitle.Equals(Current.Value)) loaded.Dispose(); - loaded.FadeInFromZero(500, Easing.OutQuint); content.Add(currentImage = loaded); }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); } @@ -154,16 +153,25 @@ namespace osu.Game.Screens.Menu { Texture = texture, Blending = BlendingParameters.Additive, - Alpha = 0, }, }; } - public void Flash() + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeInFromZero(500, Easing.OutQuint); + flash.FadeOutFromOne(4000, Easing.OutQuint); + } + + public Drawable Flash() { flash.FadeInFromZero(50) .Then() .FadeOut(500, Easing.OutQuint); + + return this; } } } From 6684987289a6dcbd0ab918cd9976909bead57dc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 15:18:21 +0900 Subject: [PATCH 120/539] Reduce refresh interval slightly --- osu.Game/Screens/Menu/SystemTitle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 8b243e8f7f..bb623cacdf 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu if (r.IsFaulted) _ = r.Exception; - Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(15).TotalMilliseconds); + Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(5).TotalMilliseconds); }); } From a1867afbb43307ca775e56586bb6c86af038cc81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 16:05:20 +0900 Subject: [PATCH 121/539] Move menu tips to main menu In preparation for removing the disclaimer screen. --- osu.Game/Configuration/OsuConfigManager.cs | 2 + osu.Game/Localisation/UserInterfaceStrings.cs | 7 +- .../UserInterface/MainMenuSettings.cs | 5 ++ osu.Game/Screens/Menu/Disclaimer.cs | 30 ------- osu.Game/Screens/Menu/MainMenu.cs | 38 +++++++- osu.Game/Screens/Menu/MenuTip.cs | 89 +++++++++++++++++++ osu.Game/Screens/Menu/SystemTitle.cs | 2 - 7 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 osu.Game/Screens/Menu/MenuTip.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 0df870655f..d4162b76d6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -96,6 +96,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.MenuVoice, true); SetDefault(OsuSetting.MenuMusic, true); + SetDefault(OsuSetting.MenuTips, true); SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -350,6 +351,7 @@ namespace osu.Game.Configuration VolumeInactive, MenuMusic, MenuVoice, + MenuTips, CursorRotation, MenuParallax, Prefer24HourTime, diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index 68c5c3ccbc..dceedca05c 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -24,6 +24,11 @@ namespace osu.Game.Localisation /// public static LocalisableString MenuCursorSize => new TranslatableString(getKey(@"menu_cursor_size"), @"Menu cursor size"); + /// + /// "Menu tips" + /// + public static LocalisableString ShowMenuTips => new TranslatableString(getKey(@"show_menu_tips"), @"Menu tips"); + /// /// "Parallax" /// @@ -154,6 +159,6 @@ namespace osu.Game.Localisation /// public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 4577fadb01..5e42c3035c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface Children = new Drawable[] { + new SettingsCheckbox + { + LabelText = UserInterfaceStrings.ShowMenuTips, + Current = config.GetBindable(OsuSetting.MenuTips) + }, new SettingsCheckbox { LabelText = UserInterfaceStrings.InterfaceVoices, diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 539d58d2d7..1afef66313 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -122,10 +121,6 @@ namespace osu.Game.Screens.Menu textFlow.NewParagraph(); textFlow.NewParagraph(); - textFlow.AddParagraph("today's tip:", formatSemiBold); - textFlow.AddParagraph(getRandomTip(), formatRegular); - textFlow.NewParagraph(); - textFlow.NewParagraph(); iconColour = colours.Yellow; @@ -228,30 +223,5 @@ namespace osu.Game.Screens.Menu this.Push(nextScreen); }); } - - private string getRandomTip() - { - string[] tips = - { - "You can press Ctrl-T anywhere in the game to toggle the toolbar!", - "You can press Ctrl-O anywhere in the game to access options!", - "All settings are dynamic and take effect in real-time. Try pausing and changing the skin while playing!", - "New features are coming online every update. Make sure to stay up-to-date!", - "If you find the UI too large or small, try adjusting UI scale in settings!", - "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", - "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", - "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", - "Try scrolling down in the mod select panel to find a bunch of new fun mods!", - "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", - "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", - "Toggle advanced frame / thread statistics with Ctrl-F11!", - "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", - }; - - return tips[RNG.Next(0, tips.Length)]; - } } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0739689daa..cde7da53ce 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -95,6 +95,8 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; private Container logoTarget; private SystemTitle systemTitle; + private MenuTip menuTip; + private FillFlowContainer bottomElementsFlow; private Sample reappearSampleSwoosh; @@ -157,7 +159,27 @@ namespace osu.Game.Screens.Menu Margin = new MarginPadding { Right = 15, Top = 5 } }, new KiaiMenuFountains(), - systemTitle = new SystemTitle(), + bottomElementsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + menuTip = new MenuTip + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + systemTitle = new SystemTitle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + }, holdToExitGameOverlay?.CreateProxy() ?? Empty() }); @@ -220,6 +242,8 @@ namespace osu.Game.Screens.Menu if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None) dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); + + menuTip.ShowNextTip(); } [CanBeNull] @@ -272,7 +296,7 @@ namespace osu.Game.Screens.Menu { base.Update(); - systemTitle.Margin = new MarginPadding + bottomElementsFlow.Margin = new MarginPadding { Bottom = (versionManager?.DrawHeight + 5) ?? 0 }; @@ -314,6 +338,10 @@ namespace osu.Game.Screens.Menu buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine); sideFlashes.FadeOut(64, Easing.OutQuint); + + bottomElementsFlow + .ScaleTo(0.9f, 1000, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); } public override void OnResuming(ScreenTransitionEvent e) @@ -330,6 +358,12 @@ namespace osu.Game.Screens.Menu preloadSongSelect(); musicController.EnsurePlayingSomething(); + + menuTip.ShowNextTip(); + + bottomElementsFlow + .ScaleTo(1, 1000, Easing.OutQuint) + .FadeIn(1000, Easing.OutQuint); } public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs new file mode 100644 index 0000000000..c9c8fea1c1 --- /dev/null +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Screens.Menu +{ + public partial class MenuTip : CompositeDrawable + { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private LinkFlowContainer textFlow = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + textFlow = new LinkFlowContainer + { + Width = 700, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(0, 2), + }, + }; + } + + public void ShowNextTip() + { + if (!config.Get(OsuSetting.MenuTips)) return; + + static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular); + static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold); + + string tip = getRandomTip(); + + AutoSizeAxes = Axes.Both; + + textFlow.Clear(); + textFlow.AddParagraph("a tip for you:", formatSemiBold); + textFlow.AddParagraph(tip, formatRegular); + + this.FadeInFromZero(200, Easing.OutQuint) + .Delay(1000 + 80 * tip.Length) + .Then() + .FadeOutFromOne(2000, Easing.OutQuint) + .Finally(_ => AutoSizeAxes = Axes.X); + } + + private string getRandomTip() + { + string[] tips = + { + "You can press Ctrl-T anywhere in the game to toggle the toolbar!", + "You can press Ctrl-O anywhere in the game to access options!", + "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", + "New features are coming online every update. Make sure to stay up-to-date!", + "If you find the UI too large or small, try adjusting UI scale in settings!", + "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", + "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", + "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", + "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", + "Try scrolling down in the mod select panel to find a bunch of new fun mods!", + "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", + "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", + "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", + "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", + "Toggle advanced frame / thread statistics with Ctrl-F11!", + "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", + }; + + return tips[RNG.Next(0, tips.Length)]; + } + } +} diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index bb623cacdf..060e426f8c 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(OsuGame? game) { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; AutoSizeAxes = Axes.Both; InternalChild = content = new OsuClickableContainer From 222459d921f886a7dd621057752a94442bc703d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 16:16:27 +0900 Subject: [PATCH 122/539] Add background and improve layout --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/Menu/MenuTip.cs | 32 ++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index cde7da53ce..e8554d077b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Menu Direction = FillDirection.Vertical, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Spacing = new Vector2(15), + Spacing = new Vector2(5), Children = new Drawable[] { menuTip = new MenuTip diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index c9c8fea1c1..ac23162bd0 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -4,12 +4,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Menu { @@ -27,14 +29,29 @@ namespace osu.Game.Screens.Menu InternalChildren = new Drawable[] { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 15, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + } + }, textFlow = new LinkFlowContainer { - Width = 700, + Width = 600, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Spacing = new Vector2(0, 2), + Margin = new MarginPadding(10) }, }; } @@ -43,13 +60,11 @@ namespace osu.Game.Screens.Menu { if (!config.Get(OsuSetting.MenuTips)) return; - static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular); - static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold); + static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); + static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); string tip = getRandomTip(); - AutoSizeAxes = Axes.Both; - textFlow.Clear(); textFlow.AddParagraph("a tip for you:", formatSemiBold); textFlow.AddParagraph(tip, formatRegular); @@ -57,8 +72,7 @@ namespace osu.Game.Screens.Menu this.FadeInFromZero(200, Easing.OutQuint) .Delay(1000 + 80 * tip.Length) .Then() - .FadeOutFromOne(2000, Easing.OutQuint) - .Finally(_ => AutoSizeAxes = Axes.X); + .FadeOutFromOne(2000, Easing.OutQuint); } private string getRandomTip() From 932d03a4f8aebf8b76d43fad4c7b8ffa17e0ae95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 16:19:12 +0900 Subject: [PATCH 123/539] Make toggle more immediately hide/show tips --- osu.Game/Screens/Menu/MainMenu.cs | 3 +-- osu.Game/Screens/Menu/MenuTip.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index e8554d077b..e3fc69dc2f 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -242,8 +242,6 @@ namespace osu.Game.Screens.Menu if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None) dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); - - menuTip.ShowNextTip(); } [CanBeNull] @@ -359,6 +357,7 @@ namespace osu.Game.Screens.Menu musicController.EnsurePlayingSomething(); + // Cycle tip on resuming menuTip.ShowNextTip(); bottomElementsFlow diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index ac23162bd0..325fb07767 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -22,6 +23,8 @@ namespace osu.Game.Screens.Menu private LinkFlowContainer textFlow = null!; + private Bindable showMenuTips = null!; + [BackgroundDependencyLoader] private void load() { @@ -56,9 +59,21 @@ namespace osu.Game.Screens.Menu }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + showMenuTips = config.GetBindable(OsuSetting.MenuTips); + showMenuTips.BindValueChanged(_ => ShowNextTip(), true); + } + public void ShowNextTip() { - if (!config.Get(OsuSetting.MenuTips)) return; + if (!showMenuTips.Value) + { + this.FadeOut(100, Easing.OutQuint); + return; + } static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); From 0ad6ac8b2a2866d5b26cb044110122cd4702cd8b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 16:48:17 +0900 Subject: [PATCH 124/539] Remove unused variable --- osu.Game/Screens/Menu/MainMenu.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index e3fc69dc2f..a90d9151b5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -94,7 +94,6 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; - private SystemTitle systemTitle; private MenuTip menuTip; private FillFlowContainer bottomElementsFlow; @@ -173,7 +172,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - systemTitle = new SystemTitle + new SystemTitle { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, From b19f72481b5d3cd1ad81458601aca0404a09d8fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:19:41 +0900 Subject: [PATCH 125/539] Fade out quickly on game exit sequence --- osu.Game/Screens/Menu/MainMenu.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a90d9151b5..bc19e9cb63 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -399,6 +399,10 @@ namespace osu.Game.Screens.Menu songTicker.Hide(); this.FadeOut(3000); + + bottomElementsFlow + .FadeOut(500, Easing.OutQuint); + return base.OnExiting(e); } From 1f2339244ef739341e3429d88678e512430a47d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:14:16 +0900 Subject: [PATCH 126/539] Add supporter display to main menu --- .../Visual/Menus/TestSceneSupporterDisplay.cs | 37 +++++ osu.Game/Screens/Menu/MainMenu.cs | 13 ++ osu.Game/Screens/Menu/SupporterDisplay.cs | 126 ++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs create mode 100644 osu.Game/Screens/Menu/SupporterDisplay.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs b/osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs new file mode 100644 index 0000000000..8b18adbe0d --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneSupporterDisplay.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneSupporterDisplay : OsuTestScene + { + [Test] + public void TestBasic() + { + AddStep("create display", () => + { + Child = new SupporterDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddStep("toggle support", () => + { + ((DummyAPIAccess)API).LocalUser.Value = new APIUser + { + Username = API.LocalUser.Value.Username, + Id = API.LocalUser.Value.Id + 1, + IsSupporter = !API.LocalUser.Value.IsSupporter, + }; + }); + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index bc19e9cb63..401460a498 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -96,6 +96,7 @@ namespace osu.Game.Screens.Menu private Container logoTarget; private MenuTip menuTip; private FillFlowContainer bottomElementsFlow; + private SupporterDisplay supporterDisplay; private Sample reappearSampleSwoosh; @@ -179,6 +180,12 @@ namespace osu.Game.Screens.Menu } } }, + supporterDisplay = new SupporterDisplay + { + Margin = new MarginPadding(5), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, holdToExitGameOverlay?.CreateProxy() ?? Empty() }); @@ -339,6 +346,9 @@ namespace osu.Game.Screens.Menu bottomElementsFlow .ScaleTo(0.9f, 1000, Easing.OutQuint) .FadeOut(500, Easing.OutQuint); + + supporterDisplay + .FadeOut(500, Easing.OutQuint); } public override void OnResuming(ScreenTransitionEvent e) @@ -403,6 +413,9 @@ namespace osu.Game.Screens.Menu bottomElementsFlow .FadeOut(500, Easing.OutQuint); + supporterDisplay + .FadeOut(500, Easing.OutQuint); + return base.OnExiting(e); } diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs new file mode 100644 index 0000000000..d8ba21cd88 --- /dev/null +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -0,0 +1,126 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class SupporterDisplay : CompositeDrawable + { + private LinkFlowContainer supportFlow = null!; + + private Drawable heart = null!; + + private readonly IBindable currentUser = new Bindable(); + + private Box backgroundBox = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = 40; + + AutoSizeAxes = Axes.X; + AutoSizeDuration = 1000; + AutoSizeEasing = Easing.OutQuint; + + Masking = true; + CornerExponent = 2.5f; + CornerRadius = 15; + + InternalChildren = new Drawable[] + { + backgroundBox = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + supportFlow = new LinkFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Spacing = new Vector2(0, 2), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const float font_size = 14; + + static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: font_size, weight: FontWeight.SemiBold); + + currentUser.BindTo(api.LocalUser); + currentUser.BindValueChanged(e => + { + supportFlow.Children.ForEach(d => d.FadeOut().Expire()); + + if (e.NewValue.IsSupporter) + { + supportFlow.AddText("Eternal thanks to you for supporting osu!", formatSemiBold); + + backgroundBox.FadeColour(colours.Pink, 250); + } + else + { + supportFlow.AddText("Consider becoming an ", formatSemiBold); + supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", formatSemiBold); + supportFlow.AddText(" to help support osu!'s development", formatSemiBold); + + backgroundBox.FadeColour(colours.Pink4, 250); + } + + supportFlow.AddIcon(FontAwesome.Solid.Heart, t => + { + heart = t; + + t.Padding = new MarginPadding { Left = 5, Top = 1 }; + t.Font = t.Font.With(size: font_size); + t.Origin = Anchor.Centre; + t.Colour = colours.Pink; + + Schedule(() => + { + heart?.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); + }); + }); + }, true); + + this + .FadeOut() + .Delay(1000) + .FadeInFromZero(800, Easing.OutQuint); + + Scheduler.AddDelayed(() => + { + AutoSizeEasing = Easing.In; + supportFlow.BypassAutoSizeAxes = Axes.X; + this + .Delay(200) + .FadeOut(750, Easing.Out); + }, 6000); + } + } +} From 7dc50b9baf5e6268c84b4177d3f6b14bbaa99514 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:18:47 +0900 Subject: [PATCH 127/539] Don't dismiss on hover, and allow dismissing via click --- osu.Game/Screens/Menu/SupporterDisplay.cs | 43 ++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs index d8ba21cd88..6639300f4a 100644 --- a/osu.Game/Screens/Menu/SupporterDisplay.cs +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -8,6 +8,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -113,8 +115,47 @@ namespace osu.Game.Screens.Menu .Delay(1000) .FadeInFromZero(800, Easing.OutQuint); - Scheduler.AddDelayed(() => + scheduleDismissal(); + } + + protected override bool OnClick(ClickEvent e) + { + dismissalDelegate?.Cancel(); + + supportFlow.BypassAutoSizeAxes = Axes.X; + this.FadeOut(500, Easing.OutQuint); + return base.OnClick(e); + } + + protected override bool OnHover(HoverEvent e) + { + backgroundBox.FadeTo(0.6f, 500, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + backgroundBox.FadeTo(0.4f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + private ScheduledDelegate? dismissalDelegate; + + private void scheduleDismissal() + { + dismissalDelegate?.Cancel(); + dismissalDelegate = Scheduler.AddDelayed(() => { + // If the user is hovering they may want to interact with the link. + // Give them more time. + if (IsHovered) + { + scheduleDismissal(); + return; + } + + dismissalDelegate?.Cancel(); + AutoSizeEasing = Easing.In; supportFlow.BypassAutoSizeAxes = Axes.X; this From bd0e2b4dde99cd20d990959ba52965cc73099977 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:21:29 +0900 Subject: [PATCH 128/539] Remove disclaimer screen completely --- .../Visual/Menus/TestSceneDisclaimer.cs | 29 --- osu.Game/Screens/Loader.cs | 13 +- osu.Game/Screens/Menu/Disclaimer.cs | 227 ------------------ 3 files changed, 2 insertions(+), 267 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs delete mode 100644 osu.Game/Screens/Menu/Disclaimer.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs deleted file mode 100644 index fb82b0df80..0000000000 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.Menu; - -namespace osu.Game.Tests.Visual.Menus -{ - public partial class TestSceneDisclaimer : ScreenTestScene - { - [BackgroundDependencyLoader] - private void load() - { - AddStep("load disclaimer", () => LoadScreen(new Disclaimer())); - - AddStep("toggle support", () => - { - ((DummyAPIAccess)API).LocalUser.Value = new APIUser - { - Username = API.LocalUser.Value.Username, - Id = API.LocalUser.Value.Id + 1, - IsSupporter = !API.LocalUser.Value.IsSupporter, - }; - }); - } - } -} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 372cfe748e..4dba512cbd 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens { public partial class Loader : StartupScreen { - private bool showDisclaimer; - public Loader() { ValidForResume = false; @@ -35,13 +33,7 @@ namespace osu.Game.Screens private LoadingSpinner spinner; private ScheduledDelegate spinnerShow; - protected virtual OsuScreen CreateLoadableScreen() - { - if (showDisclaimer) - return new Disclaimer(getIntroSequence()); - - return getIntroSequence(); - } + protected virtual OsuScreen CreateLoadableScreen() => getIntroSequence(); private IntroScreen getIntroSequence() { @@ -107,9 +99,8 @@ namespace osu.Game.Screens } [BackgroundDependencyLoader] - private void load(OsuGameBase game, OsuConfigManager config) + private void load(OsuConfigManager config) { - showDisclaimer = game.IsDeployedBuild; introSequence = config.Get(OsuSetting.IntroSequence); } diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs deleted file mode 100644 index 1afef66313..0000000000 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Screens; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Menu -{ - public partial class Disclaimer : StartupScreen - { - private SpriteIcon icon; - private Color4 iconColour; - private LinkFlowContainer textFlow; - private LinkFlowContainer supportFlow; - - private Drawable heart; - - private const float icon_y = -85; - private const float icon_size = 30; - - private readonly OsuScreen nextScreen; - - private readonly Bindable currentUser = new Bindable(); - private FillFlowContainer fill; - - private readonly List expendableText = new List(); - - public Disclaimer(OsuScreen nextScreen = null) - { - this.nextScreen = nextScreen; - ValidForResume = false; - } - - [Resolved] - private IAPIProvider api { get; set; } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChildren = new Drawable[] - { - icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = OsuIcon.Logo, - Size = new Vector2(icon_size), - Y = icon_y, - }, - fill = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Y = icon_y, - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - textFlow = new LinkFlowContainer - { - Width = 680, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(0, 2), - }, - } - }, - supportFlow = new LinkFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding(20), - Alpha = 0, - Spacing = new Vector2(0, 2), - }, - }; - - textFlow.AddText("this is osu!", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular)); - - expendableText.Add(textFlow.AddText("lazer", t => - { - t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Regular); - t.Colour = colours.PinkLight; - })); - - static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular); - static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold); - - textFlow.NewParagraph(); - - textFlow.AddText("the next ", formatRegular); - textFlow.AddText("major update", t => - { - t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold); - t.Colour = colours.Pink; - }); - expendableText.Add(textFlow.AddText(" coming to osu!", formatRegular)); - textFlow.AddText(".", formatRegular); - - textFlow.NewParagraph(); - textFlow.NewParagraph(); - - textFlow.NewParagraph(); - - iconColour = colours.Yellow; - - // manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading). - // the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters. - currentUser.Value = api.LocalUser.Value; - currentUser.BindValueChanged(e => - { - supportFlow.Children.ForEach(d => d.FadeOut().Expire()); - - if (e.NewValue.IsSupporter) - { - supportFlow.AddText("Eternal thanks to you for supporting osu!", formatSemiBold); - } - else - { - supportFlow.AddText("Consider becoming an ", formatSemiBold); - supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", formatSemiBold); - supportFlow.AddText(" to help support osu!'s development", formatSemiBold); - } - - supportFlow.AddIcon(FontAwesome.Solid.Heart, t => - { - heart = t; - - t.Padding = new MarginPadding { Left = 5, Top = 3 }; - t.Font = t.Font.With(size: 20); - t.Origin = Anchor.Centre; - t.Colour = colours.Pink; - - Schedule(() => heart?.FlashColour(Color4.White, 750, Easing.OutQuint).Loop()); - }); - - if (supportFlow.IsPresent) - supportFlow.FadeInFromZero(500); - }, true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - if (nextScreen != null) - LoadComponentAsync(nextScreen); - - ((IBindable)currentUser).BindTo(api.LocalUser); - } - - public override void OnSuspending(ScreenTransitionEvent e) - { - base.OnSuspending(e); - - // Once this screen has finished being displayed, we don't want to unnecessarily handle user change events. - currentUser.UnbindAll(); - } - - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); - - icon.RotateTo(10); - icon.FadeOut(); - icon.ScaleTo(0.5f); - - icon.Delay(500).FadeIn(500).ScaleTo(1, 500, Easing.OutQuint); - - using (BeginDelayedSequence(3000)) - { - icon.FadeColour(iconColour, 200, Easing.OutQuint); - icon.MoveToY(icon_y * 1.3f, 500, Easing.OutCirc) - .RotateTo(-360, 520, Easing.OutQuint) - .Then() - .MoveToY(icon_y, 160, Easing.InQuart) - .FadeColour(Color4.White, 160); - - using (BeginDelayedSequence(520 + 160)) - { - fill.MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart); - Schedule(() => expendableText.SelectMany(t => t.Drawables).ForEach(t => - { - t.FadeOut(100); - t.ScaleTo(new Vector2(0, 1), 100, Easing.OutQuart); - })); - } - } - - supportFlow.FadeOut().Delay(2000).FadeIn(500); - double delay = 500; - foreach (var c in textFlow.Children) - c.FadeTo(0.001f).Delay(delay += 20).FadeIn(500); - - this - .FadeInFromZero(500) - .Then(5500) - .FadeOut(250) - .ScaleTo(0.9f, 250, Easing.InQuint) - .Finally(_ => - { - if (nextScreen != null) - this.Push(nextScreen); - }); - } - } -} From 2ec9343868693f45f7650e2209271b157222d973 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:35:10 +0900 Subject: [PATCH 129/539] Add the ability to spectate a user by right clicking their user panel --- osu.Game/Localisation/ContextMenuStrings.cs | 9 +++-- osu.Game/Users/UserPanel.cs | 39 +++++++++++++-------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs index 029fba67d8..cb18a2159c 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -20,9 +20,14 @@ namespace osu.Game.Localisation public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap"); /// - /// "Invite player" + /// "Invite to room" /// - public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player"); + public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite to room"); + + /// + /// "Spectate" + /// + public static LocalisableString SpectatePlayer => new TranslatableString(getKey(@"spectate_player"), @"Spectate"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 273faf9bd1..b6a77e754d 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -13,6 +13,7 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -20,6 +21,8 @@ using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; using osu.Game.Online.Multiplayer; +using osu.Game.Screens; +using osu.Game.Screens.Play; namespace osu.Game.Users { @@ -60,6 +63,9 @@ namespace osu.Game.Users [Resolved] protected OverlayColourProvider? ColourProvider { get; private set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Resolved] protected OsuColour Colours { get; private set; } = null!; @@ -113,23 +119,26 @@ namespace osu.Game.Users new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile) }; - if (!User.Equals(api.LocalUser.Value)) - { - items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () => - { - channelManager?.OpenPrivateChannel(User); - chatOverlay?.Show(); - })); - } + if (User.Equals(api.LocalUser.Value)) + return items.ToArray(); - if ( - // TODO: uncomment this once lazer / osu-web is updating online states - // User.IsOnline && - multiplayerClient?.Room != null && - multiplayerClient.Room.Users.All(u => u.UserID != User.Id) - ) + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () => { - items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + channelManager?.OpenPrivateChannel(User); + chatOverlay?.Show(); + })); + + if (User.IsOnline) + { + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => + { + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); + })); + + if (multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + } } return items.ToArray(); From 22eced33006f959af2bc17198eafd46779a4923b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 17:40:07 +0900 Subject: [PATCH 130/539] Show local user in online users --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 37ea3f9c38..ee277ff538 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -131,9 +131,6 @@ namespace osu.Game.Overlays.Dashboard { int userId = kvp.Key; - if (userId == api.LocalUser.Value.Id) - continue; - users.GetUserAsync(userId).ContinueWith(task => { APIUser user = task.GetResultSafely(); From 28e220ca501592f854aab01b294981ca4d83154e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 19:04:19 +0900 Subject: [PATCH 131/539] Update popup dialog design Had to be done. I hated the old ones so much. As usual, disclaimer that this is an iterative design and will probably be replaced in the future. --- .../UserInterface/TestSceneDialogOverlay.cs | 19 ++--- .../Graphics/UserInterface/DialogButton.cs | 14 ++-- osu.Game/Overlays/Dialog/PopupDialog.cs | 77 +++++++++++-------- osu.Game/Overlays/DialogOverlay.cs | 12 +-- 4 files changed, 68 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs index 81b692004b..3b38343f04 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -19,11 +20,15 @@ namespace osu.Game.Tests.Visual.UserInterface { private DialogOverlay overlay; + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); + } + [Test] public void TestBasic() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog firstDialog = null; TestPopupDialog secondDialog = null; @@ -84,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface })); AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog); - AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null); + AddUntilStep("first dialog is not part of hierarchy", () => firstDialog.Parent == null); } [Test] @@ -92,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface { PopupDialog dialog = null; - AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay()); + AddStep("create slow loading dialog overlay", () => overlay = new SlowLoadingDialogOverlay()); AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add)); @@ -128,8 +133,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDismissBeforePush() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { @@ -146,8 +149,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDismissBeforePushViaButtonPress() { - AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); - TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { @@ -163,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddAssert("no dialog pushed", () => overlay.CurrentDialog == null); - AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null); + AddUntilStep("dialog is not part of hierarchy", () => testDialog.Parent == null); } private partial class TestPopupDialog : PopupDialog diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index db81bc991d..c920597a95 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface private const float idle_width = 0.8f; private const float hover_width = 0.9f; - private const float hover_duration = 500; + private const float hover_duration = 300; private const float click_duration = 200; public event Action? StateChanged; @@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box rightGlow; private readonly Box background; private readonly SpriteText spriteText; - private Vector2 hoverSpacing => new Vector2(3f, 0f); + private Vector2 hoverSpacing => new Vector2(1.4f, 0f); public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button) : base(sampleSet) @@ -279,15 +279,15 @@ namespace osu.Game.Graphics.UserInterface if (newState == SelectionState.Selected) { - spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); - ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic); + spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutQuint); + ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutQuint); glowContainer.FadeIn(hover_duration, Easing.OutQuint); } else { - ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic); - spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); - glowContainer.FadeOut(hover_duration, Easing.OutQuint); + ColourContainer.ResizeWidthTo(idle_width, hover_duration / 2, Easing.OutQuint); + spriteText.TransformSpacingTo(Vector2.Zero, hover_duration / 2, Easing.OutQuint); + glowContainer.FadeOut(hover_duration / 2, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 36a9baac67..663c2e78ce 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -25,11 +26,10 @@ namespace osu.Game.Overlays.Dialog public abstract partial class PopupDialog : VisibilityContainer { public const float ENTER_DURATION = 500; - public const float EXIT_DURATION = 200; + public const float EXIT_DURATION = 500; private readonly Vector2 ringSize = new Vector2(100f); private readonly Vector2 ringMinifiedSize = new Vector2(20f); - private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f); private readonly Box flashLayer; private Sample flashSample = null!; @@ -108,13 +108,20 @@ namespace osu.Game.Overlays.Dialog protected PopupDialog() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; Children = new Drawable[] { content = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Alpha = 0f, Children = new Drawable[] { @@ -122,11 +129,13 @@ namespace osu.Game.Overlays.Dialog { RelativeSizeAxes = Axes.Both, Masking = true, + CornerRadius = 20, + CornerExponent = 2.5f, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.5f), - Radius = 8, + Colour = Color4.Black.Opacity(0.2f), + Radius = 14, }, Children = new Drawable[] { @@ -142,23 +151,29 @@ namespace osu.Game.Overlays.Dialog ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, }, + flashLayer = new Box + { + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Colour = Color4Extensions.FromHex(@"221a21"), + }, }, }, new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0f, 10f), - Padding = new MarginPadding { Bottom = 10 }, + Padding = new MarginPadding { Vertical = 60 }, Children = new Drawable[] { new Container { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, + Padding = new MarginPadding { Bottom = 30 }, Size = ringSize, Children = new Drawable[] { @@ -181,6 +196,7 @@ namespace osu.Game.Overlays.Dialog Origin = Anchor.Centre, Anchor = Anchor.Centre, Icon = FontAwesome.Solid.TimesCircle, + Y = -2, Size = new Vector2(50), }, }, @@ -202,25 +218,18 @@ namespace osu.Game.Overlays.Dialog TextAnchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(5), + }, + buttonsContainer = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 30 }, }, }, }, - buttonsContainer = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }, - flashLayer = new Box - { - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Colour = Color4Extensions.FromHex(@"221a21"), - }, }, }, }; @@ -231,7 +240,7 @@ namespace osu.Game.Overlays.Dialog } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuColour colours) { flashSample = audio.Samples.Get(@"UI/default-select-disabled"); } @@ -288,15 +297,15 @@ namespace osu.Game.Overlays.Dialog // Reset various animations but only if the dialog animation fully completed if (content.Alpha == 0) { - buttonsContainer.TransformSpacingTo(buttonsEnterSpacing); - buttonsContainer.MoveToY(buttonsEnterSpacing.Y); + content.ScaleTo(0.7f); ring.ResizeTo(ringMinifiedSize); } - content.FadeIn(ENTER_DURATION, Easing.OutQuint); - ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint); - buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint); + content + .ScaleTo(1, 750, Easing.OutElasticHalf) + .FadeIn(ENTER_DURATION, Easing.OutQuint); + + ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint); } protected override void PopOut() @@ -306,7 +315,9 @@ namespace osu.Game.Overlays.Dialog // This is presumed to always be a sane default "cancel" action. buttonsContainer.Last().TriggerClick(); - content.FadeOut(EXIT_DURATION, Easing.InSine); + content + .ScaleTo(0.7f, EXIT_DURATION, Easing.Out) + .FadeOut(EXIT_DURATION, Easing.OutQuint); } private void pressButtonAtIndex(int index) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 005162bbcc..a85f1ecbcd 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -29,16 +29,18 @@ namespace osu.Game.Overlays public DialogOverlay() { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; Child = dialogContainer = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }; - Width = 0.4f; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + Width = 500; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } [BackgroundDependencyLoader] From b7f3c83514deec96959b51bcbb227ec0ee4f8801 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 20:01:29 +0900 Subject: [PATCH 132/539] Expose the ability to update global offset from the player loader screen --- osu.Game/Overlays/SettingsOverlay.cs | 29 +++++++++++++++---- .../PlayerSettings/BeatmapOffsetControl.cs | 19 ++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 5735b1515a..cf76ed8781 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -3,19 +3,21 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Graphics; -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Localisation; namespace osu.Game.Overlays { @@ -55,6 +57,21 @@ namespace osu.Game.Overlays public override bool AcceptsFocus => lastOpenedSubPanel == null || lastOpenedSubPanel.State.Value == Visibility.Hidden; + public void ShowAtControl() + where T : Drawable + { + Show(); + + // wait for load of sections + if (!SectionsContainer.Any()) + { + Scheduler.Add(ShowAtControl); + return; + } + + SectionsContainer.ScrollTo(SectionsContainer.ChildrenOfType().Single()); + } + private T createSubPanel(T subPanel) where T : SettingsSubPanel { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 3f0f0fd1df..8c6f1a8f7f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -21,7 +21,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Localisation; +using osu.Game.Overlays; using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Audio; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -213,6 +215,8 @@ namespace osu.Game.Screens.Play.PlayerSettings lastPlayAverage = average; lastPlayBeatmapOffset = Current.Value; + LinkFlowContainer globalOffsetText; + referenceScoreContainer.AddRange(new Drawable[] { lastPlayGraph = new HitEventTimingDistributionGraph(hitEvents) @@ -226,9 +230,24 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage }, + globalOffsetText = new LinkFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } }); + + if (settings != null) + { + globalOffsetText.AddText("You can also "); + globalOffsetText.AddLink("adjust the global offset", () => settings.ShowAtControl()); + globalOffsetText.AddText(" based off this play."); + } } + [Resolved] + private SettingsOverlay? settings { get; set; } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 91af94086c6ba2905779c07ebb35dc011979993f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 20:02:06 +0900 Subject: [PATCH 133/539] Remove offset wizard button for now --- osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index af15d310fc..e05d20a5db 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" }); + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency", "wizard" }); [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -26,10 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Current = config.GetBindable(OsuSetting.AudioOffset), }, - new SettingsButton - { - Text = AudioSettingsStrings.OffsetWizard - } }; } } From e1a376c0a742d958399734683c8eb79631f37add Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 20:13:19 +0900 Subject: [PATCH 134/539] Fix using "Back" binding at spectator fail screen not working --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 10 +++++++++- osu.Game/Screens/Play/PauseOverlay.cs | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 0680842891..440b8d37b9 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -44,7 +44,15 @@ namespace osu.Game.Screens.Play /// /// Action that is invoked when is triggered. /// - protected virtual Action BackAction => () => InternalButtons.LastOrDefault()?.TriggerClick(); + protected virtual Action BackAction => () => + { + // We prefer triggering the button click as it will animate... + // but sometimes buttons aren't present (see FailOverlay's constructor as an example). + if (Buttons.Any()) + Buttons.Last().TriggerClick(); + else + OnQuit?.Invoke(); + }; /// /// Action that is invoked when is triggered. diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 88561ada71..2aa2793fd4 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -29,7 +29,13 @@ namespace osu.Game.Screens.Play private SkinnableSound pauseLoop; - protected override Action BackAction => () => InternalButtons.First().TriggerClick(); + protected override Action BackAction => () => + { + if (Buttons.Any()) + Buttons.First().TriggerClick(); + else + OnResume?.Invoke(); + }; [BackgroundDependencyLoader] private void load(OsuColour colours) From f8347288c1a5cdc5ab6587a49e3980075eec4872 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 20:29:56 +0900 Subject: [PATCH 135/539] Add padding around text in dialogs --- .../UserInterface/TestSceneDialogOverlay.cs | 24 +++++++++++++++++++ osu.Game/Overlays/Dialog/PopupDialog.cs | 2 ++ 2 files changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs index 3b38343f04..f2313022ec 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs @@ -92,6 +92,30 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("first dialog is not part of hierarchy", () => firstDialog.Parent == null); } + [Test] + public void TestTooMuchText() + { + AddStep("dialog #1", () => overlay.Push(new TestPopupDialog + { + Icon = FontAwesome.Regular.TrashAlt, + HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of", + BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ", + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"I never want to see this again.", + Action = () => Console.WriteLine(@"OK"), + }, + new PopupDialogCancelButton + { + Text = @"Firetruck, I still want quick ranks!", + Action = () => Console.WriteLine(@"Cancel"), + }, + }, + })); + } + [Test] public void TestPushBeforeLoad() { diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 663c2e78ce..4048b35e78 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -210,6 +210,7 @@ namespace osu.Game.Overlays.Dialog RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = 5 }, }, body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18)) { @@ -218,6 +219,7 @@ namespace osu.Game.Overlays.Dialog TextAnchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 5 }, }, buttonsContainer = new FillFlowContainer { From 6bc0f02e2d46296fe2838cf97d57a3905d739928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Dec 2023 20:40:51 +0900 Subject: [PATCH 136/539] Fix test initialising dialogs without cleaning up --- .../Visual/UserInterface/TestScenePopupDialog.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 9537ab63be..37ff66aa35 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Game.Overlays.Dialog; @@ -15,18 +12,17 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestScenePopupDialog : OsuManualInputManagerTestScene { - private TestPopupDialog dialog; + private TestPopupDialog dialog = null!; [SetUpSteps] public void SetUpSteps() { AddStep("new popup", () => { - Add(dialog = new TestPopupDialog + Child = dialog = new TestPopupDialog { - RelativeSizeAxes = Axes.Both, State = { Value = Framework.Graphics.Containers.Visibility.Visible }, - }); + }; }); } From bb0737837b537fd7d97f276e48aaae871f0abc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 13:58:00 +0100 Subject: [PATCH 137/539] Fix test failing due to querying button position during transform --- osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 37ff66aa35..96d19911bd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDangerousButton([Values(false, true)] bool atEdge) { + AddStep("finish transforms", () => dialog.FinishTransforms(true)); + if (atEdge) { AddStep("move mouse to button edge", () => From e6f1d7db4446e9e0c2308b5d17c937c65a4f7a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 14:10:03 +0100 Subject: [PATCH 138/539] Move `SessionAverageHitErrorTracker` to more general namespace --- .../Audio => Configuration}/SessionAverageHitErrorTracker.cs | 3 +-- .../Settings/Sections/Audio/AudioOffsetAdjustControl.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/{Overlays/Settings/Sections/Audio => Configuration}/SessionAverageHitErrorTracker.cs (95%) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs similarity index 95% rename from osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs rename to osu.Game/Configuration/SessionAverageHitErrorTracker.cs index b714d49b89..13b1ea7d37 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/SessionAverageHitErrorTracker.cs +++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs @@ -5,12 +5,11 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -namespace osu.Game.Overlays.Settings.Sections.Audio +namespace osu.Game.Configuration { /// /// Tracks the local user's average hit error during the ongoing play session. diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 0023c60c61..e10de58a6c 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; From 6718de01ecc7397f3804954f0c01aaa81d1fffdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 14:11:08 +0100 Subject: [PATCH 139/539] Suggest audio adjust after one play --- .../Settings/Sections/Audio/AudioOffsetAdjustControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index e10de58a6c..98acea1f2e 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - suggestedOffset.Value = averageHitErrorHistory.Count < 3 ? null : -averageHitErrorHistory.Average(); + suggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average() : null; } private float getXPositionForAverage(double average) => (float)(Math.Clamp(-average, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); @@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { hintText.Text = suggestedOffset.Value == null ? @"Play a few beatmaps to receive a suggested offset!" - : $@"Based on the last {averageHitErrorHistory.Count} plays, the suggested offset is {suggestedOffset.Value:N0} ms."; + : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {suggestedOffset.Value:N0} ms."; applySuggestion.Enabled.Value = suggestedOffset.Value != null; } } From 619b0cc69b1998fdc8b1b1fcb69bcddaefc08004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 14:12:28 +0100 Subject: [PATCH 140/539] Fix code quality inspection --- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 8c6f1a8f7f..8efb80e771 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -160,11 +160,11 @@ namespace osu.Game.Screens.Play.PlayerSettings // Apply to all difficulties in a beatmap set for now (they generally always share timing). foreach (var b in setInfo.Beatmaps) { - BeatmapUserSettings settings = b.UserSettings; + BeatmapUserSettings userSettings = b.UserSettings; double val = Current.Value; - if (settings.Offset != val) - settings.Offset = val; + if (userSettings.Offset != val) + userSettings.Offset = val; } }); } From 6d124513e7d88aba2849b5494783820eeb476777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 14:15:15 +0100 Subject: [PATCH 141/539] Fix test failures due to player bailing early after failing to load beatmap --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index ff2c57bf35..83adf1f960 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play { bool exiting = base.OnExiting(e); submitFromFailOrQuit(); - statics.SetValue(Static.LastLocalUserScore, Score.ScoreInfo.DeepClone()); + statics.SetValue(Static.LastLocalUserScore, Score?.ScoreInfo.DeepClone()); return exiting; } From 93c7ebdae3f61e95e36cc61c014d71fb67e8a3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 14:30:11 +0100 Subject: [PATCH 142/539] Remove unused using --- osu.Game/OsuGameBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 90c88b2b33..4e465f59df 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -55,7 +55,6 @@ using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; -using osu.Game.Overlays.Settings.Sections.Audio; using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Resources; using osu.Game.Rulesets; From 68402bfd113b3c4051aead283fe3cf57d5b9ca78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 20:32:33 +0100 Subject: [PATCH 143/539] Add test coverage of failure scenario --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 85aa14f33e..46570e7cdf 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -24,6 +24,11 @@ namespace osu.Game.Tests.Visual.Menus Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", Url = @"https://osu.ppy.sh/community/contests/189", }); + AddStep("set title with nonexistent image", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + }); AddStep("unset system title", () => Game.ChildrenOfType().Single().Current.Value = null); } } From 7a10e132eaea162febb65d03baa16067a1244e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Dec 2023 20:33:56 +0100 Subject: [PATCH 144/539] Fix crash when retrieval of system title image fails Closes https://github.com/ppy/osu/issues/26194. --- osu.Game/Screens/Menu/SystemTitle.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 060e426f8c..7d49a1dd5f 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -138,8 +138,8 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(LargeTextureStore textureStore) { - var texture = textureStore.Get(SystemTitle.Image); - if (SystemTitle.Image.Contains(@"@2x")) + Texture? texture = textureStore.Get(SystemTitle.Image); + if (texture != null && SystemTitle.Image.Contains(@"@2x")) texture.ScaleAdjust *= 2; AutoSizeAxes = Axes.Both; From 637119f7d4ae7142d9cef29e8a2cd22000c95fb1 Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Thu, 28 Dec 2023 17:15:23 -0600 Subject: [PATCH 145/539] increase the base size of button icons --- osu.Game/Screens/Menu/MainMenuButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index bc638b44ac..a9927541f1 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Menu Shadow = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(30), + Size = new Vector2(32), Position = new Vector2(0, 0), Icon = symbol }, From 4760c6aaee2bef3a47329158c612d9ea231d4e3b Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Thu, 28 Dec 2023 17:17:24 -0600 Subject: [PATCH 146/539] move icon upwards to be visually centered --- osu.Game/Screens/Menu/MainMenuButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index a9927541f1..87136675a2 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(32), - Position = new Vector2(0, 0), + Position = new Vector2(0, -4), Icon = symbol }, new OsuSpriteText @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Menu { icon.ClearTransforms(); icon.RotateTo(0, 500, Easing.Out); - icon.MoveTo(Vector2.Zero, 500, Easing.Out); + icon.MoveTo(new Vector2(0, -4), 500, Easing.Out); icon.ScaleTo(Vector2.One, 200, Easing.Out); if (State == ButtonState.Expanded) From f1f1221e0e4f5e83f89b74df1c6e1b1c3ce4e04d Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Thu, 28 Dec 2023 17:18:41 -0600 Subject: [PATCH 147/539] move text left to be visually centered in the skewed rectangle --- osu.Game/Screens/Menu/MainMenuButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 87136675a2..69dbaa8ed8 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Menu AllowMultiline = false, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(0, 35), + Position = new Vector2(-3, 35), Text = text } } From 51d26d2d715f463a13b47d4692daac3d13a9319d Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Thu, 28 Dec 2023 17:20:44 -0600 Subject: [PATCH 148/539] make the hover scale bigger as a consequence, the rotation needs to be tweaked to be lower --- osu.Game/Screens/Menu/MainMenuButton.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 69dbaa8ed8..3d56a75ca8 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Menu /// public partial class MainMenuButton : BeatSyncedContainer, IStateful { + public const float BOUNCE_COMPRESSION = 0.9f; + public const float HOVER_SCALE = 1.2f; + public const float BOUNCE_ROTATION = 8; public event Action? StateChanged; public readonly Key[] TriggerKeys; @@ -153,14 +156,14 @@ namespace osu.Game.Screens.Menu double duration = timingPoint.BeatLength / 2; - icon.RotateTo(rightward ? 10 : -10, duration * 2, Easing.InOutSine); + icon.RotateTo(rightward ? BOUNCE_ROTATION : -BOUNCE_ROTATION, duration * 2, Easing.InOutSine); icon.Animate( i => i.MoveToY(-10, duration, Easing.Out), - i => i.ScaleTo(1, duration, Easing.Out) + i => i.ScaleTo(HOVER_SCALE, duration, Easing.Out) ).Then( i => i.MoveToY(0, duration, Easing.In), - i => i.ScaleTo(new Vector2(1, 0.9f), duration, Easing.In) + i => i.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.In) ); rightward = !rightward; @@ -177,8 +180,8 @@ namespace osu.Game.Screens.Menu double duration = TimeUntilNextBeat; icon.ClearTransforms(); - icon.RotateTo(rightward ? -10 : 10, duration, Easing.InOutSine); - icon.ScaleTo(new Vector2(1, 0.9f), duration, Easing.Out); + icon.RotateTo(rightward ? -BOUNCE_ROTATION : BOUNCE_ROTATION, duration, Easing.InOutSine); + icon.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.Out); return true; } From c68a850325e36b8585b2d60f25be81deef52565a Mon Sep 17 00:00:00 2001 From: Nitrous Date: Fri, 29 Dec 2023 08:51:54 +0800 Subject: [PATCH 149/539] Make menu tip about mod select more up to date. --- osu.Game/Screens/Menu/MenuTip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index 325fb07767..ab09c07e64 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", - "Try scrolling down in the mod select panel to find a bunch of new fun mods!", + "Try scrolling right in mod select to find a bunch of new fun mods!", "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", From 150bf670649e61ac071796fa4056aa372ff9023b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 29 Dec 2023 05:30:50 +0300 Subject: [PATCH 150/539] Fix dropdown colour not updating correctly on enabled state changes --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 632036fef9..c99a21ac28 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -363,6 +363,7 @@ namespace osu.Game.Graphics.UserInterface base.LoadComplete(); SearchBar.State.ValueChanged += _ => updateColour(); + Enabled.BindValueChanged(_ => updateColour()); updateColour(); } From c147ec0a98b22bec0a4635e1fc65cc32c5b34ddf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 29 Dec 2023 05:31:13 +0300 Subject: [PATCH 151/539] Update dropdown disabled state to match with other components --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index c99a21ac28..2dc701dc9d 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -384,6 +384,9 @@ namespace osu.Game.Graphics.UserInterface var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker; var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + Colour = Color4.White; + Alpha = Enabled.Value ? 1 : 0.3f; + if (SearchBar.State.Value == Visibility.Visible) { Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; From a6313c4ee8b78361d052304a3469fc9bf229edfb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 29 Dec 2023 17:16:16 +0900 Subject: [PATCH 152/539] Expose `Mod.UsesDefaultConfiguration` --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index d635dccab1..0500b49513 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Mods /// /// Whether all settings in this mod are set to their default state. /// - protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); + public virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); /// /// Creates a copy of this initialised to a default state. From 9bb0663b3bb3b7f717a5cedbcd62a0051062eacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 10:36:52 +0100 Subject: [PATCH 153/539] Add test coverage of failure case --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 7a2ed23cae..db87987815 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; using osu.Game.Tests.Resources; @@ -94,8 +95,11 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestAddAudioTrack() { - AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); + AddStep("enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); AddAssert("switch track to real track", () => { var setup = Editor.ChildrenOfType().First(); From cd1f6b46c40639443c49cb03bc84289fcdd47f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 10:24:48 +0100 Subject: [PATCH 154/539] Fix crash after changing audio track in editor Closes https://github.com/ppy/osu/issues/26213. Reproduction scenario: switch audio track in editor after timeline loads. Happens because `beatmap.Value.Track.Length` is 0 immediately after a track switch, until BASS computes the actual track length on the audio thread. Yes this is a hack. No I have no better immediate ideas how to address this otherwise. --- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 83d34ab61a..03a868b0f3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -141,12 +141,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); track.BindTo(editorClock.Track); - track.BindValueChanged(_ => + // schedule added as without it, `beatmap.Value.Track.Length` can be 0 immediately after a track switch. + track.BindValueChanged(_ => Schedule(() => { waveform.Waveform = beatmap.Value.Waveform; waveform.RelativePositionAxes = Axes.X; waveform.X = -(float)(Editor.WAVEFORM_VISUAL_OFFSET / beatmap.Value.Track.Length); - }, true); + }), true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); } From 99cddb631702f2aebe23056424ae86428369a5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 11:07:45 +0100 Subject: [PATCH 155/539] Use alternative workaround --- .../Compose/Components/Timeline/Timeline.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 03a868b0f3..a2704e550c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -141,17 +141,29 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); track.BindTo(editorClock.Track); - // schedule added as without it, `beatmap.Value.Track.Length` can be 0 immediately after a track switch. - track.BindValueChanged(_ => Schedule(() => + track.BindValueChanged(_ => { waveform.Waveform = beatmap.Value.Waveform; - waveform.RelativePositionAxes = Axes.X; - waveform.X = -(float)(Editor.WAVEFORM_VISUAL_OFFSET / beatmap.Value.Track.Length); - }), true); + Scheduler.AddOnce(applyVisualOffset, beatmap); + }, true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); } + private void applyVisualOffset(IBindable beatmap) + { + waveform.RelativePositionAxes = Axes.X; + + if (beatmap.Value.Track.Length > 0) + waveform.X = -(float)(Editor.WAVEFORM_VISUAL_OFFSET / beatmap.Value.Track.Length); + else + { + // sometimes this can be the case immediately after a track switch. + // reschedule with the hope that the track length eventually populates. + Scheduler.AddOnce(applyVisualOffset, beatmap); + } + } + protected override void LoadComplete() { base.LoadComplete(); From 25fa76a1b2bbb8d96794466e28024842aff9e71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 11:14:28 +0100 Subject: [PATCH 156/539] Do not show main menu version display on deployed builds See https://discord.com/channels/188630481301012481/188630652340404224/1190028102525530202. --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 37996e8832..0d6eb1ea44 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -994,7 +994,7 @@ namespace osu.Game Margin = new MarginPadding(5), }, topMostOverlayContent.Add); - if (!args?.Any(a => a == @"--no-version-overlay") ?? true) + if (!IsDeployedBuild) { dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); loadComponentSingleFile(versionManager, ScreenContainer.Add); From db78d73fa511b647e45286e805a6ae7784ee4fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 11:50:30 +0100 Subject: [PATCH 157/539] Do not display system title in inital menu state Addresses https://github.com/ppy/osu/discussions/26199. --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 15 +++++++++++---- osu.Game/Screens/Menu/MainMenu.cs | 5 ++++- osu.Game/Screens/Menu/SystemTitle.cs | 12 +++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 46570e7cdf..7053a9d544 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -3,33 +3,40 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Menu; +using osuTK.Input; namespace osu.Game.Tests.Visual.Menus { public partial class TestSceneMainMenu : OsuGameTestScene { + private SystemTitle systemTitle => Game.ChildrenOfType().Single(); + [Test] public void TestSystemTitle() { - AddStep("set system title", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + AddStep("set system title", () => systemTitle.Current.Value = new APISystemTitle { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", }); - AddStep("set another title", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + AddAssert("system title not visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("system title visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("set another title", () => systemTitle.Current.Value = new APISystemTitle { Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", Url = @"https://osu.ppy.sh/community/contests/189", }); - AddStep("set title with nonexistent image", () => Game.ChildrenOfType().Single().Current.Value = new APISystemTitle + AddStep("set title with nonexistent image", () => systemTitle.Current.Value = new APISystemTitle { Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 Url = @"https://osu.ppy.sh/community/contests/189", }); - AddStep("unset system title", () => Game.ChildrenOfType().Single().Current.Value = null); + AddStep("unset system title", () => systemTitle.Current.Value = null); } } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 401460a498..516b090a16 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; + private SystemTitle systemTitle; private MenuTip menuTip; private FillFlowContainer bottomElementsFlow; private SupporterDisplay supporterDisplay; @@ -173,7 +174,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - new SystemTitle + systemTitle = new SystemTitle { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -196,10 +197,12 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Initial: case ButtonSystemState.Exit: ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); + systemTitle.State.Value = Visibility.Hidden; break; default: ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); + systemTitle.State.Value = Visibility.Visible; break; } }; diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs index 7d49a1dd5f..813a470ed6 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/SystemTitle.cs @@ -18,10 +18,12 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Screens.Menu { - public partial class SystemTitle : CompositeDrawable + public partial class SystemTitle : VisibilityContainer { internal Bindable Current { get; } = new Bindable(); + private const float transition_duration = 500; + private Container content = null!; private CancellationTokenSource? cancellationTokenSource; private SystemTitleImage? currentImage; @@ -32,9 +34,13 @@ namespace osu.Game.Screens.Menu private void load(OsuGame? game) { AutoSizeAxes = Axes.Both; + AutoSizeDuration = transition_duration; + AutoSizeEasing = Easing.OutQuint; InternalChild = content = new OsuClickableContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Action = () => { @@ -51,6 +57,10 @@ namespace osu.Game.Screens.Menu }; } + protected override void PopIn() => content.FadeInFromZero(transition_duration, Easing.OutQuint); + + protected override void PopOut() => content.FadeOut(transition_duration, Easing.OutQuint); + protected override bool OnHover(HoverEvent e) { content.ScaleTo(1.05f, 2000, Easing.OutQuint); From cdcb3ef4aa7401243d6a97e151d9cc41cd284060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 12:20:14 +0100 Subject: [PATCH 158/539] Fix global audio offset suggestion feature not taking previous offset value into account See https://osu.ppy.sh/comments/2989193 etc. --- .../SessionAverageHitErrorTracker.cs | 25 ++++++++++++++++--- .../Audio/AudioOffsetAdjustControl.cs | 14 +++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs index 13b1ea7d37..cd21eb6fa8 100644 --- a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs +++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs @@ -17,11 +17,14 @@ namespace osu.Game.Configuration [Cached] public partial class SessionAverageHitErrorTracker : Component { - public IBindableList AverageHitErrorHistory => averageHitErrorHistory; - private readonly BindableList averageHitErrorHistory = new BindableList(); + public IBindableList AverageHitErrorHistory => averageHitErrorHistory; + private readonly BindableList averageHitErrorHistory = new BindableList(); private readonly Bindable latestScore = new Bindable(); + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [BackgroundDependencyLoader] private void load(SessionStatics statics) { @@ -46,9 +49,25 @@ namespace osu.Game.Configuration // keep a sane maximum number of entries. if (averageHitErrorHistory.Count >= 50) averageHitErrorHistory.RemoveAt(0); - averageHitErrorHistory.Add(averageError); + + double globalOffset = configManager.Get(OsuSetting.AudioOffset); + averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset)); } public void ClearHistory() => averageHitErrorHistory.Clear(); + + public readonly struct DataPoint + { + public double AverageHitError { get; } + public double GlobalAudioOffset { get; } + + public double SuggestedGlobalAudioOffset => GlobalAudioOffset - AverageHitError; + + public DataPoint(double averageHitError, double globalOffset) + { + AverageHitError = averageHitError; + GlobalAudioOffset = globalOffset; + } + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 98acea1f2e..08bf4b0dad 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); - private readonly IBindableList averageHitErrorHistory = new BindableList(); + private readonly IBindableList averageHitErrorHistory = new BindableList(); private readonly Bindable suggestedOffset = new Bindable(); @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (double average in e.NewItems!) + foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.NewItems!) { notchContainer.ForEach(n => n.Alpha *= 0.95f); notchContainer.Add(new Box @@ -122,16 +122,16 @@ namespace osu.Game.Overlays.Settings.Sections.Audio RelativePositionAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, - X = getXPositionForAverage(average) + X = getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset) }); } break; case NotifyCollectionChangedAction.Remove: - foreach (double average in e.OldItems!) + foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.OldItems!) { - var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForAverage(average)); + var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset)); Debug.Assert(notch != null); notchContainer.Remove(notch, true); } @@ -143,10 +143,10 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - suggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average() : null; + suggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; } - private float getXPositionForAverage(double average) => (float)(Math.Clamp(-average, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); + private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); private void updateHintText() { From 17753b82356c4e4f51fb6e55dbff4e8b604c6ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Dec 2023 13:48:49 +0100 Subject: [PATCH 159/539] Remove opaque background from toolbar user button Would close https://github.com/ppy/osu/issues/26223, and generally seems more consistent with the rest of toolbar anyhow? --- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 7d1b6c7404..19b98628e6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -40,8 +40,6 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login) { - BackgroundContent.Add(new OpaqueBackground { Depth = 1 }); - Flow.Add(new Container { Masking = true, From d00b7c9cdf23b204f4843523dbd7a5ae0883faaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Dec 2023 22:14:51 +0900 Subject: [PATCH 160/539] Add some new menu tips (and reword some others) --- osu.Game/Screens/Menu/MenuTip.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index ab09c07e64..e31281ea20 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -94,14 +94,14 @@ namespace osu.Game.Screens.Menu { string[] tips = { - "You can press Ctrl-T anywhere in the game to toggle the toolbar!", - "You can press Ctrl-O anywhere in the game to access options!", + "Press Ctrl-T anywhere in the game to toggle the toolbar!", + "Press Ctrl-O anywhere in the game to access options!", "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", "New features are coming online every update. Make sure to stay up-to-date!", "If you find the UI too large or small, try adjusting UI scale in settings!", "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", + "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen or by using the left and right arrow keys!", "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", "Try scrolling right in mod select to find a bunch of new fun mods!", "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", @@ -110,6 +110,18 @@ namespace osu.Game.Screens.Menu "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", "Toggle advanced frame / thread statistics with Ctrl-F11!", "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", + "You can pause during a replay by pressing Space!", + "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", + "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", + "Your gameplay HUD can be customized by using the skin layout editor. Open at any time via Ctrl-Shift-S!", + "Drag and drop any image into the skin editor to load it in quickly!", + "You can create mod presets to make toggling your favorite mod combinations easier!", + "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", + "Press Ctrl-Shift-R to switch to a random skin!", + "Press Ctrl-Shift-F to toggle the FPS Counter. But make sure not to pay too much attention to it!", + "While watching a reply replay, press Ctrl-H to toggle replay settings!", + "You can easily copy the mods from scores on a leaderboard by right-clicking on them!", + "Ctrl-Enter at song select will start a beatmap in autoplay mode!" }; return tips[RNG.Next(0, tips.Length)]; From 9f0fe6c6cab17dc1395e6874e57bf29a58b39a8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Dec 2023 22:20:29 +0900 Subject: [PATCH 161/539] Fix common typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/Menu/MenuTip.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index e31281ea20..0f250768f1 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -113,13 +113,13 @@ namespace osu.Game.Screens.Menu "You can pause during a replay by pressing Space!", "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", - "Your gameplay HUD can be customized by using the skin layout editor. Open at any time via Ctrl-Shift-S!", + "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!", "Drag and drop any image into the skin editor to load it in quickly!", "You can create mod presets to make toggling your favorite mod combinations easier!", "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", "Press Ctrl-Shift-R to switch to a random skin!", "Press Ctrl-Shift-F to toggle the FPS Counter. But make sure not to pay too much attention to it!", - "While watching a reply replay, press Ctrl-H to toggle replay settings!", + "While watching a replay, press Ctrl-H to toggle replay settings!", "You can easily copy the mods from scores on a leaderboard by right-clicking on them!", "Ctrl-Enter at song select will start a beatmap in autoplay mode!" }; From 61c46b78bd380189944107782533915f9ec37995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Dec 2023 22:24:16 +0900 Subject: [PATCH 162/539] Update MenuTip.cs Co-authored-by: Salman Ahmed --- osu.Game/Screens/Menu/MenuTip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index 0f250768f1..da349373c3 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Menu "If you find the UI too large or small, try adjusting UI scale in settings!", "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen or by using the left and right arrow keys!", + "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!", "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", "Try scrolling right in mod select to find a bunch of new fun mods!", "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", From 8df9a1ee1f8b8253e1438909f245aa709bd0be46 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 29 Dec 2023 20:43:00 +0300 Subject: [PATCH 163/539] Fix argon miss judgement on mania placed on a different position --- osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 4ce3c50f7c..6b1d8a7fcd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -76,7 +76,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveTo(Vector2.Zero); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); From 08d88ec2fa94b8d6e855246e40d7eb23e632773a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 29 Dec 2023 20:47:18 +0300 Subject: [PATCH 164/539] Add back initial position transform to ensure correctness --- .../Skinning/Argon/ArgonJudgementPiece.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 6b1d8a7fcd..a191dee1ca 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement { + private const float judgement_y_position = 160; + private RingExplosion? ringExplosion; [Resolved] @@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = 160; + Y = judgement_y_position; } [BackgroundDependencyLoader] @@ -76,6 +78,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); + this.MoveToY(judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); From 807443b6481223fc14574b6c0829e4f197dee6cb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Dec 2023 10:21:59 +0900 Subject: [PATCH 165/539] Add HitResult.SliderTailHit --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 8 ++++++++ osu.Game/Rulesets/Judgements/Judgement.cs | 2 ++ osu.Game/Rulesets/Scoring/HitResult.cs | 15 +++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +++ 4 files changed, 28 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index d883b91abc..567bf6968f 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -84,6 +84,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)] + [TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 326_963)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] @@ -96,6 +97,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)] + [TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 32_696)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) @@ -167,6 +169,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.Perfect, HitResult.Miss)] [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)] [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)] + [TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)] [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)] [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) @@ -187,6 +190,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue) @@ -207,6 +211,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue) @@ -227,6 +232,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, false)] + [TestCase(HitResult.SliderTailHit, false)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsBonus(HitResult hitResult, bool expectedReturnValue) @@ -247,6 +253,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsHit(HitResult hitResult, bool expectedReturnValue) @@ -267,6 +274,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 27402d522c..93386de483 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Judgements return HitResult.SmallTickMiss; case HitResult.LargeTickHit: + case HitResult.SliderTailHit: return HitResult.LargeTickMiss; default: @@ -104,6 +105,7 @@ namespace osu.Game.Rulesets.Judgements case HitResult.SmallTickMiss: return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: return DEFAULT_MAX_HEALTH_INCREASE; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 2d55f1a649..f307344347 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -139,6 +139,13 @@ namespace osu.Game.Rulesets.Scoring [Order(15)] ComboBreak, + /// + /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. + /// + [EnumMember(Value = "slider_tail_hit")] + [Order(16)] + SliderTailHit, + /// /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// @@ -188,6 +195,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: case HitResult.ComboBreak: + case HitResult.SliderTailHit: return true; default: @@ -246,6 +254,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickMiss: case HitResult.SmallTickHit: case HitResult.SmallTickMiss: + case HitResult.SliderTailHit: return true; default: @@ -329,6 +338,9 @@ namespace osu.Game.Rulesets.Scoring case HitResult.ComboBreak: return true; + case HitResult.SliderTailHit: + return true; + default: // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score. return result >= HitResult.Miss && result < HitResult.IgnoreMiss; @@ -383,6 +395,9 @@ namespace osu.Game.Rulesets.Scoring if (minResult == HitResult.IgnoreMiss) return; + if (maxResult == HitResult.SliderTailHit && minResult != HitResult.LargeTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); + if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5123668e54..837bb4080e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -322,6 +322,9 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickHit: return 30; + case HitResult.SliderTailHit: + return 150; + case HitResult.Meh: return 50; From 17a531209cf4ca6d2e2386f736779ad084a9fa44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Dec 2023 10:24:59 +0900 Subject: [PATCH 166/539] Use SliderTailHit result for slider tails (non-classic-mod) --- .../TestSceneSliderEarlyHitJudgement.cs | 2 +- .../TestSceneSliderInput.cs | 4 ++-- .../TestSceneSliderLateHitJudgement.cs | 14 +++++++------- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 2 +- .../Scoring/OsuHealthProcessor.cs | 1 + .../Scoring/OsuLegacyHealthProcessor.cs | 1 + 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs index 4ea21e51f6..19883060a0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 716d2e0756..12be74c4cc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -467,13 +467,13 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertHeadMissTailTracked() { - AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False); } private void assertMidSliderJudgements() { - AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); } private void assertMidSliderJudgementFail() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs index 6ba9c723da..1ba4a60b75 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); assertHeadJudgement(HitResult.Ok); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertTickJudgement(1, HitResult.LargeTickHit); assertTickJudgement(2, HitResult.LargeTickHit); assertTickJudgement(3, HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertAllTickJudgements(HitResult.LargeTickHit); assertRepeatJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertRepeatJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertAllTickJudgements(HitResult.LargeTickMiss); // This particular test actually starts tracking the slider just before the end, so the tail should be hit because of its leniency. - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -276,7 +276,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(0, HitResult.LargeTickMiss); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(0, HitResult.LargeTickMiss); assertTickJudgement(1, HitResult.LargeTickMiss); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 357476ed30..ceee513412 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class TailJudgement : SliderEndJudgement { - public override HitResult MaxResult => HitResult.LargeTickHit; + public override HitResult MaxResult => HitResult.SliderTailHit; public override HitResult MinResult => HitResult.IgnoreMiss; } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 2eb257b3e6..fe6da9af35 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Scoring // When classic slider mechanics are enabled, this result comes from the tail. return 0.02; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: switch (result.HitObject) { diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs index e383e82b86..57d2f64e2c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.Scoring increase = 0.02; break; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: // This result comes from either a slider tick or repeat. increase = hitObject is SliderTick ? 0.015 : 0.02; From bca060048266e1b124699def72b8f86e00b79c99 Mon Sep 17 00:00:00 2001 From: CaffeeLake Date: Wed, 27 Dec 2023 22:11:54 +0900 Subject: [PATCH 167/539] Use 0.99x or 1.01x Signed-off-by: CaffeeLake --- osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs | 10 ++++++++-- osu.Game/Screens/Select/FooterButtonMods.cs | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs index c758632392..85bcf6f0b2 100644 --- a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -108,13 +109,13 @@ namespace osu.Game.Overlays.Mods Current.BindValueChanged(e => { - if (e.NewValue > Current.Default) + if (Precision.DefinitelyBigger(e.NewValue, Current.Default)) { MainBackground .FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint); counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint); } - else if (e.NewValue < Current.Default) + else if (Precision.DefinitelyBigger(Current.Default, e.NewValue)) { MainBackground .FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint); @@ -131,6 +132,11 @@ namespace osu.Game.Overlays.Mods .FadeTo(0.15f, 60, Easing.OutQuint) .Then().FadeOut(500, Easing.OutQuint); + if (Precision.DefinitelyBigger(1.0, Current.Value) && Current.Value >= 0.995) + Current.Value = 0.99; + if (Precision.DefinitelyBigger(Current.Value, 1.0) && Current.Value < 1.005) + Current.Value = 1.01; + const float move_amount = 4; if (e.NewValue > e.OldValue) counter.MoveToY(Math.Max(-move_amount * 2, counter.Y - move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint); diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 9a84f9a0aa..cfbd17be01 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; using osu.Game.Input.Bindings; +using osu.Framework.Utils; namespace osu.Game.Screens.Select { @@ -88,11 +89,16 @@ namespace osu.Game.Screens.Select { double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; - MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + if (Precision.DefinitelyBigger(1.0, multiplier) && multiplier >= 0.995) + MultiplierText.Text = $"{0.99:N2}x"; + else if (Precision.DefinitelyBigger(multiplier, 1.0) && multiplier < 1.005) + MultiplierText.Text = $"{1.01:N2}x"; + else + MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - if (multiplier > 1.0) + if (Precision.DefinitelyBigger(multiplier, 1.0)) MultiplierText.FadeColour(highMultiplierColour, 200); - else if (multiplier < 1.0) + else if (Precision.DefinitelyBigger(1.0, multiplier)) MultiplierText.FadeColour(lowMultiplierColour, 200); else MultiplierText.FadeColour(Color4.White, 200); From cc89390ea89e6b1f8127adef2717e4d6438b7b46 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 21:33:03 +0300 Subject: [PATCH 168/539] Expose `SuggestedOffset` bindable for testing purposes --- .../Sections/Audio/AudioOffsetAdjustControl.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 08bf4b0dad..e46dc602eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { public partial class AudioOffsetAdjustControl : SettingsItem { + public IBindable SuggestedOffset => ((AudioOffsetPreview)Control).SuggestedOffset; + [BackgroundDependencyLoader] private void load() { @@ -44,7 +46,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private readonly IBindableList averageHitErrorHistory = new BindableList(); - private readonly Bindable suggestedOffset = new Bindable(); + public readonly Bindable SuggestedOffset = new Bindable(); private Container notchContainer = null!; private TextFlowContainer hintText = null!; @@ -90,8 +92,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Text = "Apply suggested offset", Action = () => { - if (suggestedOffset.Value.HasValue) - current.Value = suggestedOffset.Value.Value; + if (SuggestedOffset.Value.HasValue) + current.Value = SuggestedOffset.Value.Value; hitErrorTracker.ClearHistory(); } } @@ -104,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio base.LoadComplete(); averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); - suggestedOffset.BindValueChanged(_ => updateHintText(), true); + SuggestedOffset.BindValueChanged(_ => updateHintText(), true); } private void updateDisplay(object? _, NotifyCollectionChangedEventArgs e) @@ -143,17 +145,17 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - suggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; + SuggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; } private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); private void updateHintText() { - hintText.Text = suggestedOffset.Value == null + hintText.Text = SuggestedOffset.Value == null ? @"Play a few beatmaps to receive a suggested offset!" - : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {suggestedOffset.Value:N0} ms."; - applySuggestion.Enabled.Value = suggestedOffset.Value != null; + : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms."; + applySuggestion.Enabled.Value = SuggestedOffset.Value != null; } } } From 68dd103c89e25619bf4aea6aea65e61d369396d6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 21:33:22 +0300 Subject: [PATCH 169/539] Add failing test cases --- .../TestSceneAudioOffsetAdjustControl.cs | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs index efb65bb0a8..85cde966b1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -25,9 +25,15 @@ namespace osu.Game.Tests.Visual.Settings private Container content = null!; protected override Container Content => content; + private OsuConfigManager localConfig = null!; + private AudioOffsetAdjustControl adjustControl = null!; + [BackgroundDependencyLoader] private void load() { + localConfig = new OsuConfigManager(LocalStorage); + Dependencies.CacheAs(localConfig); + base.Content.AddRange(new Drawable[] { tracker, @@ -41,17 +47,21 @@ namespace osu.Game.Tests.Visual.Settings }); } - [Test] - public void TestBehaviour() + [SetUp] + public void SetUp() => Schedule(() => { - AddStep("create control", () => Child = new AudioOffsetAdjustControl + Child = adjustControl = new AudioOffsetAdjustControl { - Current = new BindableDouble - { - MinValue = -500, - MaxValue = 500 - } - }); + Current = localConfig.GetBindable(OsuSetting.AudioOffset), + }; + + localConfig.SetValue(OsuSetting.AudioOffset, 0.0); + tracker.ClearHistory(); + }); + + [Test] + public void TestDisplay() + { AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo { HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)), @@ -59,5 +69,63 @@ namespace osu.Game.Tests.Visual.Settings })); AddStep("clear history", () => tracker.ClearHistory()); } + + [Test] + public void TestBehaviour() + { + AddStep("set score with -20ms", () => setScore(-20)); + AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set score with 40ms", () => setScore(40)); + AddAssert("suggested global offset is -40ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-40)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + [Test] + public void TestNonZeroGlobalOffset() + { + AddStep("set global offset to -20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, -20.0)); + AddStep("set score with -20ms", () => setScore(-20)); + AddAssert("suggested global offset is 0ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(0)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set global offset to 20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 20.0)); + AddStep("set score with 40ms", () => setScore(40)); + AddAssert("suggested global offset is -20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-20)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + [Test] + public void TestMultiplePlays() + { + AddStep("set score with -20ms", () => setScore(-20)); + AddStep("set score with -10ms", () => setScore(-10)); + AddAssert("suggested global offset is 15ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(15)); + AddStep("clear history", () => tracker.ClearHistory()); + + AddStep("set score with -20ms", () => setScore(-20)); + AddStep("set global offset to 30ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 30.0)); + AddStep("set score with 10ms", () => setScore(10)); + AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20)); + AddStep("clear history", () => tracker.ClearHistory()); + } + + private void setScore(double averageHitError) + { + statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(averageHitError), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }); + } + + protected override void Dispose(bool isDisposing) + { + if (localConfig.IsNotNull()) + localConfig.Dispose(); + + base.Dispose(isDisposing); + } } } From e6fe631625643a23b18641a132c6e67c1062b9ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 21:34:37 +0300 Subject: [PATCH 170/539] Fix suggested value in audio offset adjust control being opposite in signs --- .../Settings/Sections/Audio/AudioOffsetAdjustControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index e46dc602eb..90f5a59215 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - SuggestedOffset.Value = averageHitErrorHistory.Any() ? -averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; + SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; } private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); From 452f201f0673426df7e88b78f720293863e8d660 Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Sat, 30 Dec 2023 12:56:38 -0600 Subject: [PATCH 171/539] use margins isntead of moving the position of the sprite --- osu.Game/Screens/Menu/MainMenuButton.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 3d56a75ca8..422599a4a8 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -129,7 +129,8 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(32), - Position = new Vector2(0, -4), + Position = new Vector2(0, 0), + Margin = new MarginPadding { Top = -4 }, Icon = symbol }, new OsuSpriteText @@ -138,7 +139,8 @@ namespace osu.Game.Screens.Menu AllowMultiline = false, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(-3, 35), + Position = new Vector2(0, 35), + Margin = new MarginPadding { Left = -3 }, Text = text } } @@ -189,7 +191,7 @@ namespace osu.Game.Screens.Menu { icon.ClearTransforms(); icon.RotateTo(0, 500, Easing.Out); - icon.MoveTo(new Vector2(0, -4), 500, Easing.Out); + icon.MoveTo(Vector2.Zero, 500, Easing.Out); icon.ScaleTo(Vector2.One, 200, Easing.Out); if (State == ButtonState.Expanded) From 535177ab97c06c291aaba6860b0499096413c708 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 30 Dec 2023 11:40:40 -0800 Subject: [PATCH 172/539] Remove outdated main menu version reference on issue template --- .github/ISSUE_TEMPLATE/bug-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index dfdcf8d320..a8a5d5e64b 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -42,7 +42,7 @@ body: - type: input attributes: label: Version - description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen. + description: The version you encountered this bug on. This is shown at the end of the settings overlay. validations: required: true - type: markdown From 922b6ccb83b0e83d9b2be6f63ee7e8366e500c73 Mon Sep 17 00:00:00 2001 From: Gabriel Del Nero <43073074+Gabixel@users.noreply.github.com> Date: Sun, 31 Dec 2023 00:36:55 +0100 Subject: [PATCH 173/539] Use `FontAwesome` solid heart icon instead of `OsuIcon`'s --- osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 08a816930e..85751e7457 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Profile.Header bool anyInfoAdded = false; anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location); - anyInfoAdded |= tryAddInfo(OsuIcon.Heart, user.Interests); + anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation); if (anyInfoAdded) From 7cfb786b1a1dae5ef00c5d0359de336c4725c757 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Dec 2023 04:48:06 +0300 Subject: [PATCH 174/539] Add helper method for properly formatting score multiplier in `ModUtils` --- osu.Game/Utils/ModUtils.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 1bd60fcdde..dad8c72aa1 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -226,5 +228,21 @@ namespace osu.Game.Utils return proposedWereValid; } + + /// + /// Given a value of a score multiplier, returns a string version with special handling for a value near 1.00x. + /// + /// The value of the score multiplier. + /// A formatted score multiplier with a trailing "x" symbol + public static LocalisableString FormatScoreMultiplier(double scoreMultiplier) + { + // Round multiplier values away from 1.00x to two significant digits. + if (scoreMultiplier > 1) + scoreMultiplier = Math.Ceiling(scoreMultiplier * 100) / 100; + else + scoreMultiplier = Math.Floor(scoreMultiplier * 100) / 100; + + return scoreMultiplier.ToLocalisableString("0.00x"); + } } } From 4dc11c4c48d49104a2d5e504a58441f1752e6d7b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Dec 2023 05:18:07 +0300 Subject: [PATCH 175/539] Update existing code to use helper method --- osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs | 14 ++++---------- osu.Game/Screens/Select/FooterButtonMods.cs | 14 ++++---------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs index 85bcf6f0b2..b599b53082 100644 --- a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs @@ -4,18 +4,17 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.Mods @@ -109,13 +108,13 @@ namespace osu.Game.Overlays.Mods Current.BindValueChanged(e => { - if (Precision.DefinitelyBigger(e.NewValue, Current.Default)) + if (e.NewValue > Current.Default) { MainBackground .FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint); counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint); } - else if (Precision.DefinitelyBigger(Current.Default, e.NewValue)) + else if (e.NewValue < Current.Default) { MainBackground .FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint); @@ -132,11 +131,6 @@ namespace osu.Game.Overlays.Mods .FadeTo(0.15f, 60, Easing.OutQuint) .Then().FadeOut(500, Easing.OutQuint); - if (Precision.DefinitelyBigger(1.0, Current.Value) && Current.Value >= 0.995) - Current.Value = 0.99; - if (Precision.DefinitelyBigger(Current.Value, 1.0) && Current.Value < 1.005) - Current.Value = 1.01; - const float move_amount = 4; if (e.NewValue > e.OldValue) counter.MoveToY(Math.Max(-move_amount * 2, counter.Y - move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint); @@ -153,7 +147,7 @@ namespace osu.Game.Overlays.Mods { protected override double RollingDuration => 500; - protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"0.00x"); + protected override LocalisableString FormatCount(double count) => ModUtils.FormatScoreMultiplier(count); protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText { diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index cfbd17be01..69782c25bb 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -19,7 +19,7 @@ using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; using osu.Game.Input.Bindings; -using osu.Framework.Utils; +using osu.Game.Utils; namespace osu.Game.Screens.Select { @@ -88,17 +88,11 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() => Schedule(() => { double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + MultiplierText.Text = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier); - if (Precision.DefinitelyBigger(1.0, multiplier) && multiplier >= 0.995) - MultiplierText.Text = $"{0.99:N2}x"; - else if (Precision.DefinitelyBigger(multiplier, 1.0) && multiplier < 1.005) - MultiplierText.Text = $"{1.01:N2}x"; - else - MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - if (Precision.DefinitelyBigger(multiplier, 1.0)) + if (multiplier > 1) MultiplierText.FadeColour(highMultiplierColour, 200); - else if (Precision.DefinitelyBigger(1.0, multiplier)) + else if (multiplier < 1) MultiplierText.FadeColour(lowMultiplierColour, 200); else MultiplierText.FadeColour(Color4.White, 200); From cd9250d08ccc415b5d94bacf8e0c31eb0d273ffb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Dec 2023 05:18:10 +0300 Subject: [PATCH 176/539] Add test coverage --- osu.Game.Tests/Mods/ModUtilsTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 9107ddd1ae..4f10a0ea54 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -310,6 +310,13 @@ namespace osu.Game.Tests.Mods Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [Test] + public void TestFormatScoreMultiplier() + { + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9999).ToString(), "0.99x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0001).ToString(), "1.01x"); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } @@ -339,6 +346,16 @@ namespace osu.Game.Tests.Mods public override bool ValidForMultiplayerAsFreeMod => false; } + public class EditableMod : Mod + { + public override string Name => string.Empty; + public override LocalisableString Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => Multiplier; + + public double Multiplier = 1; + } + public interface IModCompatibilitySpecification { } From 5ae5d7f92dc7b002e5b685922707c9cef61edbf9 Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Sat, 30 Dec 2023 23:59:47 -0600 Subject: [PATCH 177/539] change floor to round --- osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index b5315feccb..45213b7bdb 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Edit.Timing row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; row.WaveformOffsetTo(-offset, animated); row.WaveformScale = new Vector2(scale, 1); - row.BeatIndex = (int)Math.Floor(index); + row.BeatIndex = (int)Math.Round(index); index++; } From 0c85fd496f9d555e1ec1c0b07fbebd2f9e8fd998 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Sun, 31 Dec 2023 22:10:49 +0900 Subject: [PATCH 178/539] Don't leave scores screen empty if no scores are present yet Addresses https://github.com/ppy/osu/discussions/23787 I originally wanted to set `allowShowingResults` to false if there are no results but because this involves an API request to fetch the scores that would mean all the scores would have to be fetched all at once so that it knows whether to hide or show the "View results" button on a beatmap. Because that would slow things down and be very inefficient, this still allows the user to view the scores screen but if there aren't any scores, it shows a text, which I think is at least for now better than nothing. As for the testing of this, I wasn't sure how to not generate scores only for one specific test so I opted into not using `SetUpSteps` and doing it that way. --- .../TestScenePlaylistsResultsScreen.cs | 47 ++++++++++++++----- osu.Game/Screens/Ranking/ResultsScreen.cs | 35 ++++++++++---- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 + 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index cb422d8c06..e36e7f54ba 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -42,15 +42,15 @@ namespace osu.Game.Tests.Visual.Playlists private int totalCount; private ScoreInfo userScore; - [SetUpSteps] - public override void SetUpSteps() + // We don't use SetUpSteps for this because one of the tests shouldn't receive any scores. + public void InitialiseUserScoresAndBeatmap(bool noScores = false) { base.SetUpSteps(); // Previous test instances of the results screen may still exist at this point so wait for // those screens to be cleaned up by the base SetUpSteps before re-initialising test state. - // The the screen also holds a leased Beatmap bindable so reassigning it must happen after - // the screen as been exited. + // The screen also holds a leased Beatmap bindable so reassigning it must happen after + // the screen has been exited. AddStep("initialise user scores and beatmap", () => { lowestScoreId = 1; @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Playlists userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); - bindHandler(); + bindHandler(noScores: noScores); // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. @@ -74,6 +74,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowWithUserScore() { + InitialiseUserScoresAndBeatmap(); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -86,6 +88,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScore() { + InitialiseUserScoresAndBeatmap(); + createResults(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); @@ -94,6 +98,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowUserScoreWithDelay() { + InitialiseUserScoresAndBeatmap(); + AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); @@ -105,6 +111,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScoreWithDelay() { + InitialiseUserScoresAndBeatmap(); + AddStep("bind delayed handler", () => bindHandler(true)); createResults(); @@ -115,6 +123,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheRight() { + InitialiseUserScoresAndBeatmap(); + createResults(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -137,6 +147,8 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheLeft() { + InitialiseUserScoresAndBeatmap(); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -158,7 +170,16 @@ namespace osu.Game.Tests.Visual.Playlists } } - private void createResults(Func getScore = null) + [Test] + public void TestShowWithNoScores() + { + InitialiseUserScoresAndBeatmap(noScores: true); + + createResults(noScores: true); + AddAssert("no scores visible", () => resultsScreen.ScorePanelList.GetScorePanels().Count() == 0); + } + + private void createResults(Func getScore = null, bool noScores = false) { AddStep("load results", () => { @@ -169,11 +190,13 @@ namespace osu.Game.Tests.Visual.Playlists }); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); - waitForDisplay(); + waitForDisplay(noScores); } - private void waitForDisplay() + private void waitForDisplay(bool noScores = false) { + if (noScores) return; + AddUntilStep("wait for scores loaded", () => requestComplete // request handler may need to fire more than once to get scores. @@ -183,7 +206,7 @@ namespace osu.Game.Tests.Visual.Playlists AddWaitStep("wait for display", 5); } - private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => + private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false, bool noScores = false) => ((DummyAPIAccess)API).HandleRequest = request => { // pre-check for requests we should be handling (as they are scheduled below). switch (request) @@ -219,7 +242,7 @@ namespace osu.Game.Tests.Visual.Playlists break; case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i)); + triggerSuccess(i, createIndexResponse(i, noScores)); break; } }, delay); @@ -301,10 +324,12 @@ namespace osu.Game.Tests.Visual.Playlists return multiplayerUserScore; } - private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req) + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) { var result = new IndexedMultiplayerScores(); + if (noScores) return result; + string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; for (int i = 1; i <= scores_per_result; i++) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e3d19725da..e18ab63706 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -19,6 +19,7 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; @@ -204,17 +205,31 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - APIRequest nextPageRequest = null; - - if (ScorePanelList.IsScrolledToStart) - nextPageRequest = FetchNextPage(-1, fetchScoresCallback); - else if (ScorePanelList.IsScrolledToEnd) - nextPageRequest = FetchNextPage(1, fetchScoresCallback); - - if (nextPageRequest != null) + if (ScorePanelList.IsEmpty) { - lastFetchCompleted = false; - api.Queue(nextPageRequest); + // This can happen if a beatmap part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular), + Text = "no scores yet!", + }); + } + else + { + APIRequest nextPageRequest = null; + + if (ScorePanelList.IsScrolledToStart) + nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + else if (ScorePanelList.IsScrolledToEnd) + nextPageRequest = FetchNextPage(1, fetchScoresCallback); + + if (nextPageRequest != null) + { + lastFetchCompleted = false; + api.Queue(nextPageRequest); + } } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index b75f3d86ff..95c90e35a0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.Ranking public bool AllPanelsVisible => flow.All(p => p.IsPresent); + public bool IsEmpty => flow.Count == 0; + /// /// The current scroll position. /// From 4a94cfd35d74060daf691aabf6cf6235a1282344 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Dec 2023 16:47:10 +0300 Subject: [PATCH 179/539] Fix failing test case --- .../Visual/UserInterface/TestSceneFooterButtonMods.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 4e1bf1390a..b98b91e8e5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; +using osu.Game.Utils; namespace osu.Game.Tests.Visual.UserInterface { @@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.UserInterface private bool assertModsMultiplier(IEnumerable mods) { double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + string expectedValue = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier).ToString(); return expectedValue == footerButtonMods.MultiplierText.Current.Value; } From ca0f09733ab7741a1ee583d710bafead4fe86039 Mon Sep 17 00:00:00 2001 From: CaffeeLake Date: Sun, 31 Dec 2023 23:35:42 +0900 Subject: [PATCH 180/539] Remove unnecessary using directives Signed-off-by: CaffeeLake --- osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index b98b91e8e5..a95bb2c9e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; From ad4b5f6deddc8cab855be638d7c77a358e6bf05a Mon Sep 17 00:00:00 2001 From: CaffeeLake Date: Mon, 1 Jan 2024 08:32:21 +0900 Subject: [PATCH 181/539] Fix: floating point errors Signed-off-by: CaffeeLake --- osu.Game/Utils/ModUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index dad8c72aa1..252579a186 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -238,9 +238,9 @@ namespace osu.Game.Utils { // Round multiplier values away from 1.00x to two significant digits. if (scoreMultiplier > 1) - scoreMultiplier = Math.Ceiling(scoreMultiplier * 100) / 100; + scoreMultiplier = Math.Ceiling(Math.Round(scoreMultiplier * 100, 12)) / 100; else - scoreMultiplier = Math.Floor(scoreMultiplier * 100) / 100; + scoreMultiplier = Math.Floor(Math.Round(scoreMultiplier * 100, 12)) / 100; return scoreMultiplier.ToLocalisableString("0.00x"); } From 94531807e476c774c42df3ef0ef745608eb80534 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jan 2024 17:01:32 +0900 Subject: [PATCH 182/539] Make slider ends show on results screen again --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game/Scoring/ScoreInfo.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 5959576b9d..0496d1f680 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,6 +277,7 @@ namespace osu.Game.Rulesets.Osu HitResult.LargeTickHit, HitResult.SmallTickHit, + HitResult.SliderTailHit, HitResult.SmallBonus, HitResult.LargeBonus, }; @@ -289,6 +290,7 @@ namespace osu.Game.Rulesets.Osu case HitResult.LargeTickHit: return "slider tick"; + case HitResult.SliderTailHit: case HitResult.SmallTickHit: return "slider end"; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 44795c6fa7..32e4bbbf29 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -350,6 +350,7 @@ namespace osu.Game.Scoring { case HitResult.SmallTickHit: case HitResult.LargeTickHit: + case HitResult.SliderTailHit: case HitResult.LargeBonus: case HitResult.SmallBonus: if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0) From b0cfea491675a012694576168396ba1c433133e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 10:11:49 +0100 Subject: [PATCH 183/539] Fix standardised score conversion failing for some taiko scores due to overestimating accuracy portion Standardised score conversion would return a negative total score for https://osu.ppy.sh/scores/taiko/182230346. The underlying reason for this is that the estimation of the accuracy portion for a score can be above the actual accuracy portion in the taiko ruleset. When calculating the maximum accuracy portion achievable, `TaikoLegacyScoreSimulator` will include the extra 300 points from a double hit on a strong hit, per strong hit. However, this double hit is not factored into accuracy. Both of the aforementioned facts mean that in taiko maximumLegacyAccuracyScore * score.Accuracy - which normally in other rulesets can be used pretty reliably as the exact number of points gained from the accuracy portion - is an estimate in the case of taiko, and an _upper_ estimate at that, because it implicitly assumes that the user has also hit `score.Accuracy` percent of all double hits possible in the beatmap. If this assumption is not upheld, then the user will have earned _less_ points than that from the accuracy portion, which means that the combo proportion estimate will go below zero. It is possible that this has happened on other scores before, but did not result in the total score going negative as the accuracy portion gained would have counteracted the effect of that due to being larger in magnitude than the score loss incurred from the negative combo portion. In the case of the score in question this was not the case due to very low accuracy _and_ very low max combo. --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 66c816e796..9cfb9ea957 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -316,7 +316,7 @@ namespace osu.Game.Database // when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot). // In such cases, just assume 0. double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0 - ? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore) + ? Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore) : 0; // We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore. From 3c5e9ac9a9cfc224905f8f8c6da66ed143cc7e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 10:49:09 +0100 Subject: [PATCH 184/539] Fix possible double score submission when auto-retrying via perfect mod Closes https://github.com/ppy/osu/issues/26035. `submitOnFailOrQuit()`, as the name suggests, can be called both when the player has failed, or when the player screen is being exited from. Notably, when perfect mod with auto-retry is active, the two happen almost simultaneously. This double call exposes a data race in `submitScore()` concerning the handling of `scoreSubmissionSource`. The race could be experimentally confirmed by applying the following patch: diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 83adf1f960..76dd29bbdb 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -228,6 +228,7 @@ private Task submitScore(Score score) return Task.CompletedTask; } + Logger.Log($"{nameof(scoreSubmissionSource)} is {(scoreSubmissionSource == null ? "null" : "not null")}"); if (scoreSubmissionSource != null) return scoreSubmissionSource.Task; @@ -237,6 +238,7 @@ private Task submitScore(Score score) Logger.Log($"Beginning score submission (token:{token.Value})..."); + Logger.Log($"creating new {nameof(scoreSubmissionSource)}"); scoreSubmissionSource = new TaskCompletionSource(); var request = CreateSubmissionRequest(score, token.Value); which would result in the following log output: [runtime] 2024-01-02 09:54:13 [verbose]: scoreSubmissionSource is null [runtime] 2024-01-02 09:54:13 [verbose]: scoreSubmissionSource is null [runtime] 2024-01-02 09:54:13 [verbose]: Beginning score submission (token:36780)... [runtime] 2024-01-02 09:54:13 [verbose]: creating new scoreSubmissionSource [runtime] 2024-01-02 09:54:13 [verbose]: Beginning score submission (token:36780)... [runtime] 2024-01-02 09:54:13 [verbose]: creating new scoreSubmissionSource [network] 2024-01-02 09:54:13 [verbose]: Performing request osu.Game.Online.Solo.SubmitSoloScoreRequest [network] 2024-01-02 09:54:14 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/869310/solo/scores/36780 successfully completed! [network] 2024-01-02 09:54:14 [verbose]: SubmitSoloScoreRequest finished with response size of 639 bytes [network] 2024-01-02 09:54:14 [verbose]: Performing request osu.Game.Online.Solo.SubmitSoloScoreRequest [runtime] 2024-01-02 09:54:14 [verbose]: Score submission completed! (token:36780 id:20247) [network] 2024-01-02 09:54:14 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/869310/solo/scores/36780 successfully completed! [network] 2024-01-02 09:54:14 [verbose]: SubmitSoloScoreRequest finished with response size of 639 bytes [runtime] 2024-01-02 09:54:14 [error]: An unhandled error has occurred. [runtime] 2024-01-02 09:54:14 [error]: System.InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed. [runtime] 2024-01-02 09:54:14 [error]: at osu.Game.Screens.Play.SubmittingPlayer.<>c__DisplayClass30_0.b__0(MultiplayerScore s) in /home/dachb/Documents/opensource/osu/osu.Game/Screens/Play/SubmittingPlayer.cs:line 250 The intention of the submission logic was to only ever create one `scoreSubmissionSource`, and then reuse this one if a redundant submission request was made. However, because of the temporal proximity of fail and quit in this particular case, combined with the fact that the calls to `submitScore()` are taking place on TPL threads, means that there is a read-write data race on `scoreSubmissionSource`, wherein the source can be actually created twice. This leads to two concurrent score submission requests, which, upon completion, attempt to transition only _the second_ `scoreSubmissionSource` to a final state (this is because the API success/failure request callbacks capture `this`, i.e. the entire `SubmittingPlayer` instance, rather than the `scoreSubmissionSource` reference specifically). To fix, ensure correct synchronisation on the read-write critical section, which should prevent the `scoreSubmissionSource` from being created multiple times. --- osu.Game/Screens/Play/SubmittingPlayer.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 83adf1f960..fb3481cbc4 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Play [Resolved] private SessionStatics statics { get; set; } + private readonly object scoreSubmissionLock = new object(); private TaskCompletionSource scoreSubmissionSource; protected SubmittingPlayer(PlayerConfiguration configuration = null) @@ -228,16 +229,19 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } - if (scoreSubmissionSource != null) - return scoreSubmissionSource.Task; + lock (scoreSubmissionLock) + { + if (scoreSubmissionSource != null) + return scoreSubmissionSource.Task; + + scoreSubmissionSource = new TaskCompletionSource(); + } // if the user never hit anything, this score should not be counted in any way. if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) return Task.CompletedTask; Logger.Log($"Beginning score submission (token:{token.Value})..."); - - scoreSubmissionSource = new TaskCompletionSource(); var request = CreateSubmissionRequest(score, token.Value); request.Success += s => From 72e502e6156b58474c8a6c9517b6f4c73ad420b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 11:55:59 +0100 Subject: [PATCH 185/539] Fix backwards z-ordering of fruits in juice streams and banana showers Closes https://github.com/ppy/osu/issues/25827. The logic cannot be easily abstracted out (because `CompareReverseChildID()` is protected and non-static on `CompositeDrawable`), so a local copy was applied instead. No testing as any testing would have been purely visual anyways. --- .../Objects/Drawables/DrawableBananaShower.cs | 2 +- .../Objects/Drawables/DrawableJuiceStream.cs | 2 +- .../Objects/Drawables/NestedFruitContainer.cs | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 03adbce885..9ee4a15182 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(bananaContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 41ecf59276..677b61df47 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }); + AddInternal(dropletContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both, }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs new file mode 100644 index 0000000000..90bdb0237e --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/NestedFruitContainer.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public partial class NestedFruitContainer : Container + { + /// + /// This comparison logic is a copy of comparison logic, + /// which can't be easily extracted to a more common place. + /// + /// + protected override int Compare(Drawable x, Drawable y) + { + if (x is not DrawableCatchHitObject xObj || y is not DrawableCatchHitObject yObj) + return base.Compare(x, y); + + int result = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return result == 0 ? CompareReverseChildID(x, y) : result; + } + } +} From f9f03ebc0f066dc8d7c23764fc3d88e4d6278bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 14:04:40 +0100 Subject: [PATCH 186/539] Store user online state in config for next launch Closes remainder of https://github.com/ppy/osu/issues/12635. --- osu.Game/Configuration/OsuConfigManager.cs | 3 +++ osu.Game/Online/API/APIAccess.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d4162b76d6..23686db1f8 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -20,6 +20,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Configuration { @@ -193,6 +194,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); + SetDefault(OsuSetting.UserOnlineStatus, null); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -420,5 +422,6 @@ namespace osu.Game.Configuration EditorShowSpeedChanges, TouchDisableGameplayTaps, ModSelectTextSearchStartsActive, + UserOnlineStatus, } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 21107d61fc..be5bdeca77 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -62,6 +62,9 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); + private Bindable configStatus { get; } = new Bindable(); + private Bindable localUserStatus { get; } = new Bindable(); + protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -85,12 +88,20 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; + config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + localUser.BindValueChanged(u => { u.OldValue?.Activity.UnbindFrom(activity); u.NewValue.Activity.BindTo(activity); + + if (u.OldValue != null) + localUserStatus.UnbindFrom(u.OldValue.Status); + localUserStatus.BindTo(u.NewValue.Status); }, true); + localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue); + var thread = new Thread(run) { Name = "APIAccess", @@ -200,6 +211,7 @@ namespace osu.Game.Online.API setLocalUser(new APIUser { Username = ProvidedUsername, + Status = { Value = configStatus.Value ?? UserStatus.Online } }); } @@ -246,8 +258,7 @@ namespace osu.Game.Online.API }; userReq.Success += user => { - // todo: save/pull from settings - user.Status.Value = UserStatus.Online; + user.Status.Value = configStatus.Value ?? UserStatus.Online; setLocalUser(user); From d4e917448d98e6409982719231f8b53e4340e11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 14:07:04 +0100 Subject: [PATCH 187/539] Fix login panel dropdown forcing user online It was sort of assuming that the user can't be anything but online when opening, thus forcing the status to online via the immediately-run value change callback. --- osu.Game/Overlays/Login/LoginPanel.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 19af95459f..b0af2fa273 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -143,6 +143,8 @@ namespace osu.Game.Overlays.Login panel.Status.BindTo(api.LocalUser.Value.Status); panel.Activity.BindTo(api.LocalUser.Value.Activity); + panel.Status.BindValueChanged(_ => updateDropdownCurrent(), true); + dropdown.Current.BindValueChanged(action => { switch (action.NewValue) @@ -174,6 +176,24 @@ namespace osu.Game.Overlays.Login ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); }); + private void updateDropdownCurrent() + { + switch (panel.Status.Value) + { + case UserStatus.Online: + dropdown.Current.Value = UserAction.Online; + break; + + case UserStatus.DoNotDisturb: + dropdown.Current.Value = UserAction.DoNotDisturb; + break; + + case UserStatus.Offline: + dropdown.Current.Value = UserAction.AppearOffline; + break; + } + } + public override bool AcceptsFocus => true; protected override bool OnClick(ClickEvent e) => true; From 09b2a4e3b42defa1ead3a156c2f8c890ed175473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 14:07:59 +0100 Subject: [PATCH 188/539] Fix users blipping online briefly before their online status is known --- osu.Game/Online/Metadata/OnlineMetadataClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 6d00ce7551..c42c3378b7 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,7 +23,6 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); - // ReSharper disable once InconsistentlySynchronizedField public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -192,7 +191,7 @@ namespace osu.Game.Online.Metadata { Schedule(() => { - if (presence != null) + if (presence?.Status != null) userStates[userId] = presence.Value; else userStates.Remove(userId); From 5eaf5fca2ad8d162835212151b8c1c530f7ca0f1 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 2 Jan 2024 20:33:36 +0600 Subject: [PATCH 189/539] Add user card with global/country ranks for login overlay --- osu.Game/Overlays/Login/LoginPanel.cs | 7 +- osu.Game/Users/ExtendedUserPanel.cs | 9 -- osu.Game/Users/UserPanel.cs | 41 +++-- osu.Game/Users/UserRankPanel.cs | 212 ++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Users/UserRankPanel.cs diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 19af95459f..892d613ea4 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Login [Resolved] private OsuColour colours { get; set; } = null!; - private UserGridPanel panel = null!; + private UserRankPanel panel = null!; private UserDropdown dropdown = null!; /// @@ -131,7 +131,7 @@ namespace osu.Game.Overlays.Login Text = LoginPanelStrings.SignedIn, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - panel = new UserGridPanel(api.LocalUser.Value) + panel = new UserRankPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X, Action = RequestHide @@ -140,9 +140,6 @@ namespace osu.Game.Overlays.Login }, }; - panel.Status.BindTo(api.LocalUser.Value.Status); - panel.Activity.BindTo(api.LocalUser.Value.Activity); - dropdown.Current.BindValueChanged(action => { switch (action.NewValue) diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 1359f5d792..e33fb7a44e 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -3,7 +3,6 @@ #nullable disable -using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -53,14 +52,6 @@ namespace osu.Game.Users statusIcon.FinishTransforms(); } - protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); - - protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) - { - Size = new Vector2(36, 26), - Action = Action, - }; - protected Container CreateStatusIcon() => statusIcon = new StatusIcon(); protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index b6a77e754d..d38f5d8ccc 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -23,6 +23,8 @@ using osu.Game.Localisation; using osu.Game.Online.Multiplayer; using osu.Game.Screens; using osu.Game.Screens.Play; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Users { @@ -77,23 +79,18 @@ namespace osu.Game.Users { Masking = true; - AddRange(new[] + Add(new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider?.Background5 ?? Colours.Gray1 - }, - Background = new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, - CreateLayout() + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider?.Background5 ?? Colours.Gray1 }); + var background = CreateBackground(); + if (background != null) + Add(background); + + Add(CreateLayout()); + base.Action = ViewProfile = () => { Action?.Invoke(); @@ -110,6 +107,22 @@ namespace osu.Game.Users Text = User.Username, }; + protected virtual Drawable? CreateBackground() => Background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User + }; + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) + { + Size = new Vector2(36, 26), + Action = Action, + }; + public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs new file mode 100644 index 0000000000..272d2eecb4 --- /dev/null +++ b/osu.Game/Users/UserRankPanel.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Users +{ + /// + /// User card that shows user's global and country ranks in the bottom. + /// Meant to be used in the toolbar login overlay. + /// + public partial class UserRankPanel : UserPanel + { + private const int padding = 10; + private const int main_content_height = 80; + + public UserRankPanel(APIUser user) + : base(user) + { + AutoSizeAxes = Axes.Y; + CornerRadius = 10; + } + + [BackgroundDependencyLoader] + private void load() + { + BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + Name = "Main content", + RelativeSizeAxes = Axes.X, + Height = main_content_height, + CornerRadius = 10, + Masking = true, + Children = new Drawable[] + { + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + Alpha = 0.3f + }, + new GridContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, padding), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.Absolute, padding), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, padding), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + Empty(), + Empty(), + Empty(), + Empty() + }, + new[] + { + Empty(), + CreateAvatar().With(avatar => + { + avatar.Size = new Vector2(60); + avatar.Masking = true; + avatar.CornerRadius = 6; + }), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = padding }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + } + } + }, + new Drawable[] + { + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + } + } + }, + Empty() + } + } + } + } + }, + new Container + { + Name = "Bottom content", + Margin = new MarginPadding { Top = main_content_height }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 80, Vertical = padding }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension() + }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] + { + new Drawable[] + { + new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + Content = User.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" + }, + new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + Content = User.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" + } + } + } + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Height = 26, + SupportLevel = User.SupportLevel + }); + } + + return layout; + } + + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + protected override Drawable? CreateBackground() => null; + } +} From 17656e9b9cfe4e6ce3fa2627bb1b3e34694ad305 Mon Sep 17 00:00:00 2001 From: Lena Date: Tue, 2 Jan 2024 18:38:25 +0100 Subject: [PATCH 190/539] update the current activity when the multiplayer room updates --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7c12e6eab5..56256bb15c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -349,6 +349,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; Scheduler.AddOnce(UpdateMods); + + Activity.Value = new UserActivity.InLobby(Room); } private bool localUserCanAddItem => client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly; From c4be6fa9741c5fdcf8c347bdec9bf5fe3641d3fb Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 3 Jan 2024 00:37:24 +0600 Subject: [PATCH 191/539] Fix code quality, add new cards to the test scene --- .../Visual/Online/TestSceneUserPanel.cs | 25 +++++++++++++++++-- osu.Game/Overlays/Login/LoginPanel.cs | 3 +-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index b3b8fd78d3..81e17cd1b8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -29,6 +30,9 @@ namespace osu.Game.Tests.Visual.Online private UserGridPanel boundPanel1; private TestUserListPanel boundPanel2; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Resolved] private IRulesetStore rulesetStore { get; set; } @@ -85,8 +89,25 @@ namespace osu.Game.Tests.Visual.Online CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsOnline = false, LastVisit = DateTimeOffset.Now - }) - }, + }), + new UserRankPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } + }) { Width = 300 }, + new UserRankPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + }) { Width = 300 } + } }; boundPanel1.Status.BindTo(status); diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 892d613ea4..57a9f4035c 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -30,7 +30,6 @@ namespace osu.Game.Overlays.Login [Resolved] private OsuColour colours { get; set; } = null!; - private UserRankPanel panel = null!; private UserDropdown dropdown = null!; /// @@ -131,7 +130,7 @@ namespace osu.Game.Overlays.Login Text = LoginPanelStrings.SignedIn, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - panel = new UserRankPanel(api.LocalUser.Value) + new UserRankPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X, Action = RequestHide From 88509f28ad66110f0b8b7fb79e69aeccce7e5929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 20:51:22 +0100 Subject: [PATCH 192/539] Adjust assertion to match new juice stream child ordering --- osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index 419a846ec3..825e8c697c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests Mod = new CatchModHidden(), PassCondition = () => Player.Results.Count > 0 && Player.ChildrenOfType().Single().Alpha > 0 - && Player.ChildrenOfType().Last().Alpha > 0 + && Player.ChildrenOfType().First().Alpha > 0 }); } From bdfaa4b583912ebe4bba70a0d48c13ef1e79b919 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jan 2024 13:34:49 +0900 Subject: [PATCH 193/539] Fix crash when dragging rotation control in editor with both mouse buttons Closes https://github.com/ppy/osu/issues/26325. --- .../Edit/Compose/Components/SelectionBoxRotationHandle.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 024749a701..5270162189 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -62,6 +62,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { + if (e.Button != MouseButton.Left) + return false; + if (rotationHandler == null) return false; rotationHandler.Begin(); From 9e8d07d3144bd4b072d28bd9bd0e255fee410de0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 15:47:05 +0900 Subject: [PATCH 194/539] Add NVAPI and force thread optimisations on This undoes what stable does to force this setting off. --- osu.Desktop/NVAPI.cs | 737 +++++++++++++++++++++++++++++++++++++++++ osu.Desktop/Program.cs | 2 + 2 files changed, 739 insertions(+) create mode 100644 osu.Desktop/NVAPI.cs diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs new file mode 100644 index 0000000000..9648ea1072 --- /dev/null +++ b/osu.Desktop/NVAPI.cs @@ -0,0 +1,737 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using osu.Framework.Logging; + +namespace osu.Desktop +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class NVAPI + { + private const string osu_filename = "osu!.exe"; + + // This is a good reference: + // https://github.com/errollw/Warp-and-Blend-Quadros/blob/master/WarpBlend-Quadros/UnwarpAll-Quadros/include/nvapi.h + // Note our Stride == their VERSION (e.g. NVDRS_SETTING_VER) + + public const int MAX_PHYSICAL_GPUS = 64; + public const int UNICODE_STRING_MAX = 2048; + + public const string APPLICATION_NAME = @"osu!"; + public const string PROFILE_NAME = @"osu!"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus EnumPhysicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount); + + public static readonly EnumPhysicalGPUsDelegate EnumPhysicalGPUs; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus EnumLogicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount); + + public static readonly EnumLogicalGPUsDelegate EnumLogicalGPUs; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetSystemTypeDelegate(IntPtr gpuHandle, out NvSystemType systemType); + + public static readonly GetSystemTypeDelegate GetSystemType; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetGPUTypeDelegate(IntPtr gpuHandle, out NvGpuType gpuType); + + public static readonly GetGPUTypeDelegate GetGPUType; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus CreateSessionDelegate(out IntPtr sessionHandle); + + public static CreateSessionDelegate CreateSession; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus LoadSettingsDelegate(IntPtr sessionHandle); + + public static LoadSettingsDelegate LoadSettings; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus FindApplicationByNameDelegate(IntPtr sessionHandle, [MarshalAs(UnmanagedType.BStr)] string appName, out IntPtr profileHandle, ref NvApplication application); + + public static FindApplicationByNameDelegate FindApplicationByName; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetCurrentGlobalProfileDelegate(IntPtr sessionHandle, out IntPtr profileHandle); + + public static GetCurrentGlobalProfileDelegate GetCurrentGlobalProfile; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetProfileInfoDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvProfile profile); + + public static GetProfileInfoDelegate GetProfileInfo; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NvStatus GetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, NvSettingID settingID, ref NvSetting setting); + + public static GetSettingDelegate GetSetting; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus CreateProfileDelegate(IntPtr sessionHandle, ref NvProfile profile, out IntPtr profileHandle); + + private static readonly CreateProfileDelegate CreateProfile; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvSetting setting); + + private static readonly SetSettingDelegate SetSetting; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus EnumApplicationsDelegate(IntPtr sessionHandle, IntPtr profileHandle, uint startIndex, ref uint appCount, [In, Out, MarshalAs(UnmanagedType.LPArray)] NvApplication[] applications); + + private static readonly EnumApplicationsDelegate EnumApplications; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus CreateApplicationDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvApplication application); + + private static readonly CreateApplicationDelegate CreateApplication; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SaveSettingsDelegate(IntPtr sessionHandle); + + private static readonly SaveSettingsDelegate SaveSettings; + + public static NvStatus Status { get; private set; } = NvStatus.OK; + public static bool Available { get; private set; } + + private static IntPtr sessionHandle; + + public static bool IsUsingOptimusDedicatedGpu + { + get + { + if (!Available) + return false; + + if (!IsLaptop) + return false; + + IntPtr profileHandle; + if (!getProfile(out profileHandle, out _, out bool _)) + return false; + + // Get the optimus setting + NvSetting setting; + if (!getSetting(NvSettingID.SHIM_RENDERING_MODE_ID, profileHandle, out setting)) + return false; + + return (setting.U32CurrentValue & (uint)NvShimSetting.SHIM_RENDERING_MODE_ENABLE) > 0; + } + } + + public static bool IsLaptop + { + get + { + if (!Available) + return false; + + // Make sure that this is a laptop. + var gpus = new IntPtr[64]; + if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount))) + return false; + + for (int i = 0; i < gpuCount; i++) + { + if (checkError(GetSystemType(gpus[i], out var type))) + return false; + + if (type == NvSystemType.LAPTOP) + return true; + } + + return false; + } + } + + public static bool ThreadedOptimisations + { + get + { + if (!Available) + return false; + + IntPtr profileHandle; + if (!getProfile(out profileHandle, out _, out bool _)) + return false; + + // Get the threaded optimisations setting + NvSetting setting; + if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting)) + return false; + + return setting.U32CurrentValue != (uint)NvThreadControlSetting.OGL_THREAD_CONTROL_DISABLE; + } + set + { + if (!Available) + return; + + bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)(value ? NvThreadControlSetting.OGL_THREAD_CONTROL_ENABLE : NvThreadControlSetting.OGL_THREAD_CONTROL_DISABLE)); + + Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!"); + } + } + + /// + /// Checks if the profile contains the current application. + /// + /// If the profile contains the current application. + private static bool containsApplication(IntPtr profileHandle, NvProfile profile, out NvApplication application) + { + application = new NvApplication + { + Version = NvApplication.Stride + }; + + if (profile.NumOfApps == 0) + return false; + + NvApplication[] applications = new NvApplication[profile.NumOfApps]; + applications[0].Version = NvApplication.Stride; + + uint numApps = profile.NumOfApps; + + if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications))) + return false; + + for (uint i = 0; i < numApps; i++) + { + if (applications[i].AppName == osu_filename) + { + application = applications[i]; + return true; + } + } + + return false; + } + + /// + /// Retrieves the profile of the current application. + /// + /// The profile handle. + /// The current application description. + /// If this profile is not a global (default) profile. + /// If the operation succeeded. + private static bool getProfile(out IntPtr profileHandle, out NvApplication application, out bool isApplicationSpecific) + { + application = new NvApplication + { + Version = NvApplication.Stride + }; + + isApplicationSpecific = true; + + if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application))) + { + isApplicationSpecific = false; + if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle))) + return false; + } + + return true; + } + + /// + /// Creates a profile. + /// + /// The profile handle. + /// If the operation succeeded. + private static bool createProfile(out IntPtr profileHandle) + { + NvProfile newProfile = new NvProfile + { + Version = NvProfile.Stride, + IsPredefined = 0, + ProfileName = PROFILE_NAME, + GPUSupport = new uint[32] + }; + + newProfile.GPUSupport[0] = 1; + + if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle))) + return false; + + return true; + } + + /// + /// Retrieves a setting from the profile. + /// + /// The setting to retrieve. + /// The profile handle to retrieve the setting from. + /// The setting. + /// If the operation succeeded. + private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out NvSetting setting) + { + setting = new NvSetting + { + Version = NvSetting.Stride, + SettingID = settingId + }; + + if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting))) + return false; + + return true; + } + + private static bool setSetting(NvSettingID settingId, uint settingValue) + { + NvApplication application; + IntPtr profileHandle; + bool isApplicationSpecific; + if (!getProfile(out profileHandle, out application, out isApplicationSpecific)) + return false; + + if (!isApplicationSpecific) + { + // We don't want to interfere with the user's other settings, so let's create a separate config for osu! + if (!createProfile(out profileHandle)) + return false; + } + + NvSetting newSetting = new NvSetting + { + Version = NvSetting.Stride, + SettingID = settingId, + U32CurrentValue = settingValue + }; + + // Set the thread state + if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting))) + return false; + + // Get the profile (needed to check app count) + NvProfile profile = new NvProfile + { + Version = NvProfile.Stride + }; + if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile))) + return false; + + if (!containsApplication(profileHandle, profile, out application)) + { + // Need to add the current application to the profile + application.IsPredefined = 0; + + application.AppName = osu_filename; + application.UserFriendlyName = APPLICATION_NAME; + + if (checkError(CreateApplication(sessionHandle, profileHandle, ref application))) + return false; + } + + // Save! + return !checkError(SaveSettings(sessionHandle)); + } + + /// + /// Creates a session to access the driver configuration. + /// + /// If the operation succeeded. + private static bool createSession() + { + if (checkError(CreateSession(out sessionHandle))) + return false; + + // Load settings into session + if (checkError(LoadSettings(sessionHandle))) + return false; + + return true; + } + + private static bool checkError(NvStatus status) + { + Status = status; + return status != NvStatus.OK; + } + + static NVAPI() + { + // TODO: check whether gpu vendor contains NVIDIA before attempting load? + + try + { + // Try to load NVAPI + if ((IntPtr.Size == 4 && loadLibrary(@"nvapi.dll") == IntPtr.Zero) + || (IntPtr.Size == 8 && loadLibrary(@"nvapi64.dll") == IntPtr.Zero)) + { + return; + } + + InitializeDelegate initialize; + getDelegate(0x0150E828, out initialize); + + if (initialize?.Invoke() == NvStatus.OK) + { + // IDs can be found here: https://github.com/jNizM/AHK_NVIDIA_NvAPI/blob/master/info/NvAPI_IDs.txt + + getDelegate(0xE5AC921F, out EnumPhysicalGPUs); + getDelegate(0x48B3EA59, out EnumLogicalGPUs); + getDelegate(0xBAAABFCC, out GetSystemType); + getDelegate(0xC33BAEB1, out GetGPUType); + getDelegate(0x0694D52E, out CreateSession); + getDelegate(0x375DBD6B, out LoadSettings); + getDelegate(0xEEE566B2, out FindApplicationByName); + getDelegate(0x617BFF9F, out GetCurrentGlobalProfile); + getDelegate(0x577DD202, out SetSetting); + getDelegate(0x61CD6FD6, out GetProfileInfo); + getDelegate(0x73BF8338, out GetSetting); + getDelegate(0xCC176068, out CreateProfile); + getDelegate(0x7FA2173A, out EnumApplications); + getDelegate(0x4347A9DE, out CreateApplication); + getDelegate(0xFCBC7E14, out SaveSettings); + } + + if (createSession()) + Available = true; + } + catch { } + } + + private static void getDelegate(uint id, out T newDelegate) where T : class + { + IntPtr ptr = IntPtr.Size == 4 ? queryInterface32(id) : queryInterface64(id); + newDelegate = ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T; + } + + [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")] + private static extern IntPtr loadLibrary(string dllToLoad); + + [DllImport(@"nvapi.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr queryInterface32(uint id); + + [DllImport(@"nvapi64.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr queryInterface64(uint id); + + private delegate NvStatus InitializeDelegate(); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct NvSetting + { + public uint Version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string SettingName; + + public NvSettingID SettingID; + public uint SettingType; + public uint SettingLocation; + public uint IsCurrentPredefined; + public uint IsPredefinedValid; + + public uint U32PredefinedValue; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string StringPredefinedValue; + + public uint U32CurrentValue; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string StringCurrentValue; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetting)) | (1 << 16); + } + + [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)] + internal struct NvProfile + { + public uint Version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string ProfileName; + + [MarshalAs(UnmanagedType.ByValArray)] + public uint[] GPUSupport; + + public uint IsPredefined; + public uint NumOfApps; + public uint NumOfSettings; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvProfile)) | (1 << 16); + } + + [StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)] + internal struct NvApplication + { + public uint Version; + public uint IsPredefined; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string AppName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string UserFriendlyName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string Launcher; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)] + public string FileInFolder; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); + } + + internal enum NvStatus + { + OK = 0, // Success. Request is completed. + ERROR = -1, // Generic error + LIBRARY_NOT_FOUND = -2, // NVAPI support library cannot be loaded. + NO_IMPLEMENTATION = -3, // not implemented in current driver installation + API_NOT_INITIALIZED = -4, // Initialize has not been called (successfully) + INVALID_ARGUMENT = -5, // The argument/parameter value is not valid or NULL. + NVIDIA_DEVICE_NOT_FOUND = -6, // No NVIDIA display driver, or NVIDIA GPU driving a display, was found. + END_ENUMERATION = -7, // No more items to enumerate + INVALID_HANDLE = -8, // Invalid handle + INCOMPATIBLE_STRUCT_VERSION = -9, // An argument's structure version is not supported + HANDLE_INVALIDATED = -10, // The handle is no longer valid (likely due to GPU or display re-configuration) + OPENGL_CONTEXT_NOT_CURRENT = -11, // No NVIDIA OpenGL context is current (but needs to be) + INVALID_POINTER = -14, // An invalid pointer, usually NULL, was passed as a parameter + NO_GL_EXPERT = -12, // OpenGL Expert is not supported by the current drivers + INSTRUMENTATION_DISABLED = -13, // OpenGL Expert is supported, but driver instrumentation is currently disabled + NO_GL_NSIGHT = -15, // OpenGL does not support Nsight + + EXPECTED_LOGICAL_GPU_HANDLE = -100, // Expected a logical GPU handle for one or more parameters + EXPECTED_PHYSICAL_GPU_HANDLE = -101, // Expected a physical GPU handle for one or more parameters + EXPECTED_DISPLAY_HANDLE = -102, // Expected an NV display handle for one or more parameters + INVALID_COMBINATION = -103, // The combination of parameters is not valid. + NOT_SUPPORTED = -104, // Requested feature is not supported in the selected GPU + PORTID_NOT_FOUND = -105, // No port ID was found for the I2C transaction + EXPECTED_UNATTACHED_DISPLAY_HANDLE = -106, // Expected an unattached display handle as one of the input parameters. + INVALID_PERF_LEVEL = -107, // Invalid perf level + DEVICE_BUSY = -108, // Device is busy; request not fulfilled + NV_PERSIST_FILE_NOT_FOUND = -109, // NV persist file is not found + PERSIST_DATA_NOT_FOUND = -110, // NV persist data is not found + EXPECTED_TV_DISPLAY = -111, // Expected a TV output display + EXPECTED_TV_DISPLAY_ON_DCONNECTOR = -112, // Expected a TV output on the D Connector - HDTV_EIAJ4120. + NO_ACTIVE_SLI_TOPOLOGY = -113, // SLI is not active on this device. + SLI_RENDERING_MODE_NOTALLOWED = -114, // Setup of SLI rendering mode is not possible right now. + EXPECTED_DIGITAL_FLAT_PANEL = -115, // Expected a digital flat panel. + ARGUMENT_EXCEED_MAX_SIZE = -116, // Argument exceeds the expected size. + DEVICE_SWITCHING_NOT_ALLOWED = -117, // Inhibit is ON due to one of the flags in NV_GPU_DISPLAY_CHANGE_INHIBIT or SLI active. + TESTING_CLOCKS_NOT_SUPPORTED = -118, // Testing of clocks is not supported. + UNKNOWN_UNDERSCAN_CONFIG = -119, // The specified underscan config is from an unknown source (e.g. INF) + TIMEOUT_RECONFIGURING_GPU_TOPO = -120, // Timeout while reconfiguring GPUs + DATA_NOT_FOUND = -121, // Requested data was not found + EXPECTED_ANALOG_DISPLAY = -122, // Expected an analog display + NO_VIDLINK = -123, // No SLI video bridge is present + REQUIRES_REBOOT = -124, // NVAPI requires a reboot for the settings to take effect + INVALID_HYBRID_MODE = -125, // The function is not supported with the current Hybrid mode. + MIXED_TARGET_TYPES = -126, // The target types are not all the same + SYSWOW64_NOT_SUPPORTED = -127, // The function is not supported from 32-bit on a 64-bit system. + IMPLICIT_SET_GPU_TOPOLOGY_CHANGE_NOT_ALLOWED = -128, // There is no implicit GPU topology active. Use SetHybridMode to change topology. + REQUEST_USER_TO_CLOSE_NON_MIGRATABLE_APPS = -129, // Prompt the user to close all non-migratable applications. + OUT_OF_MEMORY = -130, // Could not allocate sufficient memory to complete the call. + WAS_STILL_DRAWING = -131, // The previous operation that is transferring information to or from this surface is incomplete. + FILE_NOT_FOUND = -132, // The file was not found. + TOO_MANY_UNIQUE_STATE_OBJECTS = -133, // There are too many unique instances of a particular type of state object. + INVALID_CALL = -134, // The method call is invalid. For example, a method's parameter may not be a valid pointer. + D3D10_1_LIBRARY_NOT_FOUND = -135, // d3d10_1.dll cannot be loaded. + FUNCTION_NOT_FOUND = -136, // Couldn't find the function in the loaded DLL. + INVALID_USER_PRIVILEGE = -137, // Current User is not Admin. + EXPECTED_NON_PRIMARY_DISPLAY_HANDLE = -138, // The handle corresponds to GDIPrimary. + EXPECTED_COMPUTE_GPU_HANDLE = -139, // Setting Physx GPU requires that the GPU is compute-capable. + STEREO_NOT_INITIALIZED = -140, // The Stereo part of NVAPI failed to initialize completely. Check if the stereo driver is installed. + STEREO_REGISTRY_ACCESS_FAILED = -141, // Access to stereo-related registry keys or values has failed. + STEREO_REGISTRY_PROFILE_TYPE_NOT_SUPPORTED = -142, // The given registry profile type is not supported. + STEREO_REGISTRY_VALUE_NOT_SUPPORTED = -143, // The given registry value is not supported. + STEREO_NOT_ENABLED = -144, // Stereo is not enabled and the function needed it to execute completely. + STEREO_NOT_TURNED_ON = -145, // Stereo is not turned on and the function needed it to execute completely. + STEREO_INVALID_DEVICE_INTERFACE = -146, // Invalid device interface. + STEREO_PARAMETER_OUT_OF_RANGE = -147, // Separation percentage or JPEG image capture quality is out of [0-100] range. + STEREO_FRUSTUM_ADJUST_MODE_NOT_SUPPORTED = -148, // The given frustum adjust mode is not supported. + TOPO_NOT_POSSIBLE = -149, // The mosaic topology is not possible given the current state of the hardware. + MODE_CHANGE_FAILED = -150, // An attempt to do a display resolution mode change has failed. + D3D11_LIBRARY_NOT_FOUND = -151, // d3d11.dll/d3d11_beta.dll cannot be loaded. + INVALID_ADDRESS = -152, // Address is outside of valid range. + STRING_TOO_SMALL = -153, // The pre-allocated string is too small to hold the result. + MATCHING_DEVICE_NOT_FOUND = -154, // The input does not match any of the available devices. + DRIVER_RUNNING = -155, // Driver is running. + DRIVER_NOTRUNNING = -156, // Driver is not running. + ERROR_DRIVER_RELOAD_REQUIRED = -157, // A driver reload is required to apply these settings. + SET_NOT_ALLOWED = -158, // Intended setting is not allowed. + ADVANCED_DISPLAY_TOPOLOGY_REQUIRED = -159, // Information can't be returned due to "advanced display topology". + SETTING_NOT_FOUND = -160, // Setting is not found. + SETTING_SIZE_TOO_LARGE = -161, // Setting size is too large. + TOO_MANY_SETTINGS_IN_PROFILE = -162, // There are too many settings for a profile. + PROFILE_NOT_FOUND = -163, // Profile is not found. + PROFILE_NAME_IN_USE = -164, // Profile name is duplicated. + PROFILE_NAME_EMPTY = -165, // Profile name is empty. + EXECUTABLE_NOT_FOUND = -166, // Application not found in the Profile. + EXECUTABLE_ALREADY_IN_USE = -167, // Application already exists in the other profile. + DATATYPE_MISMATCH = -168, // Data Type mismatch + PROFILE_REMOVED = -169, // The profile passed as parameter has been removed and is no longer valid. + UNREGISTERED_RESOURCE = -170, // An unregistered resource was passed as a parameter. + ID_OUT_OF_RANGE = -171, // The DisplayId corresponds to a display which is not within the normal outputId range. + DISPLAYCONFIG_VALIDATION_FAILED = -172, // Display topology is not valid so the driver cannot do a mode set on this configuration. + DPMST_CHANGED = -173, // Display Port Multi-Stream topology has been changed. + INSUFFICIENT_BUFFER = -174, // Input buffer is insufficient to hold the contents. + ACCESS_DENIED = -175, // No access to the caller. + MOSAIC_NOT_ACTIVE = -176, // The requested action cannot be performed without Mosaic being enabled. + SHARE_RESOURCE_RELOCATED = -177, // The surface is relocated away from video memory. + REQUEST_USER_TO_DISABLE_DWM = -178, // The user should disable DWM before calling NvAPI. + D3D_DEVICE_LOST = -179, // D3D device status is D3DERR_DEVICELOST or D3DERR_DEVICENOTRESET - the user has to reset the device. + INVALID_CONFIGURATION = -180, // The requested action cannot be performed in the current state. + STEREO_HANDSHAKE_NOT_DONE = -181, // Call failed as stereo handshake not completed. + EXECUTABLE_PATH_IS_AMBIGUOUS = -182, // The path provided was too short to determine the correct NVDRS_APPLICATION + DEFAULT_STEREO_PROFILE_IS_NOT_DEFINED = -183, // Default stereo profile is not currently defined + DEFAULT_STEREO_PROFILE_DOES_NOT_EXIST = -184, // Default stereo profile does not exist + CLUSTER_ALREADY_EXISTS = -185, // A cluster is already defined with the given configuration. + DPMST_DISPLAY_ID_EXPECTED = -186, // The input display id is not that of a multi stream enabled connector or a display device in a multi stream topology + INVALID_DISPLAY_ID = -187, // The input display id is not valid or the monitor associated to it does not support the current operation + STREAM_IS_OUT_OF_SYNC = -188, // While playing secure audio stream, stream goes out of sync + INCOMPATIBLE_AUDIO_DRIVER = -189, // Older audio driver version than required + VALUE_ALREADY_SET = -190, // Value already set, setting again not allowed. + TIMEOUT = -191, // Requested operation timed out + GPU_WORKSTATION_FEATURE_INCOMPLETE = -192, // The requested workstation feature set has incomplete driver internal allocation resources + STEREO_INIT_ACTIVATION_NOT_DONE = -193, // Call failed because InitActivation was not called. + SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled. + SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled. + INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer. + ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value. + ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed. + FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date. + FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported. + } + + internal enum NvSystemType + { + UNKNOWN = 0, + LAPTOP = 1, + DESKTOP = 2 + } + + internal enum NvGpuType + { + UNKNOWN = 0, + IGPU = 1, // Integrated + DGPU = 2, // Discrete + } + + internal enum NvSettingID : uint + { + OGL_AA_LINE_GAMMA_ID = 0x2089BF6C, + OGL_DEEP_COLOR_SCANOUT_ID = 0x2097C2F6, + OGL_DEFAULT_SWAP_INTERVAL_ID = 0x206A6582, + OGL_DEFAULT_SWAP_INTERVAL_FRACTIONAL_ID = 0x206C4581, + OGL_DEFAULT_SWAP_INTERVAL_SIGN_ID = 0x20655CFA, + OGL_EVENT_LOG_SEVERITY_THRESHOLD_ID = 0x209DF23E, + OGL_EXTENSION_STRING_VERSION_ID = 0x20FF7493, + OGL_FORCE_BLIT_ID = 0x201F619F, + OGL_FORCE_STEREO_ID = 0x204D9A0C, + OGL_IMPLICIT_GPU_AFFINITY_ID = 0x20D0F3E6, + OGL_MAX_FRAMES_ALLOWED_ID = 0x208E55E3, + OGL_MULTIMON_ID = 0x200AEBFC, + OGL_OVERLAY_PIXEL_TYPE_ID = 0x209AE66F, + OGL_OVERLAY_SUPPORT_ID = 0x206C28C4, + OGL_QUALITY_ENHANCEMENTS_ID = 0x20797D6C, + OGL_SINGLE_BACKDEPTH_BUFFER_ID = 0x20A29055, + OGL_THREAD_CONTROL_ID = 0x20C1221E, + OGL_TRIPLE_BUFFER_ID = 0x20FDD1F9, + OGL_VIDEO_EDITING_MODE_ID = 0x20EE02B4, + AA_BEHAVIOR_FLAGS_ID = 0x10ECDB82, + AA_MODE_ALPHATOCOVERAGE_ID = 0x10FC2D9C, + AA_MODE_GAMMACORRECTION_ID = 0x107D639D, + AA_MODE_METHOD_ID = 0x10D773D2, + AA_MODE_REPLAY_ID = 0x10D48A85, + AA_MODE_SELECTOR_ID = 0x107EFC5B, + AA_MODE_SELECTOR_SLIAA_ID = 0x107AFC5B, + ANISO_MODE_LEVEL_ID = 0x101E61A9, + ANISO_MODE_SELECTOR_ID = 0x10D2BB16, + APPLICATION_PROFILE_NOTIFICATION_TIMEOUT_ID = 0x104554B6, + APPLICATION_STEAM_ID_ID = 0x107CDDBC, + CPL_HIDDEN_PROFILE_ID = 0x106D5CFF, + CUDA_EXCLUDED_GPUS_ID = 0x10354FF8, + D3DOGL_GPU_MAX_POWER_ID = 0x10D1EF29, + EXPORT_PERF_COUNTERS_ID = 0x108F0841, + FXAA_ALLOW_ID = 0x1034CB89, + FXAA_ENABLE_ID = 0x1074C972, + FXAA_INDICATOR_ENABLE_ID = 0x1068FB9C, + MCSFRSHOWSPLIT_ID = 0x10287051, + OPTIMUS_MAXAA_ID = 0x10F9DC83, + PHYSXINDICATOR_ID = 0x1094F16F, + PREFERRED_PSTATE_ID = 0x1057EB71, + PREVENT_UI_AF_OVERRIDE_ID = 0x103BCCB5, + PS_FRAMERATE_LIMITER_ID = 0x10834FEE, + PS_FRAMERATE_LIMITER_GPS_CTRL_ID = 0x10834F01, + SHIM_MAXRES_ID = 0x10F9DC82, + SHIM_MCCOMPAT_ID = 0x10F9DC80, + SHIM_RENDERING_MODE_ID = 0x10F9DC81, + SHIM_RENDERING_OPTIONS_ID = 0x10F9DC84, + SLI_GPU_COUNT_ID = 0x1033DCD1, + SLI_PREDEFINED_GPU_COUNT_ID = 0x1033DCD2, + SLI_PREDEFINED_GPU_COUNT_DX10_ID = 0x1033DCD3, + SLI_PREDEFINED_MODE_ID = 0x1033CEC1, + SLI_PREDEFINED_MODE_DX10_ID = 0x1033CEC2, + SLI_RENDERING_MODE_ID = 0x1033CED1, + VRRFEATUREINDICATOR_ID = 0x1094F157, + VRROVERLAYINDICATOR_ID = 0x1095F16F, + VRRREQUESTSTATE_ID = 0x1094F1F7, + VSYNCSMOOTHAFR_ID = 0x101AE763, + VSYNCVRRCONTROL_ID = 0x10A879CE, + VSYNC_BEHAVIOR_FLAGS_ID = 0x10FDEC23, + WKS_API_STEREO_EYES_EXCHANGE_ID = 0x11AE435C, + WKS_API_STEREO_MODE_ID = 0x11E91A61, + WKS_MEMORY_ALLOCATION_POLICY_ID = 0x11112233, + WKS_STEREO_DONGLE_SUPPORT_ID = 0x112493BD, + WKS_STEREO_SUPPORT_ID = 0x11AA9E99, + WKS_STEREO_SWAP_MODE_ID = 0x11333333, + AO_MODE_ID = 0x00667329, + AO_MODE_ACTIVE_ID = 0x00664339, + AUTO_LODBIASADJUST_ID = 0x00638E8F, + ICAFE_LOGO_CONFIG_ID = 0x00DB1337, + LODBIASADJUST_ID = 0x00738E8F, + PRERENDERLIMIT_ID = 0x007BA09E, + PS_DYNAMIC_TILING_ID = 0x00E5C6C0, + PS_SHADERDISKCACHE_ID = 0x00198FFF, + PS_TEXFILTER_ANISO_OPTS2_ID = 0x00E73211, + PS_TEXFILTER_BILINEAR_IN_ANISO_ID = 0x0084CD70, + PS_TEXFILTER_DISABLE_TRILIN_SLOPE_ID = 0x002ECAF2, + PS_TEXFILTER_NO_NEG_LODBIAS_ID = 0x0019BB68, + QUALITY_ENHANCEMENTS_ID = 0x00CE2691, + REFRESH_RATE_OVERRIDE_ID = 0x0064B541, + SET_POWER_THROTTLE_FOR_PCIe_COMPLIANCE_ID = 0x00AE785C, + SET_VAB_DATA_ID = 0x00AB8687, + VSYNCMODE_ID = 0x00A879CF, + VSYNCTEARCONTROL_ID = 0x005A375C, + TOTAL_DWORD_SETTING_NUM = 80, + TOTAL_WSTRING_SETTING_NUM = 4, + TOTAL_SETTING_NUM = 84, + INVALID_SETTING_ID = 0xFFFFFFFF + } + + internal enum NvShimSetting : uint + { + SHIM_RENDERING_MODE_INTEGRATED = 0x00000000, + SHIM_RENDERING_MODE_ENABLE = 0x00000001, + SHIM_RENDERING_MODE_USER_EDITABLE = 0x00000002, + SHIM_RENDERING_MODE_MASK = 0x00000003, + SHIM_RENDERING_MODE_VIDEO_MASK = 0x00000004, + SHIM_RENDERING_MODE_VARYING_BIT = 0x00000008, + SHIM_RENDERING_MODE_AUTO_SELECT = 0x00000010, + SHIM_RENDERING_MODE_OVERRIDE_BIT = 0x80000000, + SHIM_RENDERING_MODE_NUM_VALUES = 8, + SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT + } + + internal enum NvThreadControlSetting : uint + { + OGL_THREAD_CONTROL_ENABLE = 0x00000001, + OGL_THREAD_CONTROL_DISABLE = 0x00000002, + OGL_THREAD_CONTROL_NUM_VALUES = 2, + OGL_THREAD_CONTROL_DEFAULT = 0 + } +} diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index a33e845f5b..b37b5cf6ca 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,6 +30,8 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { + NVAPI.ThreadedOptimisations = true; + // run Squirrel first, as the app may exit after these run if (OperatingSystem.IsWindows()) { From d38f8d9c783049f0c6a577383f3097a923ba635c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jan 2024 15:37:24 +0900 Subject: [PATCH 195/539] Change threaded optimisations setting to "Auto" on startup --- osu.Desktop/NVAPI.cs | 12 ++++++------ osu.Desktop/Program.cs | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index 9648ea1072..e1c5971e28 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -153,30 +153,30 @@ namespace osu.Desktop } } - public static bool ThreadedOptimisations + public static NvThreadControlSetting ThreadedOptimisations { get { if (!Available) - return false; + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; IntPtr profileHandle; if (!getProfile(out profileHandle, out _, out bool _)) - return false; + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; // Get the threaded optimisations setting NvSetting setting; if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting)) - return false; + return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; - return setting.U32CurrentValue != (uint)NvThreadControlSetting.OGL_THREAD_CONTROL_DISABLE; + return (NvThreadControlSetting)setting.U32CurrentValue; } set { if (!Available) return; - bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)(value ? NvThreadControlSetting.OGL_THREAD_CONTROL_ENABLE : NvThreadControlSetting.OGL_THREAD_CONTROL_DISABLE)); + bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value); Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!"); } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index b37b5cf6ca..a652e31f62 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,7 +30,10 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - NVAPI.ThreadedOptimisations = true; + // NVIDIA profiles are based on the executable name of a process. + // Lazer and stable share the same executable name. + // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. + NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; // run Squirrel first, as the app may exit after these run if (OperatingSystem.IsWindows()) From e686a6a1dddbe20c3c22dd1ec8cb572d7a9d8aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jan 2024 09:17:01 +0100 Subject: [PATCH 196/539] Fix player submission test intermittent failures due to audio playback discrepancy logic kicking in See https://github.com/ppy/osu/actions/runs/7384457927/job/20087439457#step:5:133. --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 6 ++++++ osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index f75a2656ef..96cfcd61c3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -359,6 +360,11 @@ namespace osu.Game.Tests.Visual.Gameplay AllowImportCompletion = new SemaphoreSlim(1); } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = false, + }; + protected override async Task ImportScore(Score score) { ScoreImportStarted = true; diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index a475f4823f..0d60ec4713 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -40,6 +40,12 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; + /// + /// Whether the audio playback rate should be validated. + /// Mostly disabled for tests. + /// + internal bool ShouldValidatePlaybackRate { get; init; } + /// /// Whether the audio playback is within acceptable ranges. /// Will become false if audio playback is not going as expected. @@ -223,6 +229,9 @@ namespace osu.Game.Screens.Play private void checkPlaybackValidity() { + if (!ShouldValidatePlaybackRate) + return; + if (GameplayClock.IsRunning) { elapsedGameplayClockTime += GameplayClock.ElapsedFrameTime; From 3df7430d2e278cbd3dacc173270053e6a415af07 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 3 Jan 2024 14:32:32 +0600 Subject: [PATCH 197/539] Bind `UserRankPanel` values to `Statistics` bindable in `APIAccess` --- .../Visual/Online/TestSceneUserPanel.cs | 18 ++++++++++++++++++ osu.Game/Users/UserRankPanel.cs | 17 +++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 81e17cd1b8..4df34e6244 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -157,6 +158,23 @@ namespace osu.Game.Tests.Visual.Online AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); } + [Test] + public void TestUserStatisticsChange() + { + AddStep("update statistics", () => + { + API.UpdateStatistics(new UserStatistics + { + GlobalRank = RNG.Next(100000), + CountryRank = RNG.Next(100000) + }); + }); + AddStep("set statistics to empty", () => + { + API.UpdateStatistics(new UserStatistics()); + }); + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 272d2eecb4..74ff0a06dd 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; @@ -23,6 +24,12 @@ namespace osu.Game.Users private const int padding = 10; private const int main_content_height = 80; + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private ProfileValueDisplay globalRankDisplay = null!; + private ProfileValueDisplay countryRankDisplay = null!; + public UserRankPanel(APIUser user) : base(user) { @@ -34,6 +41,12 @@ namespace osu.Game.Users private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + + api.Statistics.ValueChanged += e => + { + globalRankDisplay.Content = e.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + countryRankDisplay.Content = e.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + }; } protected override Drawable CreateLayout() @@ -166,12 +179,12 @@ namespace osu.Game.Users { new Drawable[] { - new ProfileValueDisplay(true) + globalRankDisplay = new ProfileValueDisplay(true) { Title = UsersStrings.ShowRankGlobalSimple, Content = User.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" }, - new ProfileValueDisplay(true) + countryRankDisplay = new ProfileValueDisplay(true) { Title = UsersStrings.ShowRankCountrySimple, Content = User.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" From d34f30f6ad1a2c5071eddb2ed1763465be3cc70b Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 3 Jan 2024 14:37:57 +0600 Subject: [PATCH 198/539] Add `Statistics` bindable to `IAPIProvider` and update it from `SoloStatisticsWatcher` --- osu.Game/Online/API/APIAccess.cs | 14 +++++++++++++- osu.Game/Online/API/DummyAPIAccess.cs | 14 ++++++++++++++ osu.Game/Online/API/IAPIProvider.cs | 10 ++++++++++ osu.Game/Online/Solo/SoloStatisticsWatcher.cs | 2 ++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index be5bdeca77..3910f1efea 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -53,6 +53,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; public IBindable Activity => activity; + public IBindable Statistics => statistics; public Language Language => game.CurrentLanguage.Value; @@ -65,6 +66,8 @@ namespace osu.Game.Online.API private Bindable configStatus { get; } = new Bindable(); private Bindable localUserStatus { get; } = new Bindable(); + private Bindable statistics { get; } = new Bindable(); + protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -517,9 +520,18 @@ namespace osu.Game.Online.API flushQueue(); } + public void UpdateStatistics(UserStatistics newStatistics) + { + statistics.Value = newStatistics; + } + private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); + private void setLocalUser(APIUser user) => Scheduler.Add(() => + { + localUser.Value = user; + statistics.Value = user.Statistics; + }, false); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index d585124db6..aa8658cc83 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,6 +28,8 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); + public Bindable Statistics { get; } = new Bindable(); + public Language Language => Language.en; public string AccessToken => "token"; @@ -115,6 +117,12 @@ namespace osu.Game.Online.API Id = DUMMY_USER_ID, }; + Statistics.Value = new UserStatistics + { + GlobalRank = 1, + CountryRank = 1 + }; + state.Value = APIState.Online; } @@ -126,6 +134,11 @@ namespace osu.Game.Online.API LocalUser.Value = new GuestUser(); } + public void UpdateStatistics(UserStatistics newStatistics) + { + Statistics.Value = newStatistics; + } + public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); @@ -141,6 +154,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + IBindable IAPIProvider.Statistics => Statistics; /// /// During the next simulated login, the process will fail immediately. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a1d7006c8c..b58d4a363a 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -28,6 +28,11 @@ namespace osu.Game.Online.API /// IBindable Activity { get; } + /// + /// The current user's online statistics. + /// + IBindable Statistics { get; } + /// /// The language supplied by this provider to API requests. /// @@ -111,6 +116,11 @@ namespace osu.Game.Online.API /// void Logout(); + /// + /// Sets Statistics bindable. + /// + void UpdateStatistics(UserStatistics newStatistics); + /// /// Constructs a new . May be null if not supported. /// diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs index 46449fea73..55b27fb364 100644 --- a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs +++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs @@ -127,6 +127,8 @@ namespace osu.Game.Online.Solo { string rulesetName = callback.Score.Ruleset.ShortName; + api.UpdateStatistics(updatedStatistics); + if (latestStatistics == null) return; From 474c416bd9985aab49f895054189ea5b61b40949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jan 2024 10:05:11 +0100 Subject: [PATCH 199/539] Make `dotnet format` shut up about naming --- osu.Desktop/NVAPI.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index e1c5971e28..bb3a59cc7f 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -3,6 +3,8 @@ #nullable disable +#pragma warning disable IDE1006 // Naming rule violation + using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; From 04147eb68988e4ce7f94f5d36def2c8669821508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jan 2024 11:46:26 +0100 Subject: [PATCH 200/539] Fix lack of correct default value spec --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0d60ec4713..8b8bf87436 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play /// Whether the audio playback rate should be validated. /// Mostly disabled for tests. /// - internal bool ShouldValidatePlaybackRate { get; init; } + internal bool ShouldValidatePlaybackRate { get; init; } = true; /// /// Whether the audio playback is within acceptable ranges. From 4312e9e9a9c70bd7a139833108edca8dd4ff63e3 Mon Sep 17 00:00:00 2001 From: CaffeeLake Date: Wed, 3 Jan 2024 20:17:05 +0900 Subject: [PATCH 201/539] Add Test: floating point errors --- osu.Game.Tests/Mods/ModUtilsTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 4f10a0ea54..f1db146f3a 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -315,6 +315,10 @@ namespace osu.Game.Tests.Mods { Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9999).ToString(), "0.99x"); Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0001).ToString(), "1.01x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.899999999999999).ToString(), "0.90x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.099999999999999).ToString(), "1.10x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.900000000000001).ToString(), "0.90x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.100000000000001).ToString(), "1.10x"); } public abstract class CustomMod1 : Mod, IModCompatibilitySpecification From 7262fef67f5c0abc51b78f8aa643c3b68d9bbbe3 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 3 Jan 2024 17:39:48 +0600 Subject: [PATCH 202/539] Add comments --- osu.Game/Users/UserGridPanel.cs | 2 ++ osu.Game/Users/UserPanel.cs | 17 ++++++++++------- osu.Game/Users/UserRankPanel.cs | 6 ++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index aac2315b2f..fe3435c248 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -91,6 +91,7 @@ namespace osu.Game.Users Children = new Drawable[] { CreateFlag(), + // supporter icon is being added later } } }, @@ -108,6 +109,7 @@ namespace osu.Game.Users }, new[] { + // padding Empty(), Empty() }, diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index d38f5d8ccc..ef5c95c422 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -100,13 +100,9 @@ namespace osu.Game.Users protected abstract Drawable CreateLayout(); - protected OsuSpriteText CreateUsername() => new OsuSpriteText - { - Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), - Shadow = false, - Text = User.Username, - }; - + /// + /// Panel background container. Can be null if a panel doesn't want a background under it's layout + /// protected virtual Drawable? CreateBackground() => Background = new UserCoverBackground { RelativeSizeAxes = Axes.Both, @@ -115,6 +111,13 @@ namespace osu.Game.Users User = User }; + protected OsuSpriteText CreateUsername() => new OsuSpriteText + { + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), + Shadow = false, + Text = User.Username, + }; + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 74ff0a06dd..a2e234cf82 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -96,6 +96,7 @@ namespace osu.Game.Users { new[] { + // padding Empty(), Empty(), Empty(), @@ -103,7 +104,7 @@ namespace osu.Game.Users }, new[] { - Empty(), + Empty(), // padding CreateAvatar().With(avatar => { avatar.Size = new Vector2(60); @@ -138,6 +139,7 @@ namespace osu.Game.Users Children = new Drawable[] { CreateFlag(), + // supporter icon is being added later } } }, @@ -152,7 +154,7 @@ namespace osu.Game.Users } } }, - Empty() + Empty() // padding } } } From e240443c467436a470c12fd77b3035b7720a8cdd Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 3 Jan 2024 18:15:32 +0600 Subject: [PATCH 203/539] Update `LocalUser` statistics, add test --- .../Online/TestSceneSoloStatisticsWatcher.cs | 20 +++++++++++++++++++ osu.Game/Online/API/APIAccess.cs | 3 +++ osu.Game/Online/API/DummyAPIAccess.cs | 3 +++ 3 files changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs index e62e53bd02..be819afa3e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs @@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online AddAssert("update not received", () => update == null); } + [Test] + public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed() + { + int userId = getUserId(); + long scoreId = getScoreId(); + setUpUser(userId); + + var ruleset = new OsuRuleset().RulesetInfo; + + SoloStatisticsUpdate? update = null; + registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); + + feignScoreProcessing(userId, ruleset, 5_000_000); + + AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); + AddUntilStep("update received", () => update != null); + AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + } + private int nextUserId = 2000; private long nextScoreId = 50000; diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 3910f1efea..17bf8bcc37 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -523,6 +523,9 @@ namespace osu.Game.Online.API public void UpdateStatistics(UserStatistics newStatistics) { statistics.Value = newStatistics; + + if (IsLoggedIn) + localUser.Value.Statistics = newStatistics; } private static APIUser createGuestUser() => new GuestUser(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index aa8658cc83..4b4f8061e0 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -137,6 +137,9 @@ namespace osu.Game.Online.API public void UpdateStatistics(UserStatistics newStatistics) { Statistics.Value = newStatistics; + + if (IsLoggedIn) + LocalUser.Value.Statistics = newStatistics; } public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; From 2c64db06287b40811e742f785664eafac09093c5 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Thu, 4 Jan 2024 12:15:48 +0900 Subject: [PATCH 204/539] Use already existing message placeholder + localized string --- osu.Game/Screens/Ranking/ResultsScreen.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e18ab63706..8823e4d320 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -19,10 +19,11 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Statistics; @@ -208,13 +209,7 @@ namespace osu.Game.Screens.Ranking if (ScorePanelList.IsEmpty) { // This can happen if a beatmap part of a playlist hasn't been played yet. - VerticalScrollContent.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular), - Text = "no scores yet!", - }); + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); } else { From 92c45662874d2c4e3992124a8aeabcca90fa1862 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Thu, 4 Jan 2024 12:20:05 +0900 Subject: [PATCH 205/539] Fix typo --- osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index d4b6bc2b91..6c87553971 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -275,7 +275,7 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed AddStep("set content", () => { markdownContainer.Text = @" -This is a paragraph containing `inline code` synatax. +This is a paragraph containing `inline code` syntax. Oh wow I do love the `WikiMarkdownContainer`, it is very cool! This is a line before the fenced code block: From 659118c04353bca73182a82e6d48c533779f6777 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Thu, 4 Jan 2024 12:20:51 +0900 Subject: [PATCH 206/539] Fix wiki link path inconsistencies If I access https://osu.ppy.sh/wiki/en/MAIN_PAGE or use any other capitalization my browser always redirects me to https://osu.ppy.sh/wiki/en/Main_page so I think Main_page is the correct capitalization. This might slightly reduce loading time? No idea though. Probably negligible if so. --- osu.Game/Overlays/Wiki/WikiHeader.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 24eddeb0c2..55be05ed7a 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Wiki { public partial class WikiHeader : BreadcrumbControlOverlayHeader { - private const string index_path = "Main_Page"; + private const string index_path = "Main_page"; public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex; diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index c816eca776..440e451201 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays { public partial class WikiOverlay : OnlineOverlay { - private const string index_path = @"main_page"; + private const string index_path = "Main_page"; public string CurrentPath => path.Value; @@ -161,7 +161,7 @@ namespace osu.Game.Overlays path.Value = "error"; LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", - $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page](Main_Page).")); + $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page](Main_page).")); } private void showParentPage() From ddc8a647640ee1c6f4c2dcf005cb7dad0b9e4700 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 14:55:52 +0900 Subject: [PATCH 207/539] Reduce spinner glow It was horrible --- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs index 88769442a1..76afeeb2c4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon RelativeSizeAxes = Axes.Both, InnerRadius = arc_radius, RoundedCaps = true, - GlowColour = new Color4(171, 255, 255, 255) + GlowColour = new Color4(171, 255, 255, 180) } }; } From cf5f0a2bdc79e50b178f9b41d08a7ab52b1b5a9f Mon Sep 17 00:00:00 2001 From: wooster0 Date: Thu, 4 Jan 2024 15:01:27 +0900 Subject: [PATCH 208/539] Make chat commands case-insensitive Would be nice if I accidentally have caps lock enabled and write "/HELP" it still works. --- .../Chat/TestSceneChannelManager.cs | 19 ++++++++++++++++++- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 3a4c55c65c..eae12edebd 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Chat }); AddStep("post message", () => channelManager.PostMessage("Something interesting")); - AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage)); + AddUntilStep("message posted", () => !channel.Messages.Any(m => m is LocalMessage)); AddStep("post /help command", () => channelManager.PostCommand("help", channel)); AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel)); @@ -146,6 +146,23 @@ namespace osu.Game.Tests.Chat AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty); } + [Test] + public void TestCommandNameCaseInsensitivity() + { + Channel channel = null; + + AddStep("join channel and select it", () => + { + channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public)); + channelManager.CurrentChannel.Value = channel; + }); + + AddStep("post /me command", () => channelManager.PostCommand("ME DANCES")); + AddUntilStep("/me command received", () => channel.Messages.Last().Content.Contains("DANCES")); + AddStep("post /help command", () => channelManager.PostCommand("HeLp")); + AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands")); + } + private void handlePostMessageRequest(PostMessageRequest request) { var message = new Message(++currentMessageId) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index e95bc128c8..23989caae2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -247,7 +247,7 @@ namespace osu.Game.Online.Chat string command = parameters[0]; string content = parameters.Length == 2 ? parameters[1] : string.Empty; - switch (command) + switch (command.ToLowerInvariant()) { case "np": AddInternal(new NowPlayingCommand(target)); From cd9bf0c753c0d4e6ca86c4ccc17fe1a668654e8e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 3 Jan 2024 21:30:46 -0800 Subject: [PATCH 209/539] Flash blocking ongoing operations dialog when trying to force quit --- osu.Game/Screens/Menu/MainMenu.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 516b090a16..14c950d726 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -25,6 +26,7 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; @@ -390,7 +392,12 @@ namespace osu.Game.Screens.Menu if (requiresConfirmation) { if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog) - exitDialog.PerformOkAction(); + { + if (exitDialog.Buttons.OfType().FirstOrDefault() != null) + exitDialog.PerformOkAction(); + else + exitDialog.Flash(); + } else { dialogOverlay.Push(new ConfirmExitDialog(() => From ea714c86d427cb6fa498b1d76a02e6ee389dae6f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 3 Jan 2024 22:22:25 -0800 Subject: [PATCH 210/539] Fix potential null reference with flash sample when exiting rapidly Fixes `TestForceExitWithOperationInProgress()`. --- osu.Game/Overlays/Dialog/PopupDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 4048b35e78..4ac37a63e2 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Dialog private readonly Vector2 ringMinifiedSize = new Vector2(20f); private readonly Box flashLayer; - private Sample flashSample = null!; + private Sample? flashSample; private readonly Container content; private readonly Container ring; @@ -267,7 +267,7 @@ namespace osu.Game.Overlays.Dialog flashLayer.FadeInFromZero(80, Easing.OutQuint) .Then() .FadeOutFromOne(1500, Easing.OutQuint); - flashSample.Play(); + flashSample?.Play(); } protected override bool OnKeyDown(KeyDownEvent e) From 1beb3f5462329262b902472227a3b6c097714092 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 15:45:13 +0900 Subject: [PATCH 211/539] Add failing test coverage of fast streams not working in editor --- .../TestSceneEditorAutoplayFastStreams.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs new file mode 100644 index 0000000000..cf5cd809ef --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + /// + /// This test covers autoplay working correctly in the editor on fast streams. + /// Might seem like a weird test, but frame stability being toggled can cause autoplay to operation incorrectly. + /// This is clearly a bug with the autoplay algorithm, but is worked around at an editor level for now. + /// + public partial class TestSceneEditorAutoplayFastStreams : EditorTestScene + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var testBeatmap = new TestBeatmap(ruleset, false); + testBeatmap.HitObjects.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 530 }, + new HitCircle { StartTime = 560 }, + new HitCircle { StartTime = 590 }, + new HitCircle { StartTime = 620 }, + }); + + return testBeatmap; + } + + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + [Test] + public void TestAllHit() + { + AddStep("start playback", () => EditorClock.Start()); + AddUntilStep("wait for all hit", () => + { + DrawableHitCircle[] hitCircles = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).ToArray(); + + return hitCircles.Length == 5 && hitCircles.All(h => h.IsHit); + }); + } + } +} From 65c29b4f09d49d772cfe2c9934c4d1ee65e9b384 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 13:18:41 +0900 Subject: [PATCH 212/539] Make editor remain frame stable during normal playback --- .../Edit/DrawableEditorRulesetWrapper.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 174b278d89..ebf06bcc4e 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -1,6 +1,7 @@ // 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.Extensions.ObjectExtensions; @@ -26,6 +27,9 @@ namespace osu.Game.Rulesets.Edit [Resolved] private EditorBeatmap beatmap { get; set; } = null!; + [Resolved] + private EditorClock editorClock { get; set; } = null!; + public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -38,7 +42,6 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -65,6 +68,22 @@ namespace osu.Game.Rulesets.Edit Scheduler.AddOnce(regenerateAutoplay); } + protected override void Update() + { + base.Update(); + + // Whenever possible, we want to stay in frame stability playback. + // Without doing so, we run into bugs with some gameplay elements not behaving as expected. + // + // Note that this is not using EditorClock.IsSeeking as that would exit frame stability + // on all seeks. The intention here is to retain frame stability for small seeks. + // + // I still think no gameplay elements should require frame stability in the first place, but maybe that ship has sailed already.. + bool shouldBypassFrameStability = Math.Abs(drawableRuleset.FrameStableClock.CurrentTime - editorClock.CurrentTime) > 1000; + + drawableRuleset.FrameStablePlayback = !shouldBypassFrameStability; + } + private void regenerateAutoplay() { var autoplayMod = drawableRuleset.Mods.OfType().Single(); From 3419c59b062ba0ac9670d9279029d6fbb062aac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 15:33:18 +0900 Subject: [PATCH 213/539] Fix failing tests due to frame stable seeks taking longer --- .../Editor/TestSceneOsuComposerSelection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 623cefff6b..366f17daee 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -46,10 +46,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToObject(() => slider); AddStep("seek after end", () => EditorClock.Seek(750)); + AddUntilStep("wait for seek", () => !EditorClock.IsSeeking); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); AddStep("seek to visible", () => EditorClock.Seek(650)); + AddUntilStep("wait for seek", () => !EditorClock.IsSeeking); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); } From b1813b17a2f84b5ee318a6db89fdff588356678b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 16:39:50 +0900 Subject: [PATCH 214/539] Few new rider inspection --- osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 69070b0b64..76ed5063b0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing if (sameRuleset) { AddUntilStep("prompt for save dialog shown", () => DialogOverlay.CurrentDialog is PromptForSaveDialog); - AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog).PerformOkAction()); + AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog)?.PerformOkAction()); } // ensure editor loader didn't resume. From df99a37254b930b7369c754d09b3ad2ce9d115c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 17:04:36 +0900 Subject: [PATCH 215/539] Fix another realm null inspection --- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index bb4e06654a..015c2c62ed 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -132,8 +132,8 @@ namespace osu.Game.Tests.Beatmaps public AudioManager AudioManager => Audio; public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; - public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; - RealmAccess IStorageResourceProvider.RealmAccess => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => throw new NotImplementedException(); + RealmAccess IStorageResourceProvider.RealmAccess => throw new NotImplementedException(); #endregion From f0aeeeea966f06add12cf2bca3dd48dac8573e82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 17:13:21 +0900 Subject: [PATCH 216/539] ...in a safer way --- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 015c2c62ed..1f491be7e3 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -132,8 +132,8 @@ namespace osu.Game.Tests.Beatmaps public AudioManager AudioManager => Audio; public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; - public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => throw new NotImplementedException(); - RealmAccess IStorageResourceProvider.RealmAccess => throw new NotImplementedException(); + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null!; + RealmAccess IStorageResourceProvider.RealmAccess => null!; #endregion From 0bbc27e380b7fb2d430322edba27e8b0122f583f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 16:41:52 +0900 Subject: [PATCH 217/539] Add a gameplay configuration flag to disable fail animation --- .../Multiplayer/MultiplayerPlayer.cs | 4 +- osu.Game/Screens/Play/GameplayState.cs | 2 +- osu.Game/Screens/Play/Player.cs | 57 +++++++++++-------- osu.Game/Screens/Play/PlayerConfiguration.cs | 6 ++ 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index e6d9dd4cd0..d9043df1d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { protected override bool PauseOnFocusLost => false; - // Disallow fails in multiplayer for now. - protected override bool CheckModsAllowFailure() => false; - protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] @@ -55,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AllowPause = false, AllowRestart = false, + AllowFailAnimation = false, AllowSkipping = room.AutoSkip.Value, AutomaticallySkipIntro = room.AutoSkip.Value, AlwaysShowLeaderboard = true, diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index c2162d4df2..cc399a0fbe 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play public bool HasPassed { get; set; } /// - /// Whether the user failed during gameplay. + /// Whether the user failed during gameplay. This is only set when the gameplay session has completed due to the fail. /// public bool HasFailed { get; set; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c960ac357f..df50e35986 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -735,7 +735,7 @@ namespace osu.Game.Screens.Play } // Only show the completion screen if the player hasn't failed - if (HealthProcessor.HasFailed) + if (GameplayState.HasFailed) return; GameplayState.HasPassed = true; @@ -924,37 +924,44 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - Debug.Assert(!GameplayState.HasFailed); - Debug.Assert(!GameplayState.HasPassed); - Debug.Assert(!GameplayState.HasQuit); + if (Configuration.AllowFailAnimation) + { + Debug.Assert(!GameplayState.HasFailed); + Debug.Assert(!GameplayState.HasPassed); + Debug.Assert(!GameplayState.HasQuit); - GameplayState.HasFailed = true; + GameplayState.HasFailed = true; - updateGameplayState(); + updateGameplayState(); - // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) - // could process an extra frame after the GameplayClock is stopped. - // In such cases we want the fail state to precede a user triggered pause. - if (PauseOverlay.State.Value == Visibility.Visible) - PauseOverlay.Hide(); + // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) + // could process an extra frame after the GameplayClock is stopped. + // In such cases we want the fail state to precede a user triggered pause. + if (PauseOverlay.State.Value == Visibility.Visible) + PauseOverlay.Hide(); - failAnimationContainer.Start(); + failAnimationContainer.Start(); - // Failures can be triggered either by a judgement, or by a mod. - // - // For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received - // the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above). - // - // A schedule here ensures that any lingering judgements from the current frame are applied before we - // finalise the score as "failed". - Schedule(() => + // Failures can be triggered either by a judgement, or by a mod. + // + // For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received + // the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above). + // + // A schedule here ensures that any lingering judgements from the current frame are applied before we + // finalise the score as "failed". + Schedule(() => + { + ScoreProcessor.FailScore(Score.ScoreInfo); + OnFail(); + + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) + Restart(true); + }); + } + else { ScoreProcessor.FailScore(Score.ScoreInfo); - OnFail(); - - if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(true); - }); + } return true; } diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 122e25f406..466a691118 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -15,6 +15,12 @@ namespace osu.Game.Screens.Play /// public bool ShowResults { get; set; } = true; + /// + /// Whether the fail animation / screen should be triggered on failing. + /// If false, the score will still be marked as failed but gameplay will continue. + /// + public bool AllowFailAnimation { get; set; } = true; + /// /// Whether the player should be allowed to trigger a restart. /// From 705f25e4b990c0ab726c29d6541121f7eb47b034 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 16:42:05 +0900 Subject: [PATCH 218/539] Make `ScoreProcessor.Rank` read-only --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 3 --- osu.Game/Rulesets/Mods/ModHidden.cs | 2 -- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 14 ++++++++------ osu.Game/Screens/Play/BreakOverlay.cs | 4 +++- osu.Game/Screens/Play/Player.cs | 2 -- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 95406cc9e6..dc2ad6f47e 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -56,9 +56,6 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { Combo.BindTo(scoreProcessor.Combo); - - // Default value of ScoreProcessor's Rank in Flashlight Mod should be SS+ - scoreProcessor.Rank.Value = ScoreRank.XH; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index 5a8226115f..8b25768575 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - // Default value of ScoreProcessor's Rank in Hidden Mod should be SS+ - scoreProcessor.Rank.Value = ScoreRank.XH; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 837bb4080e..4ef65c55ab 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -86,7 +86,9 @@ namespace osu.Game.Rulesets.Scoring /// /// The current rank. /// - public readonly Bindable Rank = new Bindable(ScoreRank.X); + public IBindable Rank => rank; + + private readonly Bindable rank = new Bindable(ScoreRank.X); /// /// The highest combo achieved by this score. @@ -186,9 +188,9 @@ namespace osu.Game.Rulesets.Scoring Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += accuracy => { - Rank.Value = RankFromAccuracy(accuracy.NewValue); + rank.Value = RankFromAccuracy(accuracy.NewValue); foreach (var mod in Mods.Value.OfType()) - Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); + rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); }; Mods.ValueChanged += mods => @@ -411,8 +413,8 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = 0; Accuracy.Value = 1; Combo.Value = 0; - Rank.Disabled = false; - Rank.Value = ScoreRank.X; + rank.Disabled = false; + rank.Value = ScoreRank.X; HighestCombo.Value = 0; } @@ -448,7 +450,7 @@ namespace osu.Game.Rulesets.Scoring return; score.Passed = false; - Rank.Value = ScoreRank.F; + rank.Value = ScoreRank.F; PopulateScore(score); } diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 3ca82ec00b..e18612c955 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -4,12 +4,14 @@ #nullable disable using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play.Break; namespace osu.Game.Screens.Play @@ -113,7 +115,7 @@ namespace osu.Game.Screens.Play if (scoreProcessor != null) { info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); - info.GradeDisplay.Current.BindTo(scoreProcessor.Rank); + ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index df50e35986..b87306b9a2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -801,8 +801,6 @@ namespace osu.Game.Screens.Play // This player instance may already be in the process of exiting. return; - Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F); - this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely())); }, Time.Current + delay, 50); From a4dee1a01af5c572f4fdd48c58caa754e542a32b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 16:51:25 +0900 Subject: [PATCH 219/539] Don't unset `Disabled` on rank (never actually disabled?) --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4ef65c55ab..6d2b43a3e7 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -413,7 +413,6 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = 0; Accuracy.Value = 1; Combo.Value = 0; - rank.Disabled = false; rank.Value = ScoreRank.X; HighestCombo.Value = 0; } From b12011d501ab28a34932176ab72854f4ed19fa52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 17:04:45 +0900 Subject: [PATCH 220/539] Avoid rank updates after failing --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 6d2b43a3e7..13c5d523da 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -188,6 +188,10 @@ namespace osu.Game.Rulesets.Scoring Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += accuracy => { + // Once failed, we shouldn't update the rank anymore. + if (rank.Value == ScoreRank.F) + return; + rank.Value = RankFromAccuracy(accuracy.NewValue); foreach (var mod in Mods.Value.OfType()) rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); From 44d35020d16a1f445abd65758dd37a939de44b3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 17:54:52 +0900 Subject: [PATCH 221/539] Add test coverage of failed multiplayer score --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 16030d568b..462286335a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -690,6 +691,13 @@ namespace osu.Game.Tests.Visual.Multiplayer } AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); + + AddAssert("check is fail", () => + { + var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score; + + return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F; + }); } [Test] From fc1a2c594cbf53322b8e10aea3ce173dd6d9d82d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 18:02:37 +0900 Subject: [PATCH 222/539] Add a bit more test coverage --- osu.Game.Tests/Mods/ModUtilsTest.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index f1db146f3a..13da69871e 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -314,11 +314,20 @@ namespace osu.Game.Tests.Mods public void TestFormatScoreMultiplier() { Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9999).ToString(), "0.99x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0).ToString(), "1.00x"); Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0001).ToString(), "1.01x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.899999999999999).ToString(), "0.90x"); - Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.099999999999999).ToString(), "1.10x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9).ToString(), "0.90x"); Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.900000000000001).ToString(), "0.90x"); + + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.099999999999999).ToString(), "1.10x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.1).ToString(), "1.10x"); Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.100000000000001).ToString(), "1.10x"); + + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.045).ToString(), "1.05x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.05).ToString(), "1.05x"); + Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); } public abstract class CustomMod1 : Mod, IModCompatibilitySpecification From adac3b65cea7a896907ed5451abedc1e241e866f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 18:48:13 +0900 Subject: [PATCH 223/539] Fix beatmap carousel not preloading panels when off-screen --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 73 +++++++++++++------ 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 89911c9a69..3353aeed7d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -96,12 +96,12 @@ namespace osu.Game.Screens.Select /// /// Extend the range to retain already loaded pooled drawables. /// - private const float distance_offscreen_before_unload = 1024; + private const float distance_offscreen_before_unload = 2048; /// /// Extend the range to update positions / retrieve pooled drawables outside of visible range. /// - private const float distance_offscreen_to_preload = 512; // todo: adjust this appropriately once we can make set panel contents load while off-screen. + private const float distance_offscreen_to_preload = 768; /// /// Whether carousel items have completed asynchronously loaded. diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index f16e92a82a..c24e09582e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -46,6 +47,8 @@ namespace osu.Game.Screens.Select.Carousel private MenuItem[]? mainMenuItems; + private double timeSinceUnpool; + [Resolved] private BeatmapManager manager { get; set; } = null!; @@ -54,6 +57,7 @@ namespace osu.Game.Screens.Select.Carousel base.FreeAfterUse(); Item = null; + timeSinceUnpool = 0; ClearTransforms(); } @@ -92,13 +96,21 @@ namespace osu.Game.Screens.Select.Carousel // algorithm for this is taken from ScrollContainer. // while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct. Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed)); + + loadContentIfRequired(); } + private CancellationTokenSource? loadCancellation; + protected override void UpdateItem() { + loadCancellation?.Cancel(); + loadCancellation = null; + base.UpdateItem(); Content.Clear(); + Header.Clear(); beatmapContainer = null; beatmapsLoadTask = null; @@ -107,32 +119,8 @@ namespace osu.Game.Screens.Select.Carousel return; beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet; - - DelayedLoadWrapper background; - DelayedLoadWrapper mainFlow; - - Header.Children = new Drawable[] - { - // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). - background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) - { - RelativeSizeAxes = Axes.Both, - }, 200) - { - RelativeSizeAxes = Axes.Both - }, - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 50) - { - RelativeSizeAxes = Axes.Both - }, - }; - - background.DelayedLoadComplete += fadeContentIn; - mainFlow.DelayedLoadComplete += fadeContentIn; } - private void fadeContentIn(Drawable d) => d.FadeInFromZero(150); - protected override void Deselected() { base.Deselected(); @@ -190,6 +178,43 @@ namespace osu.Game.Screens.Select.Carousel } } + private void loadContentIfRequired() + { + // Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen + // to provide a better user experience. + + // This is tracking time that this drawable is updating since the last pool. + // This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel) + // don't cause huge overheads. + const double time_updating_before_load = 150; + + Debug.Assert(Item != null); + + if (loadCancellation == null && (timeSinceUnpool += Time.Elapsed) > time_updating_before_load) + { + loadCancellation = new CancellationTokenSource(); + + LoadComponentAsync(new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) + { + RelativeSizeAxes = Axes.Both, + }, background => + { + Header.Add(background); + background.FadeInFromZero(150); + }, loadCancellation.Token); + + LoadComponentAsync(new SetPanelContent((CarouselBeatmapSet)Item) + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + }, mainFlow => + { + Header.Add(mainFlow); + mainFlow.FadeInFromZero(150); + }, loadCancellation.Token); + } + } + private void updateBeatmapYPositions() { if (beatmapContainer == null) From 81c6fd5589751e125a8728ad57eb286dc4b146f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 19:13:36 +0900 Subject: [PATCH 224/539] Load items closer to the centre of the screen as a priority --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3353aeed7d..4408634787 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -108,6 +108,7 @@ namespace osu.Game.Screens.Select /// public bool BeatmapSetsLoaded { get; private set; } + [Cached] protected readonly CarouselScrollContainer Scroll; private readonly NoResultsPlaceholder noResultsPlaceholder; @@ -1251,7 +1252,7 @@ namespace osu.Game.Screens.Select } } - protected partial class CarouselScrollContainer : UserTrackingScrollContainer + public partial class CarouselScrollContainer : UserTrackingScrollContainer { private bool rightMouseScrollBlocked; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index c24e09582e..61658526db 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -8,9 +8,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -178,39 +180,44 @@ namespace osu.Game.Screens.Select.Carousel } } + [Resolved] + private BeatmapCarousel.CarouselScrollContainer scrollContainer { get; set; } = null!; + private void loadContentIfRequired() { + Quad containingSsdq = scrollContainer.ScreenSpaceDrawQuad; + // Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen // to provide a better user experience. // This is tracking time that this drawable is updating since the last pool. // This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel) // don't cause huge overheads. - const double time_updating_before_load = 150; + // + // We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first. + float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100; Debug.Assert(Item != null); - if (loadCancellation == null && (timeSinceUnpool += Time.Elapsed) > time_updating_before_load) + if (loadCancellation == null && (timeSinceUnpool += Time.Elapsed) > timeUpdatingBeforeLoad) { loadCancellation = new CancellationTokenSource(); - LoadComponentAsync(new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) + LoadComponentsAsync(new CompositeDrawable[] { - RelativeSizeAxes = Axes.Both, - }, background => + new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) + { + RelativeSizeAxes = Axes.Both, + }, + new SetPanelContent((CarouselBeatmapSet)Item) + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + } + }, drawables => { - Header.Add(background); - background.FadeInFromZero(150); - }, loadCancellation.Token); - - LoadComponentAsync(new SetPanelContent((CarouselBeatmapSet)Item) - { - Depth = float.MinValue, - RelativeSizeAxes = Axes.Both, - }, mainFlow => - { - Header.Add(mainFlow); - mainFlow.FadeInFromZero(150); + Header.AddRange(drawables); + drawables.ForEach(d => d.FadeInFromZero(150)); }, loadCancellation.Token); } } From 085f5acd1abaf8cf309c5e2ccb12df367a19cc33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jan 2024 19:26:28 +0900 Subject: [PATCH 225/539] Fix another rider inspection (why do these keep coming up at random) --- osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 8c32135cfd..8691f46605 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Online { var cardContainer = this.ChildrenOfType>().Single().Parent; var expandedContent = this.ChildrenOfType().Single(); - return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer.ScreenSpaceDrawQuad.Contains(v)); + return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer!.ScreenSpaceDrawQuad.Contains(v)); }); } From 9b734bac2515c5a8cf61ca3cc7576bf1e012a4b9 Mon Sep 17 00:00:00 2001 From: Zachary Date: Fri, 5 Jan 2024 01:14:34 +1000 Subject: [PATCH 226/539] Allow track control after intro screen finishes. --- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Menu/MainMenu.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0986c0513c..01086d3e33 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -43,7 +43,7 @@ namespace osu.Game.Overlays /// /// Whether user control of the global track should be allowed. /// - public readonly BindableBool AllowTrackControl = new BindableBool(true); + public readonly BindableBool AllowTrackControl = new BindableBool(false); /// /// Fired when the global has changed. diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 516b090a16..ffb68367c2 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -241,6 +241,8 @@ namespace osu.Game.Screens.Menu { var track = musicController.CurrentTrack; + musicController.AllowTrackControl.Value = true; + // presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now. if (!track.IsRunning) { From 91bb3f6c57d7263876d5de57bab01c5d4b444b9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 01:24:00 +0900 Subject: [PATCH 227/539] Cache argon character glyph lookups to reduce string allocations --- .../Play/HUD/ArgonCounterTextComponent.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 2a3f4365cb..266dfb3301 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -137,33 +139,48 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(TextureStore textures) { + const string font_name = @"argon-counter"; + Spacing = new Vector2(-2f, 0f); - Font = new FontUsage(@"argon-counter", 1); - glyphStore = new GlyphStore(textures, getLookup); + Font = new FontUsage(font_name, 1); + glyphStore = new GlyphStore(font_name, textures, getLookup); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); private class GlyphStore : ITexturedGlyphLookupStore { + private readonly string fontName; private readonly TextureStore textures; private readonly Func getLookup; - public GlyphStore(TextureStore textures, Func getLookup) + private readonly Dictionary cache = new Dictionary(); + + public GlyphStore(string fontName, TextureStore textures, Func getLookup) { + this.fontName = fontName; this.textures = textures; this.getLookup = getLookup; } public ITexturedCharacterGlyph? Get(string? fontName, char character) { + // We only service one font. + Debug.Assert(fontName == this.fontName); + + if (cache.TryGetValue(character, out var cached)) + return cached; + string lookup = getLookup(character); var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}"); - if (texture == null) - return null; + TexturedCharacterGlyph? glyph = null; - return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f); + if (texture != null) + glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f); + + cache[character] = glyph; + return glyph; } public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); From b190333c17f3e0209e7f04ad1463b272b957c774 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 4 Jan 2024 09:00:24 -0800 Subject: [PATCH 228/539] Use repeat step for more delay between the two exits --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d8dc512787..a0069f55c7 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -799,11 +799,7 @@ namespace osu.Game.Tests.Visual.Navigation }); }); - AddStep("attempt exit", () => - { - for (int i = 0; i < 2; ++i) - Game.ScreenStack.CurrentScreen.Exit(); - }); + AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2); AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); } From 5b55ca66920b40b394b0403f92b7f371c35cf6b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 02:26:26 +0900 Subject: [PATCH 229/539] Cache legacy skin character glyph lookups to reduce string allocations --- osu.Game/Skinning/LegacySpriteText.cs | 35 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 8aefa50252..81db5fdf36 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -44,10 +46,11 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth); + string fontPrefix = skin.GetFontPrefix(font); + base.Font = new FontUsage(fontPrefix, 1, fixedWidth: FixedWidth); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); + glyphStore = new LegacyGlyphStore(fontPrefix, skin, MaxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); @@ -57,25 +60,41 @@ namespace osu.Game.Skinning private readonly ISkin skin; private readonly Vector2? maxSize; - public LegacyGlyphStore(ISkin skin, Vector2? maxSize) + private readonly string fontName; + + private readonly Dictionary cache = new Dictionary(); + + public LegacyGlyphStore(string fontName, ISkin skin, Vector2? maxSize) { + this.fontName = fontName; this.skin = skin; this.maxSize = maxSize; } public ITexturedCharacterGlyph? Get(string? fontName, char character) { + // We only service one font. + Debug.Assert(fontName == this.fontName); + + if (cache.TryGetValue(character, out var cached)) + return cached; + string lookup = getLookupName(character); var texture = skin.GetTexture($"{fontName}-{lookup}"); - if (texture == null) - return null; + TexturedCharacterGlyph? glyph = null; - if (maxSize != null) - texture = texture.WithMaximumSize(maxSize.Value); + if (texture != null) + { + if (maxSize != null) + texture = texture.WithMaximumSize(maxSize.Value); - return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); + glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); + } + + cache[character] = glyph; + return glyph; } private static string getLookupName(char character) From e9289cfbe78f318d00c9b77065daca6fa824cb09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 01:51:20 +0900 Subject: [PATCH 230/539] Reduce precision of audio balance adjustments during slider sliding --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1daaa24d57..bce28361cb 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -599,7 +599,9 @@ namespace osu.Game.Rulesets.Objects.Drawables float balanceAdjustAmount = positionalHitsoundsLevel.Value * 2; double returnedValue = balanceAdjustAmount * (position - 0.5f); - return returnedValue; + // Rounded to reduce the overhead of audio adjustments (which are currently bindable heavy). + // Balance is very hard to perceive in small increments anyways. + return Math.Round(returnedValue, 2); } /// From 091241634c08aaea3b6d3e34762fb2f1cb459295 Mon Sep 17 00:00:00 2001 From: Zachary Date: Fri, 5 Jan 2024 23:55:17 +1000 Subject: [PATCH 231/539] Make IntroScreen set `AllowTrackControl` to false instead --- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 01086d3e33..0986c0513c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -43,7 +43,7 @@ namespace osu.Game.Overlays /// /// Whether user control of the global track should be allowed. /// - public readonly BindableBool AllowTrackControl = new BindableBool(false); + public readonly BindableBool AllowTrackControl = new BindableBool(true); /// /// Fired when the global has changed. diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index de7732dd5e..a81e24ee88 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -109,6 +109,8 @@ namespace osu.Game.Screens.Menu // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); + musicController.AllowTrackControl.Value = false; + MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); From 8295ad1feb19c30ae944e2f8ea9bd3c5df6d04ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 21:58:44 +0100 Subject: [PATCH 232/539] Change catch scoring to match score V2 --- .../Scoring/CatchScoreProcessor.cs | 60 ++++++++++++++++++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 24 ++++---- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 4b3d378889..161a59c5fd 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -20,20 +21,73 @@ namespace osu.Game.Rulesets.Catch.Scoring private const int combo_cap = 200; private const double combo_base = 4; + private double fruitTinyScale; + public CatchScoreProcessor() : base(new CatchRuleset()) { } + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + // large ticks are *purposefully* not counted to match stable + int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great); + fruitTinyScale = fruitTinyScaleDivisor == 0 + ? 0 + : (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor; + } + protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { - return 600000 * comboProgress - + 400000 * Accuracy.Value * accuracyProgress + const int max_tiny_droplets_portion = 400000; + + double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale); + double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale; + double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0 + ? 0 + : (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit); + + return comboPortion * comboProgress + + dropletsPortion * dropletsHit + bonusPortion; } + public override int GetBaseScoreForResult(HitResult result) + { + switch (result) + { + // dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion + case HitResult.Great: + case HitResult.LargeTickHit: + case HitResult.SmallTickHit: + return 300; + + case HitResult.LargeBonus: + return 200; + } + + return base.GetBaseScoreForResult(result); + } + protected override double GetComboScoreChange(JudgementResult result) - => GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + { + double baseIncrease = 0; + + switch (result.Type) + { + case HitResult.Great: + baseIncrease = 300; + break; + + case HitResult.LargeTickHit: + baseIncrease = 100; + break; + } + + return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + } public override ScoreRank RankFromAccuracy(double accuracy) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 837bb4080e..869ad2c4ae 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -167,14 +167,14 @@ namespace osu.Game.Rulesets.Scoring if (!beatmapApplied) throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}."); - return new Dictionary(maximumResultCounts); + return new Dictionary(MaximumResultCounts); } } private bool beatmapApplied; - private readonly Dictionary scoreResultCounts = new Dictionary(); - private readonly Dictionary maximumResultCounts = new Dictionary(); + protected readonly Dictionary ScoreResultCounts = new Dictionary(); + protected readonly Dictionary MaximumResultCounts = new Dictionary(); private readonly List hitEvents = new List(); private HitObject? lastHitObject; @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1; if (result.Type.IncreasesCombo()) Combo.Value++; @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; + ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1; if (result.Judgement.MaxResult.AffectsAccuracy()) { @@ -394,13 +394,13 @@ namespace osu.Game.Rulesets.Scoring maximumComboPortion = currentComboPortion; maximumAccuracyJudgementCount = currentAccuracyJudgementCount; - maximumResultCounts.Clear(); - maximumResultCounts.AddRange(scoreResultCounts); + MaximumResultCounts.Clear(); + MaximumResultCounts.AddRange(ScoreResultCounts); MaximumTotalScore = TotalScore.Value; } - scoreResultCounts.Clear(); + ScoreResultCounts.Clear(); currentBaseScore = 0; currentMaximumBaseScore = 0; @@ -430,10 +430,10 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics.Clear(); foreach (var result in HitResultExtensions.ALL_TYPES) - score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result); + score.Statistics[result] = ScoreResultCounts.GetValueOrDefault(result); foreach (var result in HitResultExtensions.ALL_TYPES) - score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); + score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. score.TotalScore = TotalScore.Value; @@ -464,8 +464,8 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = frame.Header.MaxCombo; TotalScore.Value = frame.Header.TotalScore; - scoreResultCounts.Clear(); - scoreResultCounts.AddRange(frame.Header.Statistics); + ScoreResultCounts.Clear(); + ScoreResultCounts.AddRange(frame.Header.Statistics); SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics); From ea7078fab52bc1db38f4311c8822e99fa6e82e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jan 2024 15:03:06 +0100 Subject: [PATCH 233/539] Implement approximate score conversion algorithm matching score V2 --- .../StandardisedScoreMigrationTools.cs | 113 +++++++++++++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 +- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 9cfb9ea957..f029d85aed 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -437,9 +437,30 @@ namespace osu.Game.Database break; case 2: + // compare logic in `CatchScoreProcessor`. + + // this could technically be slightly incorrect in the case of stable scores. + // because large droplet misses are counted as full misses in stable scores, + // `score.MaximumStatistics.GetValueOrDefault(Great)` will be equal to the count of fruits *and* large droplets + // rather than just fruits (which was the intent). + // this is not fixable without introducing an extra legacy score attribute dedicated for catch, + // and this is a ballpark conversion process anyway, so attempt to trudge on. + int fruitTinyScaleDivisor = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + score.MaximumStatistics.GetValueOrDefault(HitResult.Great); + double fruitTinyScale = fruitTinyScaleDivisor == 0 + ? 0 + : (double)score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor; + + const int max_tiny_droplets_portion = 400000; + + double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale); + double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale; + double dropletsHit = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) == 0 + ? 0 + : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); + convertedTotalScore = (long)Math.Round(( - 600000 * comboProportion - + 400000 * score.Accuracy + comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) + + dropletsPortion * dropletsHit + bonusProportion) * modMultiplier); break; @@ -461,6 +482,94 @@ namespace osu.Game.Database return convertedTotalScore; } + /// + /// + /// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless. + /// This is because in stable score V1, catch has quadratic score progression, + /// while in stable score V2, score progression is logarithmic up to 200 combo and then linear. + /// + /// + /// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map) + /// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it. + /// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account. + /// + /// + /// The general idea is that aside from the which the player is known to have hit, + /// the remaining misses are evenly distributed across the rest of the objects that give combo. + /// This is therefore a worst-case estimate. + /// + /// + private static double estimateComboProportionForCatch(int beatmapMaxCombo, int scoreMaxCombo, int scoreMissCount) + { + if (beatmapMaxCombo == 0) + return 1; + + if (scoreMaxCombo == 0) + return 0; + + if (beatmapMaxCombo == scoreMaxCombo) + return 1; + + double estimatedBestCaseTotal = estimateBestCaseComboTotal(beatmapMaxCombo); + + int remainingCombo = beatmapMaxCombo - (scoreMaxCombo + scoreMissCount); + double totalDroppedScore = 0; + + int assumedLengthOfRemainingCombos = (int)Math.Floor((double)remainingCombo / scoreMissCount); + + if (assumedLengthOfRemainingCombos > 0) + { + while (remainingCombo > 0) + { + int comboLength = Math.Min(assumedLengthOfRemainingCombos, remainingCombo); + + remainingCombo -= comboLength; + totalDroppedScore += estimateDroppedComboScoreAfterMiss(comboLength); + } + } + else + { + // there are so many misses that attempting to evenly divide remaining combo results in 0 length per combo, + // i.e. all remaining judgements are combo breaks. + // in that case, presume every single remaining object is a miss and did not give any combo score. + totalDroppedScore = estimatedBestCaseTotal - estimateBestCaseComboTotal(scoreMaxCombo); + } + + return estimatedBestCaseTotal == 0 + ? 1 + : 1 - Math.Clamp(totalDroppedScore / estimatedBestCaseTotal, 0, 1); + + double estimateBestCaseComboTotal(int maxCombo) + { + if (maxCombo == 0) + return 1; + + double estimatedTotal = 0.5 * Math.Min(maxCombo, 2); + + if (maxCombo <= 2) + return estimatedTotal; + + // int_2^x log_4(t) dt + estimatedTotal += (Math.Min(maxCombo, 200) * (Math.Log(Math.Min(maxCombo, 200)) - 1) + 2 - Math.Log(4)) / Math.Log(4); + + if (maxCombo <= 200) + return estimatedTotal; + + estimatedTotal += (maxCombo - 200) * Math.Log(200) / Math.Log(4); + return estimatedTotal; + } + + double estimateDroppedComboScoreAfterMiss(int lengthOfComboAfterMiss) + { + if (lengthOfComboAfterMiss >= 200) + lengthOfComboAfterMiss = 200; + + // int_0^x (log_4(200) - log_4(t)) dt + // note that this is an pessimistic estimate, i.e. it may subtract too much if the miss happened before reaching 200 combo + return lengthOfComboAfterMiss * (1 + Math.Log(200) - Math.Log(lengthOfComboAfterMiss)) / Math.Log(4); + } + } + public static double ComputeAccuracy(ScoreInfo scoreInfo) { Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index cf0a7bd54f..495edaf49c 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -36,9 +36,10 @@ namespace osu.Game.Scoring.Legacy /// 30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores. /// 30000008: Add accuracy conversion. Reconvert all scores. /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. + /// 30000010: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000009; + public const int LATEST_VERSION = 30000010; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From e2769dbda14b21e550c3bafa9446af1e406e2966 Mon Sep 17 00:00:00 2001 From: Zachary Date: Sat, 6 Jan 2024 19:29:41 +1000 Subject: [PATCH 234/539] Attempt at creating a test. --- .../TestSceneIntroMusicActionHandling.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs new file mode 100644 index 0000000000..cc2b16a842 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneIntroMusicActionHandling : OsuTestScene + { + private OsuGameTestScene.TestOsuGame? game; + + private GlobalActionContainer globalActionContainer => game.ChildrenOfType().First(); + + [Test] + public void TestPauseDuringIntro() + { + AddStep("Create new game instance", () => + { + if (game?.Parent != null) + Remove(game, true); + + RecycleLocalStorage(false); + + AddGame(game = new OsuGameTestScene.TestOsuGame(LocalStorage, API)); + }); + + AddUntilStep("Wait for load", () => game?.IsLoaded ?? false); + AddUntilStep("Wait for intro", () => game?.ScreenStack.CurrentScreen is IntroScreen); + AddUntilStep("Wait for music", () => game?.MusicController.IsPlaying == true); + + // Check that pause dosesn't work during intro sequence. + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("Still playing before menu", () => game?.MusicController.IsPlaying == true); + AddUntilStep("Wait for main menu", () => game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); + + // Check that toggling after intro still works. + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddUntilStep("Music paused", () => game?.MusicController.IsPlaying == false && game?.MusicController.UserPauseRequested == true); + AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); + AddUntilStep("Music resumed", () => game?.MusicController.IsPlaying == true && game?.MusicController.UserPauseRequested == false); + } + } +} From 14a43375a70104094f7a9d23cf091c18d1223ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 6 Jan 2024 20:25:03 +0900 Subject: [PATCH 235/539] Fix overall ranking text overlapping at some aspect ratios Can't confirm on the actual ranking screen due to stuff not working. Maybe it'll work tomorrow. Closes https://github.com/ppy/osu/issues/26341. --- osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs index 73b9897096..762be61853 100644 --- a/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/SoloStatisticsPanel.cs @@ -37,7 +37,6 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 0.5f, StatisticsUpdate = { BindTarget = StatisticsUpdate } })).ToArray(); } From d3710f0bfd0e8fb457ffbde1fb14dce853ad4f14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 6 Jan 2024 20:44:07 +0900 Subject: [PATCH 236/539] Remove scores from song select leaderboard when leaving the screen --- osu.Game/Online/Leaderboards/Leaderboard.cs | 13 ++++++++++--- osu.Game/Screens/Select/PlaySongSelect.cs | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 93aa0b95a7..67f2590ad8 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -152,6 +152,15 @@ namespace osu.Game.Online.Leaderboards /// public void RefetchScores() => Scheduler.AddOnce(refetchScores); + /// + /// Clear all scores from the display. + /// + public void ClearScores() + { + cancelPendingWork(); + SetScores(null); + } + /// /// Call when a retrieval or display failure happened to show a relevant message to the user. /// @@ -220,9 +229,7 @@ namespace osu.Game.Online.Leaderboards { Debug.Assert(ThreadSafety.IsUpdateThread); - cancelPendingWork(); - - SetScores(null); + ClearScores(); setState(LeaderboardState.Retrieving); currentFetchCancellationSource = new CancellationTokenSource(); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 7b7b8857f3..4951504ff5 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -146,6 +146,14 @@ namespace osu.Game.Screens.Select } } + public override void OnSuspending(ScreenTransitionEvent e) + { + // Scores will be refreshed on arriving at this screen. + // Clear them to avoid animation overload on returning to song select. + playBeatmapDetailArea.Leaderboard.ClearScores(); + base.OnSuspending(e); + } + public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); From b809d4c068f77558e30e80e0a9a8846cd41af5c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 02:14:03 +0900 Subject: [PATCH 237/539] Remove delegate overhead from argon health display's animation updates --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 16 ++++++++++++---- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 8acc43c091..7721f9c0c0 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -68,11 +69,11 @@ namespace osu.Game.Screens.Play.HUD get => glowBarValue; set { - if (glowBarValue == value) + if (Precision.AlmostEquals(glowBarValue, value, 0.0001)) return; glowBarValue = value; - Scheduler.AddOnce(updatePathVertices); + pathVerticesCache.Invalidate(); } } @@ -83,11 +84,11 @@ namespace osu.Game.Screens.Play.HUD get => healthBarValue; set { - if (healthBarValue == value) + if (Precision.AlmostEquals(healthBarValue, value, 0.0001)) return; healthBarValue = value; - Scheduler.AddOnce(updatePathVertices); + pathVerticesCache.Invalidate(); } } @@ -100,6 +101,8 @@ namespace osu.Game.Screens.Play.HUD private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + private readonly Cached pathVerticesCache = new Cached(); + public ArgonHealthDisplay() { AddLayout(drawSizeLayout); @@ -208,6 +211,9 @@ namespace osu.Game.Screens.Play.HUD drawSizeLayout.Validate(); } + if (!pathVerticesCache.IsValid) + updatePathVertices(); + mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); } @@ -346,6 +352,8 @@ namespace osu.Game.Screens.Play.HUD mainBar.Vertices = healthBarVertices.Select(v => v - healthBarVertices[0]).ToList(); mainBar.Position = healthBarVertices[0]; + + pathVerticesCache.Validate(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 7747036826..54aba6b8da 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play.HUD public Bindable Current { get; } = new BindableDouble { MinValue = 0, - MaxValue = 1 + MaxValue = 1, }; private BindableNumber health = null!; From 9d9e6fcfdbc3c21faa7e637375ac4a9e4cdf0c51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 03:22:19 +0900 Subject: [PATCH 238/539] Remove LINQ calls in hot paths --- osu.Game/Skinning/SkinnableSound.cs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f866a4f8ec..f153f4f8d3 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -194,9 +194,33 @@ namespace osu.Game.Skinning /// /// Whether any samples are currently playing. /// - public bool IsPlaying => samplesContainer.Any(s => s.Playing); + public bool IsPlaying + { + get + { + foreach (PoolableSkinnableSample s in samplesContainer) + { + if (s.Playing) + return true; + } - public bool IsPlayed => samplesContainer.Any(s => s.Played); + return false; + } + } + + public bool IsPlayed + { + get + { + foreach (PoolableSkinnableSample s in samplesContainer) + { + if (s.Played) + return true; + } + + return false; + } + } public IBindable AggregateVolume => samplesContainer.AggregateVolume; From 35eff639cb936188f73c34dc71e67ea4d93a061d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 03:25:00 +0900 Subject: [PATCH 239/539] Remove unnecessary second iteration over `NestedHitObjects` --- .../Objects/Drawables/DrawableSlider.cs | 7 ++----- .../Objects/Drawables/DrawableSliderRepeat.cs | 2 +- .../Objects/Drawables/ITrackSnaking.cs | 15 --------------- 3 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b306fd38c1..0f8c9a4d36 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -246,11 +246,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ball.UpdateProgress(completionProgress); SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0); - foreach (DrawableHitObject hitObject in NestedHitObjects) - { - if (hitObject is ITrackSnaking s) - s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0)); - } + foreach (DrawableSliderRepeat repeat in repeatContainer) + repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0)); Size = SliderBody?.Size ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index c6d4f7c4ca..3239565528 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking + public partial class DrawableSliderRepeat : DrawableOsuHitObject { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs deleted file mode 100644 index cae2a7c36d..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - /// - /// A component which tracks the current end snaking position of a slider. - /// - public interface ITrackSnaking - { - void UpdateSnakingPosition(Vector2 start, Vector2 end); - } -} From 5cc4a586acf40fd3d9be3425f875e54406a38c4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jan 2024 03:26:58 +0900 Subject: [PATCH 240/539] Avoid iteration over `NestedHitObjects` in silder's `Update` unless necessary --- .../Objects/Drawables/DrawableSlider.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 0f8c9a4d36..c5194025c1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private ShakeContainer shakeContainer; + private Vector2? childAnchorPosition; + protected override IEnumerable DimmablePieces => new Drawable[] { HeadCircle, @@ -254,10 +256,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (DrawSize != Vector2.Zero) { - var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize); - foreach (var obj in NestedHitObjects) - obj.RelativeAnchorPosition = childAnchorPosition; - Ball.RelativeAnchorPosition = childAnchorPosition; + Vector2 pos = Vector2.Divide(OriginPosition, DrawSize); + + if (pos != childAnchorPosition) + { + childAnchorPosition = pos; + foreach (var obj in NestedHitObjects) + obj.RelativeAnchorPosition = pos; + Ball.RelativeAnchorPosition = pos; + } } } From 16ea7f9b7787462aa1506bac4ecb0c57090c630d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Jan 2024 13:10:50 +0900 Subject: [PATCH 241/539] Avoid completely unnecessary string allocations in `ArgonCounterTextComponent` --- .../Screens/Play/HUD/ArgonCounterTextComponent.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 266dfb3301..9d364acd59 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -35,14 +34,7 @@ namespace osu.Game.Screens.Play.HUD public LocalisableString Text { get => textPart.Text; - set - { - int remainingCount = RequiredDisplayDigits.Value - value.ToString().Count(char.IsDigit); - string remainingText = remainingCount > 0 ? new string('#', remainingCount) : string.Empty; - - wireframesPart.Text = remainingText + value; - textPart.Text = value; - } + set => textPart.Text = value; } public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null) @@ -83,6 +75,8 @@ namespace osu.Game.Screens.Play.HUD } } }; + + RequiredDisplayDigits.BindValueChanged(digits => wireframesPart.Text = new string('#', digits.NewValue)); } private string textLookup(char c) From dc31c66f6229445dd0ebd0d6a6a52c7241da59f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Jan 2024 19:49:42 +0900 Subject: [PATCH 242/539] Return null on font lookup failure instead of asserting Fallback weirdness. --- osu.Game/Skinning/LegacySpriteText.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 81db5fdf36..581e7534e4 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -74,7 +73,8 @@ namespace osu.Game.Skinning public ITexturedCharacterGlyph? Get(string? fontName, char character) { // We only service one font. - Debug.Assert(fontName == this.fontName); + if (fontName != this.fontName) + return null; if (cache.TryGetValue(character, out var cached)) return cached; From 962c8ba4acfc8920633f99c022824d4d58564ef9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Jan 2024 20:55:28 +0900 Subject: [PATCH 243/539] Reset child anchor position cache on hitobject position change --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index c5194025c1..6cc8b8e935 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -121,7 +121,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }); - PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); + PositionBindable.BindValueChanged(_ => + { + Position = HitObject.StackedPosition; + childAnchorPosition = null; + }); StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue)); From 7b663a27bd4d37334e428f1afc3179bc8f9da7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 10:47:22 +0100 Subject: [PATCH 244/539] Fix score conversion incorrectly assuming zero combo score in certain cases --- .../StandardisedScoreMigrationTools.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 9cfb9ea957..24fe147593 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -311,13 +311,22 @@ namespace osu.Game.Database long maximumLegacyBonusScore = attributes.BonusScore; double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy; - // We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio. - // Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0 - // when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot). - // In such cases, just assume 0. - double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0 - ? Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore) - : 0; + + double comboProportion; + + if (maximumLegacyComboScore + maximumLegacyBonusScore > 0) + { + // We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio. + comboProportion = Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore); + } + else + { + // Two possible causes: + // the beatmap has no bonus objects *AND* + // either the active mods have a zero mod multiplier, in which case assume 0, + // or the *beatmap* has a zero `difficultyPeppyStars` (or just no combo-giving objects), in which case assume 1. + comboProportion = legacyModMultiplier == 0 ? 0 : 1; + } // We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore. long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore; From 50eba9ebdb92790c10fda1904219525029abe0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 12:52:14 +0100 Subject: [PATCH 245/539] Reduce code duplication in test --- .../TestSceneIntroMusicActionHandling.cs | 32 +++++++------------ osu.Game/Tests/Visual/OsuGameTestScene.cs | 8 +++-- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs index cc2b16a842..00c14dc797 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs @@ -9,39 +9,31 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus { - public partial class TestSceneIntroMusicActionHandling : OsuTestScene + public partial class TestSceneIntroMusicActionHandling : OsuGameTestScene { - private OsuGameTestScene.TestOsuGame? game; + private GlobalActionContainer globalActionContainer => Game.ChildrenOfType().First(); - private GlobalActionContainer globalActionContainer => game.ChildrenOfType().First(); + public override void SetUpSteps() + { + CreateNewGame(); + // we do not want to progress to main menu immediately, hence the override and lack of `ConfirmAtMainMenu()` call here. + } [Test] public void TestPauseDuringIntro() { - AddStep("Create new game instance", () => - { - if (game?.Parent != null) - Remove(game, true); - - RecycleLocalStorage(false); - - AddGame(game = new OsuGameTestScene.TestOsuGame(LocalStorage, API)); - }); - - AddUntilStep("Wait for load", () => game?.IsLoaded ?? false); - AddUntilStep("Wait for intro", () => game?.ScreenStack.CurrentScreen is IntroScreen); - AddUntilStep("Wait for music", () => game?.MusicController.IsPlaying == true); + AddUntilStep("Wait for music", () => Game?.MusicController.IsPlaying == true); // Check that pause dosesn't work during intro sequence. AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); - AddAssert("Still playing before menu", () => game?.MusicController.IsPlaying == true); - AddUntilStep("Wait for main menu", () => game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); + AddAssert("Still playing before menu", () => Game?.MusicController.IsPlaying == true); + AddUntilStep("Wait for main menu", () => Game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); // Check that toggling after intro still works. AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); - AddUntilStep("Music paused", () => game?.MusicController.IsPlaying == false && game?.MusicController.UserPauseRequested == true); + AddUntilStep("Music paused", () => Game?.MusicController.IsPlaying == false && Game?.MusicController.UserPauseRequested == true); AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); - AddUntilStep("Music resumed", () => game?.MusicController.IsPlaying == true && game?.MusicController.UserPauseRequested == false); + AddUntilStep("Music resumed", () => Game?.MusicController.IsPlaying == true && Game?.MusicController.UserPauseRequested == false); } } } diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 94be4a375d..947305439e 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual [SetUpSteps] public virtual void SetUpSteps() + { + CreateNewGame(); + ConfirmAtMainMenu(); + } + + protected void CreateNewGame() { AddStep("Create new game instance", () => { @@ -71,8 +77,6 @@ namespace osu.Game.Tests.Visual AddUntilStep("Wait for load", () => Game.IsLoaded); AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); - - ConfirmAtMainMenu(); } [TearDownSteps] From b869be2f463b41781bfaefd2926c54925a33ac03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 12:52:23 +0100 Subject: [PATCH 246/539] Fix typo --- .../Visual/Menus/TestSceneIntroMusicActionHandling.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs index 00c14dc797..01aeaff1db 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroMusicActionHandling.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Menus { AddUntilStep("Wait for music", () => Game?.MusicController.IsPlaying == true); - // Check that pause dosesn't work during intro sequence. + // Check that pause doesn't work during intro sequence. AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); AddAssert("Still playing before menu", () => Game?.MusicController.IsPlaying == true); AddUntilStep("Wait for main menu", () => Game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); From b6ce57b777983af3808b9736d86d1aba4b6faf80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 12:54:16 +0100 Subject: [PATCH 247/539] Use override that was intended to steer global track control rather than local sets --- osu.Game/Screens/Menu/IntroScreen.cs | 4 ++-- osu.Game/Screens/Menu/MainMenu.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a81e24ee88..ac7dffc241 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -95,6 +95,8 @@ namespace osu.Game.Screens.Menu Colour = Color4.Black }; + public override bool? AllowGlobalTrackControl => false; + protected IntroScreen([CanBeNull] Func createNextScreen = null) { this.createNextScreen = createNextScreen; @@ -109,8 +111,6 @@ namespace osu.Game.Screens.Menu // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); - musicController.AllowTrackControl.Value = false; - MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ffb68367c2..b264341cc5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.Menu public override bool AllowExternalScreenChange => true; + public override bool? AllowGlobalTrackControl => true; + private Screen songSelect; private MenuSideFlashes sideFlashes; @@ -241,8 +243,6 @@ namespace osu.Game.Screens.Menu { var track = musicController.CurrentTrack; - musicController.AllowTrackControl.Value = true; - // presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now. if (!track.IsRunning) { From 8c82bb006cc5f842f943c513d70ab33d9d7234f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 09:43:53 +0100 Subject: [PATCH 248/539] Fix mania score conversion using score V1 accuracy Partially addresses https://github.com/ppy/osu/discussions/26416 As pointed out in the discussion thread above, the total score conversion process for mania was using accuracy directly from the replay. In mania accuracy is calculated differently in score V1 than in score V2, which meant that scores coming from stable were treated more favourably (due to weighting GREAT and PERFECT equally). To fix, recompute accuracy locally and use that for the accuracy portion. Note that this will still not be (and cannot be made) 100% accurate, as in stable score V2, as well as in lazer, hold notes are *two* judgements, not one as in stable score V1, meaning that full and correct score statistics are not available without playing back the replay. The effects of the change can be previewed on the following spreadsheet: https://docs.google.com/spreadsheets/d/1wxD4UwLjwcr7n9y5Yq7EN0lgiLBN93kpd4gBnAlG-E0/edit#gid=1711190356 Top 5 changed scores with replays: | score | master | this PR | replay | | :------------------------------------------------------------------------------------------------------------------------------- | ------: | ------: | ------: | | [Outlasted on Uwa!! So Holiday by toby fox [[4K] easy] (0.71\*)](https://osu.ppy.sh/scores/mania/460404716) | 935,917 | 927,269 | 920,579 | | [ag0 on Emotional Uplifting Orchestral by bradbreeck [[4K] Rocket's Normal] (0.76\*)](https://osu.ppy.sh/scores/mania/453133066) | 921,636 | 913,535 | 875,549 | | [rlarkgus on Zen Zen Zense by Gom (HoneyWorks) [[5K] Normal] (1.68\*)](https://osu.ppy.sh/scores/mania/458368312) | 934,340 | 926,787 | 918,855 | | [YuJJun on Harumachi Clover by R3 Music Box [4K Catastrophe] (1.80\*)](https://osu.ppy.sh/scores/mania/548215786) | 918,606 | 911,111 | 885,454 | | [Fritte on 45-byou by respon feat. Hatsune Miku & Megpoid [[5K] Normal] (1.52\*)](https://osu.ppy.sh/scores/mania/516079410) | 900,024 | 892,569 | 907,456 | --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 7 ++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 9cfb9ea957..1ef7355027 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -444,9 +444,14 @@ namespace osu.Game.Database break; case 3: + // in the mania case accuracy actually changes between score V1 and score V2 / standardised + // (PERFECT weighting changes from 300 to 305), + // so for better accuracy recompute accuracy locally based on hit statistics and use that instead, + double scoreV2Accuracy = ComputeAccuracy(score); + convertedTotalScore = (long)Math.Round(( 850000 * comboProportion - + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + + 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy) + bonusProportion) * modMultiplier); break; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index cf0a7bd54f..95f2ee0552 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -36,9 +36,10 @@ namespace osu.Game.Scoring.Legacy /// 30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores. /// 30000008: Add accuracy conversion. Reconvert all scores. /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. + /// 30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000009; + public const int LATEST_VERSION = 30000010; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From e77d203a24088174a47810a70112e2a4cda46fc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 01:08:17 +0900 Subject: [PATCH 249/539] Refactor delayed load logic to hopefully read better --- .../Carousel/DrawableCarouselBeatmapSet.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 61658526db..b70278c9bb 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -199,27 +199,34 @@ namespace osu.Game.Screens.Select.Carousel Debug.Assert(Item != null); - if (loadCancellation == null && (timeSinceUnpool += Time.Elapsed) > timeUpdatingBeforeLoad) - { - loadCancellation = new CancellationTokenSource(); + // A load is already in progress if the cancellation token is non-null. + if (loadCancellation != null) + return; - LoadComponentsAsync(new CompositeDrawable[] + timeSinceUnpool += Time.Elapsed; + + // We only trigger a load after this set has been in an updating state for a set amount of time. + if (timeSinceUnpool <= timeUpdatingBeforeLoad) + return; + + loadCancellation = new CancellationTokenSource(); + + LoadComponentsAsync(new CompositeDrawable[] + { + new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) { - new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) - { - RelativeSizeAxes = Axes.Both, - }, - new SetPanelContent((CarouselBeatmapSet)Item) - { - Depth = float.MinValue, - RelativeSizeAxes = Axes.Both, - } - }, drawables => + RelativeSizeAxes = Axes.Both, + }, + new SetPanelContent((CarouselBeatmapSet)Item) { - Header.AddRange(drawables); - drawables.ForEach(d => d.FadeInFromZero(150)); - }, loadCancellation.Token); - } + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + } + }, drawables => + { + Header.AddRange(drawables); + drawables.ForEach(d => d.FadeInFromZero(150)); + }, loadCancellation.Token); } private void updateBeatmapYPositions() From 51bd32bf7e94e74967d6852ead980b71bed6a8e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 01:08:47 +0900 Subject: [PATCH 250/539] Restore comment regarding usage of `MinBy` --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b70278c9bb..369db37e63 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -213,6 +213,7 @@ namespace osu.Game.Screens.Select.Carousel LoadComponentsAsync(new CompositeDrawable[] { + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID))) { RelativeSizeAxes = Axes.Both, From c4ac53002c04567f29b9bd0efe9a7ac16e72a4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 19:49:22 +0100 Subject: [PATCH 251/539] Remove loop in combo score loss estimation calculation --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index f029d85aed..07b4fe7f40 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -519,13 +519,13 @@ namespace osu.Game.Database if (assumedLengthOfRemainingCombos > 0) { - while (remainingCombo > 0) - { - int comboLength = Math.Min(assumedLengthOfRemainingCombos, remainingCombo); + int assumedCombosCount = (int)Math.Floor((double)remainingCombo / assumedLengthOfRemainingCombos); + totalDroppedScore += assumedCombosCount * estimateDroppedComboScoreAfterMiss(assumedLengthOfRemainingCombos); - remainingCombo -= comboLength; - totalDroppedScore += estimateDroppedComboScoreAfterMiss(comboLength); - } + remainingCombo -= assumedCombosCount * assumedLengthOfRemainingCombos; + + if (remainingCombo > 0) + totalDroppedScore += estimateDroppedComboScoreAfterMiss(remainingCombo); } else { From 8a87301c55262f888017cb2ce5ed23d04429ab05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 21:33:25 +0100 Subject: [PATCH 252/539] Add test for crashing scenario --- .../Navigation/TestSceneScreenNavigation.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a0069f55c7..8cb993eff2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -938,6 +938,35 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); } + [Test] + public void TestExitSongSelectAndImmediatelyClickLogo() + { + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("press escape and then click logo immediately", () => + { + InputManager.Key(Key.Escape); + clickLogoWhenNotCurrent(); + }); + + void clickLogoWhenNotCurrent() + { + if (songSelect.IsCurrentScreen()) + Scheduler.AddOnce(clickLogoWhenNotCurrent); + else + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + } + } + } + private Func playToResults() { var player = playToCompletion(); From 58db39ec3206a34e1b8a0cbdbe196059dd4b8306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 21:37:25 +0100 Subject: [PATCH 253/539] Fix crash when clicking osu! logo in song select immediately after exiting Closes https://github.com/ppy/osu/issues/26415. The crash report with incomplete log was backwards, the exit comes first. Sentry events and the reproducing test in 8a87301c55262f888017cb2ce5ed23d04429ab05 confirm this. --- osu.Game/Screens/Select/PlaySongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 4951504ff5..3cf8de5267 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -92,6 +92,9 @@ namespace osu.Game.Screens.Select { if (playerLoader != null) return false; + if (!this.IsCurrentScreen()) + return false; + modsAtGameplayStart = Mods.Value; // Ctrl+Enter should start map with autoplay enabled. From 67df7b33fb6c355ad41382fae8323c5c9e3d0c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 21:51:01 +0100 Subject: [PATCH 254/539] Add failing test coverage for not attempting to upgrade custom ruleset scores --- .../BackgroundDataStoreProcessorTests.cs | 30 +++++++++++++++++++ osu.Game/BackgroundDataStoreProcessor.cs | 4 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 43ce7200d2..9dbfde7bce 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -1,6 +1,7 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; @@ -182,9 +183,38 @@ namespace osu.Game.Tests.Database AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); } + [Test] + public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available) + { + RulesetInfo rulesetInfo = null!; + ScoreInfo scoreInfo = null!; + TestBackgroundDataStoreProcessor processor = null!; + + AddStep("Add unavailable ruleset", () => Realm.Write(r => r.Add(rulesetInfo = new RulesetInfo + { + ShortName = Guid.NewGuid().ToString(), + Available = available + }))); + + AddStep("Add score for unavailable ruleset", () => Realm.Write(r => r.Add(scoreInfo = new ScoreInfo( + ruleset: rulesetInfo, + beatmap: r.All().First()) + { + TotalScoreVersion = 30000001 + }))); + + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); + + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); + } + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; + + public bool Completed => ProcessingTask.IsCompleted; } } } diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs index a748a7422a..969062be57 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -28,6 +28,8 @@ namespace osu.Game /// public partial class BackgroundDataStoreProcessor : Component { + protected Task ProcessingTask = null!; + [Resolved] private RulesetStore rulesetStore { get; set; } = null!; @@ -61,7 +63,7 @@ namespace osu.Game { base.LoadComplete(); - Task.Factory.StartNew(() => + ProcessingTask = Task.Factory.StartNew(() => { Logger.Log("Beginning background data store processing.."); From 388f6599e0264d640d4da1fda0e27441212ca389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 21:54:10 +0100 Subject: [PATCH 255/539] Add failing test for not attempting to upgrade non-legacy scores This was a source of confusion for users previously, wondering why their non-legacy (non-stable) scores weren't being converted in line with new scoring changes, when it was never actually our intention to support anything of the sort. --- .../BackgroundDataStoreProcessorTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 9dbfde7bce..8b066f860f 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -210,6 +210,31 @@ namespace osu.Game.Tests.Database AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); } + [Test] + public void TestNonLegacyScoreNotSubjectToUpgrades() + { + ScoreInfo scoreInfo = null!; + TestBackgroundDataStoreProcessor processor = null!; + + AddStep("Add score which requires upgrade (and has beatmap)", () => + { + Realm.Write(r => + { + r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) + { + TotalScoreVersion = 30000005, + LegacyTotalScore = 123456, + }); + }); + }); + + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); + + AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005)); + } + public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; From aa83b84bb225781adccc15f4276a74901ed03397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 22:34:41 +0100 Subject: [PATCH 256/539] Fix Cinema mod being compatible with mods that can force failure Addresses https://github.com/ppy/osu/pull/26080#issuecomment-1868833214. --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 +- osu.Game/Rulesets/Mods/ModFailCondition.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index dbb37e0af6..7c88a8a588 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModCinema; public override LocalisableString Description => "Watch the video without visual distractions."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }).ToArray(); public void ApplyToHUD(HUDOverlay overlay) { diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 471c3bfe8d..0b229766c1 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail) }; + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); From 4f7dcb3a5022cb533cc467d974a7f382d1cf285e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jan 2024 22:07:33 +0100 Subject: [PATCH 257/539] Do not attempt to recalculate non-legacy scores or scores set on custom rulesets Addresses discussions such as https://github.com/ppy/osu/discussions/26407 or https://github.com/ppy/osu/discussions/25914 wherein: - the game would attempt to convert scores for custom rulesets, which makes no sense, especially so when they're not there, - the game would also "recalculate" lazer scores, but that was never the intention or was never supported; the game would just increment the score version on those but still include them in the converted tally. --- osu.Game/BackgroundDataStoreProcessor.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/BackgroundDataStoreProcessor.cs index 969062be57..fc7db13d41 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/BackgroundDataStoreProcessor.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -28,7 +29,7 @@ namespace osu.Game /// public partial class BackgroundDataStoreProcessor : Component { - protected Task ProcessingTask = null!; + protected Task ProcessingTask { get; private set; } = null!; [Resolved] private RulesetStore rulesetStore { get; set; } = null!; @@ -316,10 +317,17 @@ namespace osu.Game { Logger.Log("Querying for scores that need total score conversion..."); - HashSet scoreIds = realmAccess.Run(r => new HashSet(r.All() - .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null - && s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) - .AsEnumerable().Select(s => s.ID))); + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => !s.BackgroundReprocessingFailed + && s.BeatmapInfo != null + && s.IsLegacyScore + && s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() + // must be done after materialisation, as realm doesn't want to support + // nested property predicates + .Where(s => s.Ruleset.IsLegacyRuleset()) + .Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); From 58619f168458ead922914536a651951595f33875 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jan 2024 14:16:05 -0800 Subject: [PATCH 258/539] Fix wiki main page not displaying custom layout --- osu.Game/Overlays/WikiOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 440e451201..a8d9cdcdb2 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays wikiData.Value = response; path.Value = response.Path; - if (response.Layout == index_path) + if (response.Layout == index_path.ToLowerInvariant()) { LoadDisplay(new WikiMainPage { From d6ba7a9c6eeab7c2c4889249df4f7280f28515d2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jan 2024 14:24:56 -0800 Subject: [PATCH 259/539] Centralise `INDEX_PATH` to `WikiOverlay` --- osu.Game/Overlays/Wiki/WikiHeader.cs | 4 +--- osu.Game/Overlays/WikiOverlay.cs | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 55be05ed7a..d64d6b934a 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -17,8 +17,6 @@ namespace osu.Game.Overlays.Wiki { public partial class WikiHeader : BreadcrumbControlOverlayHeader { - private const string index_path = "Main_page"; - public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex; public readonly Bindable WikiPageData = new Bindable(); @@ -45,7 +43,7 @@ namespace osu.Game.Overlays.Wiki TabControl.AddItem(IndexPageString); - if (e.NewValue.Path == index_path) + if (e.NewValue.Path == WikiOverlay.INDEX_PATH) { Current.Value = IndexPageString; return; diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index a8d9cdcdb2..3777e83cde 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -19,11 +19,11 @@ namespace osu.Game.Overlays { public partial class WikiOverlay : OnlineOverlay { - private const string index_path = "Main_page"; + public const string INDEX_PATH = @"Main_page"; public string CurrentPath => path.Value; - private readonly Bindable path = new Bindable(index_path); + private readonly Bindable path = new Bindable(INDEX_PATH); private readonly Bindable wikiData = new Bindable(); @@ -43,7 +43,7 @@ namespace osu.Game.Overlays { } - public void ShowPage(string pagePath = index_path) + public void ShowPage(string pagePath = INDEX_PATH) { path.Value = pagePath.Trim('/'); Show(); @@ -137,7 +137,7 @@ namespace osu.Game.Overlays wikiData.Value = response; path.Value = response.Path; - if (response.Layout == index_path.ToLowerInvariant()) + if (response.Layout == INDEX_PATH.ToLowerInvariant()) { LoadDisplay(new WikiMainPage { @@ -161,7 +161,7 @@ namespace osu.Game.Overlays path.Value = "error"; LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", - $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page](Main_page).")); + $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } private void showParentPage() From b03813d3b40e0a279bb8ef6800cd587162adf0f2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jan 2024 14:36:08 -0800 Subject: [PATCH 260/539] Update casing of hardcoded "Main_page" string in tests --- osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs | 4 ++-- osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs | 2 +- .../Visual/Online/TestSceneWikiMarkdownContainer.cs | 4 ++-- osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs index 4e71c5977e..40eda3f3dc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs @@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly Bindable wikiPageData = new Bindable(new APIWikiPage { - Title = "Main Page", - Path = "Main_Page", + Title = "Main page", + Path = "Main_page", }); private TestHeader header; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs index 9967be73e8..7b4eadd46d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMainPage.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online }; } - // From https://osu.ppy.sh/api/v2/wiki/en/Main_Page + // From https://osu.ppy.sh/api/v2/wiki/en/Main_page private const string main_page_markdown = "---\nlayout: main_page\n---\n\n\n\n
\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n
\n\n
\n
\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n
\n
\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n
\n
\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n
\n
\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n
\n
\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n
\n
\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n
\n
\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n
\n
\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n
\n
\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n
\n
\n"; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 6c87553971..8909305602 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/"); - AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page"); + AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs index 79c7e3a22e..352f100f3b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online }; }); - // From https://osu.ppy.sh/api/v2/wiki/en/Main_Page + // From https://osu.ppy.sh/api/v2/wiki/en/Main_page private APIWikiPage responseMainPage => new APIWikiPage { - Title = "Main Page", + Title = "Main page", Layout = "main_page", - Path = "Main_Page", + Path = "Main_page", Locale = "en", Subtitle = null, Markdown = From 7d57a668aba2032be4c000561be60f17674c4732 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 13:12:54 +0900 Subject: [PATCH 261/539] Use main page constant in more places --- osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs index 40eda3f3dc..d259322d4a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online private readonly Bindable wikiPageData = new Bindable(new APIWikiPage { Title = "Main page", - Path = "Main_page", + Path = WikiOverlay.INDEX_PATH, }); private TestHeader header; diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs index 352f100f3b..8765d8485a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs @@ -11,6 +11,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.Wiki; namespace osu.Game.Tests.Visual.Online { @@ -111,8 +112,8 @@ namespace osu.Game.Tests.Visual.Online private APIWikiPage responseMainPage => new APIWikiPage { Title = "Main page", - Layout = "main_page", - Path = "Main_page", + Layout = WikiOverlay.INDEX_PATH.ToLowerInvariant(), // custom classes are always lower snake. + Path = WikiOverlay.INDEX_PATH, Locale = "en", Subtitle = null, Markdown = From 172fe53099bf4e2e26e7acc9dc0dc7708b0ea39d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 13:13:32 +0900 Subject: [PATCH 262/539] Use better method of ignore case comparison --- osu.Game/Overlays/WikiOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 3777e83cde..ffbc168fb7 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays wikiData.Value = response; path.Value = response.Path; - if (response.Layout == INDEX_PATH.ToLowerInvariant()) + if (response.Layout.Equals(INDEX_PATH, StringComparison.OrdinalIgnoreCase)) { LoadDisplay(new WikiMainPage { From bb2b7d3c313ab8c2e78ca9a790de290f874c0b6d Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 12:23:01 +0800 Subject: [PATCH 263/539] Add playback controls. --- .../Play/PlayerSettings/PlaybackSettings.cs | 117 ++++++++++++++++-- 1 file changed, 105 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 4753effdb0..69bfe666ee 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,11 +1,17 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Screens.Play.PlayerSettings { @@ -24,34 +30,115 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly OsuSpriteText multiplierText; + private readonly IconButton play; + + [Resolved] + private GameplayClockContainer gameplayClock { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + public PlaybackSettings() : base("playback") { + const double seek_amount = 5000; + const double seek_fast_amount = 10000; + Children = new Drawable[] { - new Container + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = padding }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, padding), Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = "Playback speed", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.FastBackward, + Action = () => seek(-1, seek_fast_amount), + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Backward, + Action = () => seek(-1, seek_amount), + }, + play = new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.4f), + IconScale = new Vector2(1.4f), + Icon = FontAwesome.Regular.PlayCircle, + Action = () => + { + if (gameplayClock != null) + { + if (gameplayClock.IsRunning) + gameplayClock.Stop(); + else + gameplayClock.Start(); + } + } + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Forward, + Action = () => seek(1, seek_amount), + }, + new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.FastForward, + Action = () => seek(1, seek_fast_amount), + }, + }, }, - multiplierText = new OsuSpriteText + new Container { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + rateSlider = new PlayerSliderBar + { + LabelText = "Playback speed", + Current = UserPlaybackRate, + }, + multiplierText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Margin = new MarginPadding { Right = 20 }, + } + }, + }, }, }, - rateSlider = new PlayerSliderBar { Current = UserPlaybackRate } }; + + void seek(int direction, double amount) + { + double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState.Beatmap.GetLastObjectTime()); + gameplayClock?.Seek(target); + } } protected override void LoadComplete() @@ -59,5 +146,11 @@ namespace osu.Game.Screens.Play.PlayerSettings base.LoadComplete(); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); } + + protected override void Update() + { + base.Update(); + play.Icon = gameplayClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + } } } From 2e041823a15b2720c90c0f2fbcdf8afcee8417df Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 12:24:09 +0800 Subject: [PATCH 264/539] Perform null check on gameplay state. --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 69bfe666ee..3662b7ddc8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play.PlayerSettings void seek(int direction, double amount) { - double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState.Beatmap.GetLastObjectTime()); + double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0); gameplayClock?.Seek(target); } } From afa808695bf9c6060f2f706c15172861e1c56269 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 12:48:11 +0800 Subject: [PATCH 265/539] Make resolved properties nullable. --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 3662b7ddc8..2a8701d4d5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -33,10 +33,10 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly IconButton play; [Resolved] - private GameplayClockContainer gameplayClock { get; set; } = null!; + private GameplayClockContainer? gameplayClock { get; set; } [Resolved] - private GameplayState gameplayState { get; set; } = null!; + private GameplayState? gameplayState { get; set; } public PlaybackSettings() : base("playback") @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override void Update() { base.Update(); - play.Icon = gameplayClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + play.Icon = gameplayClock?.IsRunning == true ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; } } } From 3f5899dae041e66d8292acc757e25556a2172926 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 14:05:59 +0900 Subject: [PATCH 266/539] Fix incorrect implementation of wireframe digits --- .../Play/HUD/ArgonCounterTextComponent.cs | 1 - .../Screens/Play/HUD/ArgonScoreCounter.cs | 36 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 9d364acd59..f88874f872 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 005f7e36a7..f7ca218767 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -1,6 +1,7 @@ // 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 osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -15,6 +16,8 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable { + private ArgonScoreTextComponent scoreText = null!; + protected override double RollingDuration => 500; protected override Easing RollingEasing => Easing.OutQuint; @@ -33,13 +36,42 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); - protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) + protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) { - RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits }, WireframeOpacity = { BindTarget = WireframeOpacity }, ShowLabel = { BindTarget = ShowLabel }, }; + public ArgonScoreCounter() + { + RequiredDisplayDigits.BindValueChanged(_ => updateWireframe()); + } + + public override long DisplayedCount + { + get => base.DisplayedCount; + set + { + base.DisplayedCount = value; + updateWireframe(); + } + } + + private void updateWireframe() + { + scoreText.RequiredDisplayDigits.Value = + Math.Max(RequiredDisplayDigits.Value, getDigitsRequiredForDisplayCount()); + } + + private int getDigitsRequiredForDisplayCount() + { + int digitsRequired = 1; + long c = DisplayedCount; + while ((c /= 10) > 0) + digitsRequired++; + return digitsRequired; + } + private partial class ArgonScoreTextComponent : ArgonCounterTextComponent { public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null) From 765d41faa9940e8ae5878babe326cc76f39ba930 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 14:06:59 +0900 Subject: [PATCH 267/539] Change second occurrence of debug.assert with early return for fallback safety --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index f88874f872..a11f2f01cd 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -159,7 +159,8 @@ namespace osu.Game.Screens.Play.HUD public ITexturedCharacterGlyph? Get(string? fontName, char character) { // We only service one font. - Debug.Assert(fontName == this.fontName); + if (fontName != this.fontName) + return null; if (cache.TryGetValue(character, out var cached)) return cached; From 5970a68e2d12f4f8fc3a118cf68da7a69c1f1fcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 14:17:33 +0900 Subject: [PATCH 268/539] Use invalidation based logic for child anchor position updpates in `DrawableSlider` --- .../Objects/Drawables/DrawableSlider.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 6cc8b8e935..4099d47d61 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Audio; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; @@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private ShakeContainer shakeContainer; - private Vector2? childAnchorPosition; - protected override IEnumerable DimmablePieces => new Drawable[] { HeadCircle, @@ -68,6 +67,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container repeatContainer; private PausableSkinnableSound slidingSample; + private readonly LayoutValue drawSizeLayout; + public DrawableSlider() : this(null) { @@ -84,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AlwaysPresent = true, Alpha = 0 }; + AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); } [BackgroundDependencyLoader] @@ -121,11 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }); - PositionBindable.BindValueChanged(_ => - { - Position = HitObject.StackedPosition; - childAnchorPosition = null; - }); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue)); @@ -258,17 +256,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Size = SliderBody?.Size ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero; - if (DrawSize != Vector2.Zero) + if (!drawSizeLayout.IsValid) { Vector2 pos = Vector2.Divide(OriginPosition, DrawSize); + foreach (var obj in NestedHitObjects) + obj.RelativeAnchorPosition = pos; + Ball.RelativeAnchorPosition = pos; - if (pos != childAnchorPosition) - { - childAnchorPosition = pos; - foreach (var obj in NestedHitObjects) - obj.RelativeAnchorPosition = pos; - Ball.RelativeAnchorPosition = pos; - } + drawSizeLayout.Validate(); } } From 1f6e1cbe56d5f0dce5c3fe60f779bdd66c5fe655 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 14:45:21 +0900 Subject: [PATCH 269/539] Allow interacting with playlist item buttons when not selected --- .../Screens/OnlinePlay/DrawableRoomPlaylist.cs | 6 +++++- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 8abdec9ade..5a1648c91f 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -165,7 +165,11 @@ namespace osu.Game.Screens.OnlinePlay { d.SelectedItem.BindTarget = SelectedItem; d.RequestDeletion = i => RequestDeletion?.Invoke(i); - d.RequestResults = i => RequestResults?.Invoke(i); + d.RequestResults = i => + { + SelectedItem.Value = i; + RequestResults?.Invoke(i); + }; d.RequestEdit = i => RequestEdit?.Invoke(i); d.AllowReordering = AllowReordering; d.AllowDeletion = AllowDeletion; diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 8f405399a7..823f6ea308 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -118,8 +118,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; - public DrawableRoomPlaylistItem(PlaylistItem item) : base(item) { @@ -367,7 +365,7 @@ namespace osu.Game.Screens.OnlinePlay AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 8, Right = 8 }, }, - mainFillFlow = new FillFlowContainer + mainFillFlow = new MainFlow(() => SelectedItem.Value == Model) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -670,5 +668,17 @@ namespace osu.Game.Screens.OnlinePlay public LocalisableString TooltipText => avatar.TooltipText; } } + + public partial class MainFlow : FillFlowContainer + { + private readonly Func isSelected; + + public override bool PropagatePositionalInputSubTree => isSelected(); + + public MainFlow(Func isSelected) + { + this.isSelected = isSelected; + } + } } } From 79ff767eba341cba9e4c81e3410eacf75eb4cd07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 14:51:30 +0900 Subject: [PATCH 270/539] Remove unused using --- osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs index 8765d8485a..e70d35f74a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs @@ -11,7 +11,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; -using osu.Game.Overlays.Wiki; namespace osu.Game.Tests.Visual.Online { From 19d1fff5362b3652eb70c366f7ea361f89af4737 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 15:37:29 +0900 Subject: [PATCH 271/539] Use native query to avoid huge overheads when cleaning up realm files --- osu.Game/Database/RealmFileStore.cs | 9 +++------ osu.Game/Models/RealmFile.cs | 4 ++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs index 1da64d5be8..f1ed3f4b63 100644 --- a/osu.Game/Database/RealmFileStore.cs +++ b/osu.Game/Database/RealmFileStore.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.IO; using System.Linq; using osu.Framework.Extensions; @@ -98,15 +99,11 @@ namespace osu.Game.Database // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. realm.Write(r => { - // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) - var files = r.All().ToList(); - - foreach (var file in files) + foreach (var file in r.All().Filter("Usages.@count = 0")) { totalFiles++; - if (file.BacklinksCount > 0) - continue; + Debug.Assert(file.BacklinksCount == 0); try { diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs index 2faa3f0ca6..4d1642fb5f 100644 --- a/osu.Game/Models/RealmFile.cs +++ b/osu.Game/Models/RealmFile.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Game.IO; using Realms; @@ -11,5 +12,8 @@ namespace osu.Game.Models { [PrimaryKey] public string Hash { get; set; } = string.Empty; + + [Backlink(nameof(RealmNamedFileUsage.File))] + public IQueryable Usages { get; } = null!; } } From 99c76854956f1d6f3116a462fa6af4f0320500c6 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 15:18:21 +0800 Subject: [PATCH 272/539] use `GameplayClock.IsPaused` bindable instead of polling in `Update` --- .../Screens/Play/PlayerSettings/PlaybackSettings.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 2a8701d4d5..a0fafb821c 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly IconButton play; + private readonly BindableBool isPaused = new BindableBool(); + [Resolved] private GameplayClockContainer? gameplayClock { get; set; } @@ -134,6 +136,8 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; + isPaused.BindValueChanged(e => play.Icon = e.NewValue ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle, true); + void seek(int direction, double amount) { double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0); @@ -145,12 +149,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.LoadComplete(); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); - } - - protected override void Update() - { - base.Update(); - play.Icon = gameplayClock?.IsRunning == true ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + gameplayClock?.IsPaused.BindTo(isPaused); } } } From bdecac6d797661836279482a031ee97d3aae955a Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 15:19:54 +0800 Subject: [PATCH 273/539] Inverse check. --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index a0fafb821c..7974b07438 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; - isPaused.BindValueChanged(e => play.Icon = e.NewValue ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle, true); + isPaused.BindValueChanged(e => play.Icon = !e.NewValue ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle, true); void seek(int direction, double amount) { From 1837b31f9b5e1d3a2b829fe97105a0abcc882e8b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 16:38:02 +0900 Subject: [PATCH 274/539] Remove usage of `HealthDisplay.BindValueChanged` Health updates very often when using HP drain. Let's avoid bindable overheads. --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 29 ++++++++--- osu.Game/Skinning/LegacyHealthDisplay.cs | 56 ++++++++++------------ 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 7747036826..a20121b20b 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -56,13 +56,6 @@ namespace osu.Game.Screens.Play.HUD // Don't bind directly so we can animate the startup procedure. health = HealthProcessor.Health.GetBoundCopy(); - health.BindValueChanged(h => - { - if (initialIncrease != null) - FinishInitialAnimation(h.OldValue); - - Current.Value = h.NewValue; - }); if (hudOverlay != null) showHealthBar.BindTo(hudOverlay.ShowHealthBar); @@ -76,6 +69,28 @@ namespace osu.Game.Screens.Play.HUD Current.Value = health.Value; } + protected override void Update() + { + base.Update(); + + // Health changes every frame in draining situations. + // Manually handle value changes to avoid bindable event flow overhead. + if (health.Value != Current.Value) + { + if (initialIncrease != null) + FinishInitialAnimation(Current.Value); + + Current.Value = health.Value; + + if (health.Value > Current.Value) + HealthIncreased(); + } + } + + protected virtual void HealthIncreased() + { + } + private void startInitialAnimation() { if (Current.Value >= health.Value) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 845fc77394..00e19c4f76 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -79,7 +79,13 @@ namespace osu.Game.Skinning marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0); } - protected override void Flash() => marker.Flash(); + protected override void HealthIncreased() + { + marker.Bulge(); + base.HealthIncreased(); + } + + protected override void Flash() => marker.Flash(Current.Value >= epic_cutoff); private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}"); @@ -113,19 +119,16 @@ namespace osu.Game.Skinning Origin = Anchor.Centre, }; - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); + base.Update(); - Current.BindValueChanged(hp => - { - if (hp.NewValue < 0.2f) - Main.Texture = superDangerTexture; - else if (hp.NewValue < epic_cutoff) - Main.Texture = dangerTexture; - else - Main.Texture = normalTexture; - }); + if (Current.Value < 0.2f) + Main.Texture = superDangerTexture; + else if (Current.Value < epic_cutoff) + Main.Texture = dangerTexture; + else + Main.Texture = normalTexture; } } @@ -226,37 +229,30 @@ namespace osu.Game.Skinning public abstract Sprite CreateSprite(); - protected override void LoadComplete() + public override void Flash(bool isEpic) { - base.LoadComplete(); - - Current.BindValueChanged(val => - { - if (val.NewValue > val.OldValue) - bulgeMain(); - }); - } - - public override void Flash() - { - bulgeMain(); - - bool isEpic = Current.Value >= epic_cutoff; - + Bulge(); explode.Blending = isEpic ? BlendingParameters.Additive : BlendingParameters.Inherit; explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120); explode.FadeOutFromOne(120); } - private void bulgeMain() => + public override void Bulge() + { + base.Bulge(); Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } } public partial class LegacyHealthPiece : CompositeDrawable { public Bindable Current { get; } = new Bindable(); - public virtual void Flash() + public virtual void Bulge() + { + } + + public virtual void Flash(bool isEpic) { } } From d83b8dbdaf489320b1ef2632e7b9fba5a3d7c26a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 16:50:27 +0900 Subject: [PATCH 275/539] Refactor `ArgonHealthDisplay` to user interpolation and less bindable events --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 8acc43c091..f15789601c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -158,7 +158,6 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); HealthProcessor.NewJudgement += onNewJudgement; - Current.BindValueChanged(onCurrentChanged, true); // we're about to set `RelativeSizeAxes` depending on the value of `UseRelativeSize`. // setting `RelativeSizeAxes` internally transforms absolute sizing to relative and back to keep the size the same, @@ -173,31 +172,6 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult result) => pendingMissAnimation |= !result.IsHit; - private void onCurrentChanged(ValueChangedEvent valueChangedEvent) - // schedule display updates one frame later to ensure we know the judgement result causing this change (if there is one). - => Scheduler.AddOnce(updateDisplay); - - private void updateDisplay() - { - double newHealth = Current.Value; - - if (newHealth >= GlowBarValue) - finishMissDisplay(); - - double time = newHealth > GlowBarValue ? 500 : 250; - - // TODO: this should probably use interpolation in update. - this.TransformTo(nameof(HealthBarValue), newHealth, time, Easing.OutQuint); - - if (pendingMissAnimation && newHealth < GlowBarValue) - triggerMissDisplay(); - - pendingMissAnimation = false; - - if (!displayingMiss) - this.TransformTo(nameof(GlowBarValue), newHealth, time, Easing.OutQuint); - } - protected override void Update() { base.Update(); @@ -210,6 +184,19 @@ namespace osu.Game.Screens.Play.HUD mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); + + double newHealth = Current.Value; + + if (newHealth >= GlowBarValue) + finishMissDisplay(); + + if (pendingMissAnimation && newHealth < GlowBarValue) + triggerMissDisplay(); + pendingMissAnimation = false; + + HealthBarValue = Interpolation.DampContinuously(HealthBarValue, newHealth, 50, Time.Elapsed); + if (!displayingMiss) + GlowBarValue = Interpolation.DampContinuously(GlowBarValue, newHealth, 50, Time.Elapsed); } protected override void FinishInitialAnimation(double value) From 63961ea276a282f681e1b57866ad36dbb3d472e1 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 16:08:29 +0800 Subject: [PATCH 276/539] use `RepeatingButtonBehavior` for seek buttons --- .../Play/PlayerSettings/PlaybackSettings.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 7974b07438..6e1ce39d9b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Timing; using osuTK; namespace osu.Game.Screens.Play.PlayerSettings @@ -64,14 +65,14 @@ namespace osu.Game.Screens.Play.PlayerSettings Spacing = new Vector2(5, 0), Children = new Drawable[] { - new IconButton + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, Action = () => seek(-1, seek_fast_amount), }, - new IconButton + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -96,14 +97,14 @@ namespace osu.Game.Screens.Play.PlayerSettings } } }, - new IconButton + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, Action = () => seek(1, seek_amount), }, - new IconButton + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -151,5 +152,13 @@ namespace osu.Game.Screens.Play.PlayerSettings rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); gameplayClock?.IsPaused.BindTo(isPaused); } + + private partial class SeekButton : IconButton + { + public SeekButton() + { + AddInternal(new RepeatingButtonBehaviour(this)); + } + } } } From f1b4c305b02df1d0495f30f2390bd168cdbf8249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 17:09:19 +0900 Subject: [PATCH 277/539] Change skinnable health test scene to drain --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 15a7b48323..f215a5d528 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -35,6 +35,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + protected override void Update() + { + base.Update(); + + healthProcessor.Health.Value -= 0.0001f * Time.Elapsed; + } + [Test] public void TestHealthDisplayIncrementing() { From b3533d270c0c9ad70cb89919a3c55d3899be2fd3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 17:20:49 +0900 Subject: [PATCH 278/539] Remove delegate overhead of `HealthBarValue`/`GlowBarValue` --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index f15789601c..43de99b4b5 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -63,34 +63,8 @@ namespace osu.Game.Screens.Play.HUD private double glowBarValue; - public double GlowBarValue - { - get => glowBarValue; - set - { - if (glowBarValue == value) - return; - - glowBarValue = value; - Scheduler.AddOnce(updatePathVertices); - } - } - private double healthBarValue; - public double HealthBarValue - { - get => healthBarValue; - set - { - if (healthBarValue == value) - return; - - healthBarValue = value; - Scheduler.AddOnce(updatePathVertices); - } - } - public const float MAIN_PATH_RADIUS = 10f; private const float curve_start_offset = 70; @@ -183,27 +157,29 @@ namespace osu.Game.Screens.Play.HUD } mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); - glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); + glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, glowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); double newHealth = Current.Value; - if (newHealth >= GlowBarValue) + if (newHealth >= glowBarValue) finishMissDisplay(); - if (pendingMissAnimation && newHealth < GlowBarValue) + if (pendingMissAnimation && newHealth < glowBarValue) triggerMissDisplay(); pendingMissAnimation = false; - HealthBarValue = Interpolation.DampContinuously(HealthBarValue, newHealth, 50, Time.Elapsed); + healthBarValue = Interpolation.DampContinuously(healthBarValue, newHealth, 50, Time.Elapsed); if (!displayingMiss) - GlowBarValue = Interpolation.DampContinuously(GlowBarValue, newHealth, 50, Time.Elapsed); + glowBarValue = Interpolation.DampContinuously(glowBarValue, newHealth, 50, Time.Elapsed); + + updatePathVertices(); } protected override void FinishInitialAnimation(double value) { base.FinishInitialAnimation(value); - this.TransformTo(nameof(HealthBarValue), value, 500, Easing.OutQuint); - this.TransformTo(nameof(GlowBarValue), value, 250, Easing.OutQuint); + this.TransformTo(nameof(healthBarValue), value, 500, Easing.OutQuint); + this.TransformTo(nameof(glowBarValue), value, 250, Easing.OutQuint); } protected override void Flash() @@ -232,7 +208,7 @@ namespace osu.Game.Screens.Play.HUD this.Delay(500).Schedule(() => { - this.TransformTo(nameof(GlowBarValue), Current.Value, 300, Easing.OutQuint); + this.TransformTo(nameof(glowBarValue), Current.Value, 300, Easing.OutQuint); finishMissDisplay(); }, out resetMissBarDelegate); From c081ca21451baa684c343c8a019b90432a4c1336 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 16:30:48 +0800 Subject: [PATCH 279/539] Make field to a local. --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 6e1ce39d9b..e208be6095 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly OsuSpriteText multiplierText; - private readonly IconButton play; - private readonly BindableBool isPaused = new BindableBool(); [Resolved] @@ -47,6 +45,8 @@ namespace osu.Game.Screens.Play.PlayerSettings const double seek_amount = 5000; const double seek_fast_amount = 10000; + IconButton play; + Children = new Drawable[] { new FillFlowContainer From 12a59eb34cb44322315ec3608de8c112b2b217ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 17:30:13 +0900 Subject: [PATCH 280/539] Remove vertex update overheads --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 50 ++++++++++--------- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 9 ++-- osu.Game/Skinning/LegacyHealthDisplay.cs | 7 +-- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 43de99b4b5..e22f45b40d 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -58,8 +57,7 @@ namespace osu.Game.Screens.Play.HUD private bool displayingMiss => resetMissBarDelegate != null; - private readonly List missBarVertices = new List(); - private readonly List healthBarVertices = new List(); + private readonly List vertices = new List(); private double glowBarValue; @@ -156,23 +154,27 @@ namespace osu.Game.Screens.Play.HUD drawSizeLayout.Validate(); } + healthBarValue = Interpolation.DampContinuously(healthBarValue, Current.Value, 50, Time.Elapsed); + if (!displayingMiss) + glowBarValue = Interpolation.DampContinuously(glowBarValue, Current.Value, 50, Time.Elapsed); + mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, glowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); - double newHealth = Current.Value; + updatePathVertices(); + } - if (newHealth >= glowBarValue) + protected override void HealthChanged(bool increase) + { + if (Current.Value >= glowBarValue) finishMissDisplay(); - if (pendingMissAnimation && newHealth < glowBarValue) + if (pendingMissAnimation && Current.Value < glowBarValue) triggerMissDisplay(); + pendingMissAnimation = false; - healthBarValue = Interpolation.DampContinuously(healthBarValue, newHealth, 50, Time.Elapsed); - if (!displayingMiss) - glowBarValue = Interpolation.DampContinuously(glowBarValue, newHealth, 50, Time.Elapsed); - - updatePathVertices(); + base.HealthChanged(increase); } protected override void FinishInitialAnimation(double value) @@ -265,7 +267,6 @@ namespace osu.Game.Screens.Play.HUD if (DrawWidth - padding < rescale_cutoff) rescalePathProportionally(); - List vertices = new List(); barPath.GetPathToProgress(vertices, 0.0, 1.0); background.Vertices = vertices; @@ -295,20 +296,23 @@ namespace osu.Game.Screens.Play.HUD private void updatePathVertices() { - barPath.GetPathToProgress(healthBarVertices, 0.0, healthBarValue); - barPath.GetPathToProgress(missBarVertices, healthBarValue, Math.Max(glowBarValue, healthBarValue)); + barPath.GetPathToProgress(vertices, 0.0, healthBarValue); + if (vertices.Count == 0) vertices.Add(Vector2.Zero); + Vector2 initialVertex = vertices[0]; + for (int i = 0; i < vertices.Count; i++) + vertices[i] -= initialVertex; - if (healthBarVertices.Count == 0) - healthBarVertices.Add(Vector2.Zero); + mainBar.Vertices = vertices; + mainBar.Position = initialVertex; - if (missBarVertices.Count == 0) - missBarVertices.Add(Vector2.Zero); + barPath.GetPathToProgress(vertices, healthBarValue, Math.Max(glowBarValue, healthBarValue)); + if (vertices.Count == 0) vertices.Add(Vector2.Zero); + initialVertex = vertices[0]; + for (int i = 0; i < vertices.Count; i++) + vertices[i] -= initialVertex; - glowBar.Vertices = missBarVertices.Select(v => v - missBarVertices[0]).ToList(); - glowBar.Position = missBarVertices[0]; - - mainBar.Vertices = healthBarVertices.Select(v => v - healthBarVertices[0]).ToList(); - mainBar.Position = healthBarVertices[0]; + glowBar.Vertices = vertices; + glowBar.Position = initialVertex; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index a20121b20b..d5b238cd50 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -75,19 +76,17 @@ namespace osu.Game.Screens.Play.HUD // Health changes every frame in draining situations. // Manually handle value changes to avoid bindable event flow overhead. - if (health.Value != Current.Value) + if (!Precision.AlmostEquals(health.Value, Current.Value, 0.001f)) { if (initialIncrease != null) FinishInitialAnimation(Current.Value); + HealthChanged(Current.Value > health.Value); Current.Value = health.Value; - - if (health.Value > Current.Value) - HealthIncreased(); } } - protected virtual void HealthIncreased() + protected virtual void HealthChanged(bool increase) { } diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 00e19c4f76..9c06cbbfb5 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -79,10 +79,11 @@ namespace osu.Game.Skinning marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0); } - protected override void HealthIncreased() + protected override void HealthChanged(bool increase) { - marker.Bulge(); - base.HealthIncreased(); + if (increase) + marker.Bulge(); + base.HealthChanged(increase); } protected override void Flash() => marker.Flash(Current.Value >= epic_cutoff); From b6505ba0634887d1d48db6aaa880a55622b3f715 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 18:16:53 +0900 Subject: [PATCH 281/539] Reduce colour tween overhead and mark other calls of concern --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index e22f45b40d..f1653c8742 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -188,15 +188,9 @@ namespace osu.Game.Screens.Play.HUD { base.Flash(); - mainBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour.Opacity(0.8f)) - .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); - if (!displayingMiss) { - glowBar.TransformTo(nameof(BarPath.BarColour), Colour4.White, 30, Easing.OutQuint) - .Then() - .TransformTo(nameof(BarPath.BarColour), main_bar_colour, 1000, Easing.OutQuint); - + // TODO: REMOVE THIS. It's recreating textures. glowBar.TransformTo(nameof(BarPath.GlowColour), Colour4.White, 30, Easing.OutQuint) .Then() .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); @@ -214,9 +208,11 @@ namespace osu.Game.Screens.Play.HUD finishMissDisplay(); }, out resetMissBarDelegate); + // TODO: REMOVE THIS. It's recreating textures. glowBar.TransformTo(nameof(BarPath.BarColour), new Colour4(255, 147, 147, 255), 100, Easing.OutQuint).Then() .TransformTo(nameof(BarPath.BarColour), new Colour4(255, 93, 93, 255), 800, Easing.OutQuint); + // TODO: REMOVE THIS. It's recreating textures. glowBar.TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255).Lighten(0.2f)) .TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255), 800, Easing.OutQuint); } @@ -228,6 +224,7 @@ namespace osu.Game.Screens.Play.HUD if (Current.Value > 0) { + // TODO: REMOVE THIS. It's recreating textures. glowBar.TransformTo(nameof(BarPath.BarColour), main_bar_colour, 300, Easing.In); glowBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.In); } From 80892f316754c4004112f9be1dbe4dc4a8723191 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 18:18:11 +0900 Subject: [PATCH 282/539] Fix misses not displaying properly --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index f1653c8742..78720ec087 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -169,10 +169,11 @@ namespace osu.Game.Screens.Play.HUD if (Current.Value >= glowBarValue) finishMissDisplay(); - if (pendingMissAnimation && Current.Value < glowBarValue) + if (pendingMissAnimation) + { triggerMissDisplay(); - - pendingMissAnimation = false; + pendingMissAnimation = false; + } base.HealthChanged(increase); } From 9c7e555237b29e241e22b0c40e14e94afdbec4bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 18:27:37 +0900 Subject: [PATCH 283/539] Fix initial animation not playing correctly --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index d5b238cd50..cd4d050b52 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Play.HUD private BindableNumber health = null!; + protected bool InitialAnimationPlaying => initialIncrease != null; + private ScheduledDelegate? initialIncrease; /// @@ -64,25 +66,35 @@ namespace osu.Game.Screens.Play.HUD // this probably shouldn't be operating on `this.` showHealthBar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + initialHealthValue = health.Value; + if (PlayInitialIncreaseAnimation) startInitialAnimation(); else Current.Value = health.Value; } + private double lastValue; + private double initialHealthValue; + protected override void Update() { base.Update(); - // Health changes every frame in draining situations. - // Manually handle value changes to avoid bindable event flow overhead. - if (!Precision.AlmostEquals(health.Value, Current.Value, 0.001f)) + if (!InitialAnimationPlaying || health.Value != initialHealthValue) { + Current.Value = health.Value; + if (initialIncrease != null) FinishInitialAnimation(Current.Value); + } - HealthChanged(Current.Value > health.Value); - Current.Value = health.Value; + // Health changes every frame in draining situations. + // Manually handle value changes to avoid bindable event flow overhead. + if (!Precision.AlmostEquals(lastValue, Current.Value, 0.001f)) + { + HealthChanged(Current.Value > lastValue); + lastValue = Current.Value; } } From 6ac1c799bde83a8fb49ad44e37de703421a4097c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 18:34:20 +0900 Subject: [PATCH 284/539] Fix `SettingsToolboxGroup` allocating excessively due to missing cache validation --- osu.Game/Overlays/SettingsToolboxGroup.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index c0948c1eab..de13bd96d4 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -151,9 +151,12 @@ namespace osu.Game.Overlays base.Update(); if (!headerTextVisibilityCache.IsValid) + { // These toolbox grouped may be contracted to only show icons. // For now, let's hide the header to avoid text truncation weirdness in such cases. headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); + headerTextVisibilityCache.Validate(); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) From 66b3945cd644abd7704adde5284211768981a36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jan 2024 10:44:30 +0100 Subject: [PATCH 285/539] Move current screen check to better place --- osu.Game/Screens/Select/PlaySongSelect.cs | 3 --- osu.Game/Screens/Select/SongSelect.cs | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 3cf8de5267..4951504ff5 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -92,9 +92,6 @@ namespace osu.Game.Screens.Select { if (playerLoader != null) return false; - if (!this.IsCurrentScreen()) - return false; - modsAtGameplayStart = Mods.Value; // Ctrl+Enter should start map with autoplay enabled. diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2d5c44e5a5..bf1724995a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -660,7 +660,8 @@ namespace osu.Game.Screens.Select logo.Action = () => { - FinaliseSelection(); + if (this.IsCurrentScreen()) + FinaliseSelection(); return false; }; } From cac0b0de6dba025e99692d208cd56f545bb05660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jan 2024 11:38:01 +0100 Subject: [PATCH 286/539] Remove unused using directive --- osu.Game/Database/RealmFileStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs index f1ed3f4b63..1bd22af4c7 100644 --- a/osu.Game/Database/RealmFileStore.cs +++ b/osu.Game/Database/RealmFileStore.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Linq; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; From a8a70be04ab13f0ad4c565b86ee206c19ba01d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jan 2024 11:49:42 +0100 Subject: [PATCH 287/539] Reference property via `nameof` rather than hardcoding --- osu.Game/Database/RealmFileStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmFileStore.cs b/osu.Game/Database/RealmFileStore.cs index 1bd22af4c7..9683baec69 100644 --- a/osu.Game/Database/RealmFileStore.cs +++ b/osu.Game/Database/RealmFileStore.cs @@ -98,7 +98,7 @@ namespace osu.Game.Database // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. realm.Write(r => { - foreach (var file in r.All().Filter("Usages.@count = 0")) + foreach (var file in r.All().Filter(@$"{nameof(RealmFile.Usages)}.@count = 0")) { totalFiles++; From 4110adc4c0a9fdc763ec73061169a5e4e8952a7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jan 2024 20:16:27 +0900 Subject: [PATCH 288/539] Fix missing wireframe on argon combo counter --- .../Screens/Play/HUD/ArgonAccuracyCounter.cs | 1 + .../Screens/Play/HUD/ArgonComboCounter.cs | 24 +++++++++++++++++++ osu.Game/Screens/Play/HUD/ComboCounter.cs | 5 ---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 521ad63426..5284e3167a 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.Play.HUD }, fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft) { + RequiredDisplayDigits = { Value = 2 }, WireframeOpacity = { BindTarget = WireframeOpacity }, Scale = new Vector2(0.5f), }, diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 5ea7fd0b82..af884aa441 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -57,6 +57,30 @@ namespace osu.Game.Screens.Play.HUD }); } + public override int DisplayedCount + { + get => base.DisplayedCount; + set + { + base.DisplayedCount = value; + updateWireframe(); + } + } + + private void updateWireframe() + { + text.RequiredDisplayDigits.Value = getDigitsRequiredForDisplayCount(); + } + + private int getDigitsRequiredForDisplayCount() + { + int digitsRequired = 1; + long c = DisplayedCount; + while ((c /= 10) > 0) + digitsRequired++; + return digitsRequired; + } + protected override LocalisableString FormatCount(int count) => $@"{count}x"; protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs index 17531281aa..93802e11c2 100644 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ComboCounter.cs @@ -11,11 +11,6 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - protected ComboCounter() - { - Current.Value = DisplayedCount = 0; - } - protected override double GetProportionalDuration(int currentValue, int newValue) { return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; From 8ad697ff4cea857bec7f18a94723b098c6e036ef Mon Sep 17 00:00:00 2001 From: wooster0 Date: Tue, 9 Jan 2024 21:28:46 +0900 Subject: [PATCH 289/539] apply some suggestions/corrections --- .../TestScenePlaylistsResultsScreen.cs | 16 ++++++---- osu.Game/Screens/Ranking/ResultsScreen.cs | 32 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index e36e7f54ba..98aeb66428 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -79,6 +79,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); + waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert($"score panel position is {real_user_position}", @@ -91,6 +92,7 @@ namespace osu.Game.Tests.Visual.Playlists InitialiseUserScoresAndBeatmap(); createResults(); + waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -103,6 +105,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); + waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); @@ -116,6 +119,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind delayed handler", () => bindHandler(true)); createResults(); + waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -126,6 +130,7 @@ namespace osu.Game.Tests.Visual.Playlists InitialiseUserScoresAndBeatmap(); createResults(); + waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -152,6 +157,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); + waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -174,12 +180,11 @@ namespace osu.Game.Tests.Visual.Playlists public void TestShowWithNoScores() { InitialiseUserScoresAndBeatmap(noScores: true); - - createResults(noScores: true); + createResults(); AddAssert("no scores visible", () => resultsScreen.ScorePanelList.GetScorePanels().Count() == 0); } - private void createResults(Func getScore = null, bool noScores = false) + private void createResults(Func getScore = null) { AddStep("load results", () => { @@ -190,13 +195,10 @@ namespace osu.Game.Tests.Visual.Playlists }); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); - waitForDisplay(noScores); } - private void waitForDisplay(bool noScores = false) + private void waitForDisplay() { - if (noScores) return; - AddUntilStep("wait for scores loaded", () => requestComplete // request handler may need to fire more than once to get scores. diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 8823e4d320..697d62ad6e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -206,25 +206,17 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - if (ScorePanelList.IsEmpty) - { - // This can happen if a beatmap part of a playlist hasn't been played yet. - VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); - } - else - { - APIRequest nextPageRequest = null; + APIRequest nextPageRequest = null; - if (ScorePanelList.IsScrolledToStart) - nextPageRequest = FetchNextPage(-1, fetchScoresCallback); - else if (ScorePanelList.IsScrolledToEnd) - nextPageRequest = FetchNextPage(1, fetchScoresCallback); + if (ScorePanelList.IsScrolledToStart) + nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + else if (ScorePanelList.IsScrolledToEnd) + nextPageRequest = FetchNextPage(1, fetchScoresCallback); - if (nextPageRequest != null) - { - lastFetchCompleted = false; - api.Queue(nextPageRequest); - } + if (nextPageRequest != null) + { + lastFetchCompleted = false; + api.Queue(nextPageRequest); } } } @@ -255,6 +247,12 @@ namespace osu.Game.Screens.Ranking addScore(s); lastFetchCompleted = true; + + if (ScorePanelList.IsEmpty) + { + // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); + } }); public override void OnEntering(ScreenTransitionEvent e) From ea37c70e0b0e032e84a9fa1ee7c902c1a7cb2c2e Mon Sep 17 00:00:00 2001 From: wooster0 Date: Tue, 9 Jan 2024 21:34:40 +0900 Subject: [PATCH 290/539] apply suggestion --- .../TestScenePlaylistsResultsScreen.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 98aeb66428..02bd7f950f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -42,8 +42,8 @@ namespace osu.Game.Tests.Visual.Playlists private int totalCount; private ScoreInfo userScore; - // We don't use SetUpSteps for this because one of the tests shouldn't receive any scores. - public void InitialiseUserScoresAndBeatmap(bool noScores = false) + [SetUpSteps] + public override void SetUpSteps() { base.SetUpSteps(); @@ -63,18 +63,24 @@ namespace osu.Game.Tests.Visual.Playlists userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); - bindHandler(noScores: noScores); - // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); }); } + public void SetUpRequestHandler(bool noScores = false) + { + AddStep("set up request handler", () => + { + bindHandler(noScores: noScores); + }); + } + [Test] public void TestShowWithUserScore() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); @@ -89,7 +95,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScore() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); createResults(); waitForDisplay(); @@ -100,7 +106,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowUserScoreWithDelay() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); AddStep("bind user score info handler", () => bindHandler(true, userScore)); @@ -114,7 +120,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScoreWithDelay() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -127,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheRight() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); createResults(); waitForDisplay(); @@ -152,7 +158,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheLeft() { - InitialiseUserScoresAndBeatmap(); + SetUpRequestHandler(); AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); @@ -179,7 +185,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowWithNoScores() { - InitialiseUserScoresAndBeatmap(noScores: true); + SetUpRequestHandler(true); createResults(); AddAssert("no scores visible", () => resultsScreen.ScorePanelList.GetScorePanels().Count() == 0); } From d5fdd0c0f9bb4a52630e9fead6b27ab44e472ca0 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Tue, 9 Jan 2024 21:44:05 +0900 Subject: [PATCH 291/539] make each test bind the handler only once, depending on its need --- .../TestScenePlaylistsResultsScreen.cs | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 02bd7f950f..727d9da50b 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -69,19 +69,9 @@ namespace osu.Game.Tests.Visual.Playlists }); } - public void SetUpRequestHandler(bool noScores = false) - { - AddStep("set up request handler", () => - { - bindHandler(noScores: noScores); - }); - } - [Test] public void TestShowWithUserScore() { - SetUpRequestHandler(); - AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -95,7 +85,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScore() { - SetUpRequestHandler(); + AddStep("bind user score info handler", () => bindHandler()); createResults(); waitForDisplay(); @@ -106,8 +96,6 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowUserScoreWithDelay() { - SetUpRequestHandler(); - AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); @@ -120,8 +108,6 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScoreWithDelay() { - SetUpRequestHandler(); - AddStep("bind delayed handler", () => bindHandler(true)); createResults(); @@ -133,13 +119,11 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheRight() { - SetUpRequestHandler(); + AddStep("bind delayed handler", () => bindHandler(true)); createResults(); waitForDisplay(); - AddStep("bind delayed handler", () => bindHandler(true)); - for (int i = 0; i < 2; i++) { int beforePanelCount = 0; @@ -158,8 +142,6 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheLeft() { - SetUpRequestHandler(); - AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -185,7 +167,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowWithNoScores() { - SetUpRequestHandler(true); + AddStep("bind user score info handler", () => bindHandler(noScores: true)); createResults(); AddAssert("no scores visible", () => resultsScreen.ScorePanelList.GetScorePanels().Count() == 0); } From 77bf6e3244111b026f930a75fbb35dd497d9c921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jan 2024 13:59:27 +0100 Subject: [PATCH 292/539] Fix missing wireframe behind "x" sign on combo counter display --- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index af884aa441..1d6ca3c893 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -74,7 +74,8 @@ namespace osu.Game.Screens.Play.HUD private int getDigitsRequiredForDisplayCount() { - int digitsRequired = 1; + // one for the single presumed starting digit, one for the "x" at the end. + int digitsRequired = 2; long c = DisplayedCount; while ((c /= 10) > 0) digitsRequired++; From 92ba77031403632ad03b86efcbd2754a47b6c646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jan 2024 14:00:58 +0100 Subject: [PATCH 293/539] Fix missing wireframe behind percent sign on accuracy counter --- osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 5284e3167a..171aa3f44b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -90,6 +90,7 @@ namespace osu.Game.Screens.Play.HUD percentText = new ArgonCounterTextComponent(Anchor.TopLeft) { Text = @"%", + RequiredDisplayDigits = { Value = 1 }, WireframeOpacity = { BindTarget = WireframeOpacity } }, } From 484e9e8ee636d9d1ec4aaf549e7615fb18682226 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Tue, 9 Jan 2024 22:09:20 +0800 Subject: [PATCH 294/539] Fix binding order of `IsPaused` bindable and disable playback controls in spectator mode. --- .../Screens/Play/PlayerSettings/PlaybackSettings.cs | 13 +++++++++++-- osu.Game/Screens/Play/SpectatorPlayer.cs | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index e208be6095..e2859868bd 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; + public readonly Bindable AllowControls = new BindableBool(true); + private readonly PlayerSliderBar rateSlider; private readonly OsuSpriteText multiplierText; @@ -71,6 +73,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, Action = () => seek(-1, seek_fast_amount), + Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -78,6 +81,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Backward, Action = () => seek(-1, seek_amount), + Enabled = { BindTarget = AllowControls }, }, play = new IconButton { @@ -95,7 +99,8 @@ namespace osu.Game.Screens.Play.PlayerSettings else gameplayClock.Start(); } - } + }, + Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -103,6 +108,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, Action = () => seek(1, seek_amount), + Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -110,6 +116,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastForward, Action = () => seek(1, seek_fast_amount), + Enabled = { BindTarget = AllowControls }, }, }, }, @@ -150,7 +157,9 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.LoadComplete(); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); - gameplayClock?.IsPaused.BindTo(isPaused); + + if (gameplayClock != null) + isPaused.BindTarget = gameplayClock.IsPaused; } private partial class SeekButton : IconButton diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 2faead0ee1..dd7418d563 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play master.UserPlaybackRate.Value = 1; } }, true); + + HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.AllowControls.Value = false; } /// From 91677158a058e03bae2bace8d236c1ef12a5e66e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jan 2024 22:33:00 +0900 Subject: [PATCH 295/539] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b179b8b837..56931bbcb4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7e03ab50e2..c180baeab7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 49d13cda6b80fcd439a7d40723e2635c7792eee2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jan 2024 23:09:43 +0900 Subject: [PATCH 296/539] Fix failing test by setting health on source of truth --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 18 ++++++++++-------- osu.Game/Screens/Play/HUD/FailingLayer.cs | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 235ada2d63..684d263a58 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -22,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Bindable showHealth = new Bindable(); + private HealthProcessor healthProcessor; + [Resolved] private OsuConfigManager config { get; set; } @@ -29,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create layer", () => { - Child = new HealthProcessorContainer(healthProcessor) + Child = new HealthProcessorContainer(this.healthProcessor = healthProcessor) { RelativeSizeAxes = Axes.Both, Child = layer = new FailingLayer() @@ -50,12 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddSliderStep("current health", 0.0, 1.0, 1.0, val => { if (layer != null) - layer.Current.Value = val; + healthProcessor.Health.Value = val; }); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer fade is visible", () => layer.ChildrenOfType().First().Alpha > 0.1f); - AddStep("set health to 1", () => layer.Current.Value = 1f); + AddStep("set health to 1", () => healthProcessor.Health.Value = 1f); AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType().First().IsPresent); } @@ -65,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay create(new DrainingHealthProcessor(0)); AddUntilStep("layer is visible", () => layer.IsPresent); AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay { create(new AccumulatingHealthProcessor(1)); AddUntilStep("layer is not visible", () => !layer.IsPresent); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerVisibilityWithDrainingProcessor() { create(new DrainingHealthProcessor(0)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } @@ -92,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay { create(new DrainingHealthProcessor(0)); - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1); AddStep("don't show health", () => showHealth.Value = false); AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 3954e23cbe..2bac7660b3 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -100,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { + base.Update(); + double target = Math.Clamp(max_alpha * (1 - Current.Value / low_health_threshold), 0, max_alpha); boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f); - - base.Update(); } } } From 84f704a6c2747360d89b5c241fa09ecb5b3a60e8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jan 2024 17:15:27 +0300 Subject: [PATCH 297/539] Add failing test case --- .../Resources/mania-skin-broken-array.ini | 3 +++ .../Skins/LegacyManiaSkinDecoderTest.cs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 osu.Game.Tests/Resources/mania-skin-broken-array.ini diff --git a/osu.Game.Tests/Resources/mania-skin-broken-array.ini b/osu.Game.Tests/Resources/mania-skin-broken-array.ini new file mode 100644 index 0000000000..5a6d37eef6 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-broken-array.ini @@ -0,0 +1,3 @@ +[Mania] +Keys: 4 +ColumnLineWidth: 3,,3,3,3 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index b96bf09255..c4117014db 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -114,5 +114,24 @@ namespace osu.Game.Tests.Skins Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16)); } } + + [Test] + public void TestParseArrayWithSomeEmptyElements() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-broken-array.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero + Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[3], Is.EqualTo(3)); + Assert.That(configs[0].ColumnLineWidth[4], Is.EqualTo(3)); + } + } } } From 698ae66a494a899bfeb4159d72db665a7f514a8a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jan 2024 17:41:58 +0300 Subject: [PATCH 298/539] Fix mania skin array decoder not handling malformed entries rigorously --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index b472afb74f..720b05a0b1 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -155,7 +155,15 @@ namespace osu.Game.Skinning if (i >= output.Length) break; - output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * (applyScaleFactor ? LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR : 1); + if (!float.TryParse(values[i], NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out float parsedValue)) + // some skins may provide incorrect entries in array values. to match stable behaviour, read such entries as zero. + // see: https://github.com/ppy/osu/issues/26464, stable code: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/Components/Section.cs#L134-L137 + parsedValue = 0; + + if (applyScaleFactor) + parsedValue *= LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + + output[i] = parsedValue; } } } From 7ca4d854416ae663825bfc7d84c5df0288a7ce41 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jan 2024 17:48:47 +0300 Subject: [PATCH 299/539] Remove unnecessary `AllowThousands` flag The flag is there to match `float.Parse` behaviour, but it's too illogical and unnecessary to have it. --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 720b05a0b1..ff6e7fc38e 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -155,7 +155,7 @@ namespace osu.Game.Skinning if (i >= output.Length) break; - if (!float.TryParse(values[i], NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out float parsedValue)) + if (!float.TryParse(values[i], NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue)) // some skins may provide incorrect entries in array values. to match stable behaviour, read such entries as zero. // see: https://github.com/ppy/osu/issues/26464, stable code: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/Components/Section.cs#L134-L137 parsedValue = 0; From 7b848e1458c484cad520ce8ceee4d4fab64fa726 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jan 2024 17:51:27 +0300 Subject: [PATCH 300/539] Assert column line width length for extra safety --- osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index c4117014db..d577e0fedf 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -126,6 +126,7 @@ namespace osu.Game.Tests.Skins var configs = decoder.Decode(stream); Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].ColumnLineWidth.Length, Is.EqualTo(5)); Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3)); Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3)); From c2706ca91be7293a47142c0ae184774a17b263f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jan 2024 23:48:16 +0900 Subject: [PATCH 301/539] Also show drain on argon health display test --- .../Visual/Gameplay/TestSceneArgonHealthDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 30fb4412f4..06d199513c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -159,5 +159,11 @@ namespace osu.Game.Tests.Visual.Gameplay Type = HitResult.Perfect }); } + + protected override void Update() + { + base.Update(); + healthProcessor.Health.Value -= 0.0001f * Time.Elapsed; + } } } From 5d6f767dbdc613ca54bbfdb98880c33517769b1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jan 2024 23:52:39 +0900 Subject: [PATCH 302/539] Reduce excessive `Color4` allocations during path colour updates --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index afc64b0b84..236bd3366d 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -328,14 +328,17 @@ namespace osu.Game.Screens.Play.HUD private partial class BackgroundPath : SmoothPath { + private static readonly Color4 colour_white = Color4.White.Opacity(0.8f); + private static readonly Color4 colour_black = Color4.Black.Opacity(0.2f); + protected override Color4 ColourAt(float position) { if (position <= 0.16f) - return Color4.White.Opacity(0.8f); + return colour_white; return Interpolation.ValueAt(position, - Color4.White.Opacity(0.8f), - Color4.Black.Opacity(0.2f), + colour_white, + colour_black, -0.5f, 1f, Easing.OutQuint); } } @@ -374,12 +377,14 @@ namespace osu.Game.Screens.Play.HUD public float GlowPortion { get; init; } + private static readonly Colour4 transparent_black = Colour4.Black.Opacity(0.0f); + protected override Color4 ColourAt(float position) { if (position >= GlowPortion) return BarColour; - return Interpolation.ValueAt(position, Colour4.Black.Opacity(0.0f), GlowColour, 0.0, GlowPortion, Easing.InQuint); + return Interpolation.ValueAt(position, transparent_black, GlowColour, 0.0, GlowPortion, Easing.InQuint); } } } From 7c9adc7ad3c1d4c72620d92cb44bd665c6e8a28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jan 2024 16:51:40 +0100 Subject: [PATCH 303/539] Fix incorrect score conversion on selected beatmaps due to incorrect `difficultyPeppyStars` rounding Fixes issue that occurs on *about* 246 beatmaps and was first described by me on discord: https://discord.com/channels/188630481301012481/188630652340404224/1154367700378865715 and then rediscovered again during work on https://github.com/ppy/osu/pull/26405: https://gist.github.com/bdach/414d5289f65b0399fa8f9732245a4f7c#venenog-on-ultmate-end-by-blacky-overdose-631 It so happens that in stable, due to .NET Framework internals, float math would be performed using x87 registers and opcodes. .NET (Core) however uses SSE instructions on 32- and 64-bit words. x87 registers are _80 bits_ wide. Which is notably wider than _both_ float and double. Therefore, on a significant number of beatmaps, the rounding would not produce correct values due to insufficient precision. See following gist for corroboration of the above: https://gist.github.com/bdach/dcde58d5a3607b0408faa3aa2b67bf10 Thus, to crudely - but, seemingly accurately, after checking across all ranked maps - emulate this, use `decimal`, which is slow, but has bigger precision than `double`. The single known exception beatmap in whose case this results in an incorrect result is https://osu.ppy.sh/beatmapsets/1156087#osu/2625853 which is considered an "acceptable casualty" of sorts. Doing this requires some fooling of the compiler / runtime (see second inline comment in new method). To corroborate that this is required, you can try the following code snippet: Console.WriteLine(string.Join(' ', BitConverter.GetBytes(1.3f).Select(x => x.ToString("X2")))); Console.WriteLine(string.Join(' ', BitConverter.GetBytes(1.3).Select(x => x.ToString("X2")))); Console.WriteLine(); decimal d1 = (decimal)1.3f; decimal d2 = (decimal)1.3; decimal d3 = (decimal)(double)1.3f; Console.WriteLine(string.Join(' ', decimal.GetBits(d1).SelectMany(BitConverter.GetBytes).Select(x => x.ToString("X2")))); Console.WriteLine(string.Join(' ', decimal.GetBits(d2).SelectMany(BitConverter.GetBytes).Select(x => x.ToString("X2")))); Console.WriteLine(string.Join(' ', decimal.GetBits(d3).SelectMany(BitConverter.GetBytes).Select(x => x.ToString("X2")))); which will print 66 66 A6 3F CD CC CC CC CC CC F4 3F 0D 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 0D 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 8C 5D 89 FB 3B 76 00 00 00 00 00 00 00 00 0E 00 Note that despite `d1` being converted from a less-precise floating- -point value than `d2`, it still is represented 100% accurately as a decimal number. After applying this change, recomputation of legacy scoring attributes for *all* rulesets will be required. --- .../Difficulty/CatchLegacyScoreSimulator.cs | 9 ++--- .../Difficulty/OsuLegacyScoreSimulator.cs | 9 ++--- .../Difficulty/TaikoLegacyScoreSimulator.cs | 7 ++-- .../Objects/Legacy/LegacyRulesetExtensions.cs | 35 +++++++++++++++++++ osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 7 +++- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index f65b6ef381..f931795ff2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; @@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - int difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - - scoreMultiplier = difficultyPeppyStars; + scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index a76054b42c..b808deab5c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - int difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - - scoreMultiplier = difficultyPeppyStars; + scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index b20aa4f2b6..66ff0fc3d9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; @@ -65,11 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); + difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs index 53cf835248..2a5a11161b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -57,5 +57,40 @@ namespace osu.Game.Rulesets.Objects.Legacy return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1); } + + public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength) + { + /* + * WARNING: DO NOT TOUCH IF YOU DO NOT KNOW WHAT YOU ARE DOING + * + * It so happens that in stable, due to .NET Framework internals, float math would be performed + * using x87 registers and opcodes. + * .NET (Core) however uses SSE instructions on 32- and 64-bit words. + * x87 registers are _80 bits_ wide. Which is notably wider than _both_ float and double. + * Therefore, on a significant number of beatmaps, the rounding would not produce correct values. + * + * Thus, to crudely - but, seemingly *mostly* accurately, after checking across all ranked maps - emulate this, + * use `decimal`, which is slow, but has bigger precision than `double`. + * At the time of writing, there is _one_ ranked exception to this - namely https://osu.ppy.sh/beatmapsets/1156087#osu/2625853 - + * but it is considered an "acceptable casualty", since in that case scores aren't inflated by _that_ much compared to others. + */ + + decimal objectToDrainRatio = drainLength != 0 + ? Math.Clamp((decimal)objectCount / drainLength * 8, 0, 16) + : 16; + + /* + * Notably, THE `double` CASTS BELOW ARE IMPORTANT AND MUST REMAIN. + * Their goal is to trick the compiler / runtime into NOT promoting from single-precision float, as doing so would prompt it + * to attempt to "silently" fix the single-precision values when converting to decimal, + * which is NOT what the x87 FPU does. + */ + + decimal drainRate = (decimal)(double)difficulty.DrainRate; + decimal overallDifficulty = (decimal)(double)difficulty.OverallDifficulty; + decimal circleSize = (decimal)(double)difficulty.CircleSize; + + return (int)Math.Round((drainRate + overallDifficulty + circleSize + objectToDrainRatio) / 38 * 5); + } } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 110cf63e5c..389b20b5c8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -13,6 +13,7 @@ using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.IO.Serialization; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -38,9 +39,13 @@ namespace osu.Game.Scoring.Legacy /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. /// 30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores. /// 30000011: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores. + /// + /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct + /// method. Reconvert all scores. + /// /// /// - public const int LATEST_VERSION = 30000011; + public const int LATEST_VERSION = 30000012; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 861080d3ae42de1b57e4471abc2f4f43a3026fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jan 2024 10:04:37 +0100 Subject: [PATCH 304/539] Move simulated drain to separate test case Having it on at all times was causing other tests to fail. --- .../Visual/Gameplay/TestSceneArgonHealthDisplay.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 06d199513c..5d2921107e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -160,10 +161,14 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected override void Update() + [Test] + public void TestSimulateDrain() { - base.Update(); - healthProcessor.Health.Value -= 0.0001f * Time.Elapsed; + ScheduledDelegate del = null!; + + AddStep("simulate drain", () => del = Scheduler.AddDelayed(() => healthProcessor.Health.Value -= 0.00025f * Time.Elapsed, 0, true)); + AddUntilStep("wait until zero", () => healthProcessor.Health.Value == 0); + AddStep("cancel drain", () => del.Cancel()); } } } From 600e4b6ef3c930ad8dc82f513c870582747e5bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jan 2024 10:17:32 +0100 Subject: [PATCH 305/539] Adjust skinnable health display test scene for usability --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index f215a5d528..1849e8abd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); - protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f }; + protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 600, UseRelativeSize = { Value = false } }; protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) }; protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) }; From 90ab306a96e17b3d7e21011fb75acd9d753ba071 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 11 Jan 2024 14:55:04 +0300 Subject: [PATCH 306/539] Implement ArgonHealthDisplayBackground --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 11 +- .../ArgonHealthDisplayBackground.cs | 168 ++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 236bd3366d..868ef9283d 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -19,6 +19,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -100,6 +101,14 @@ namespace osu.Game.Screens.Play.HUD background = new BackgroundPath { PathRadius = MAIN_PATH_RADIUS, + Alpha = 0, + AlwaysPresent = true + }, + new ArgonHealthDisplayBackground + { + RelativeSizeAxes = Axes.Both, + PathRadius = MAIN_PATH_RADIUS, + PathPadding = MAIN_PATH_RADIUS }, glowBar = new BarPath { @@ -121,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD GlowColour = main_bar_glow_colour, PathRadius = MAIN_PATH_RADIUS, GlowPortion = 0.6f, - }, + } } }; } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs new file mode 100644 index 0000000000..a819fb6f30 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -0,0 +1,168 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts +{ + public partial class ArgonHealthDisplayBackground : Box + { + private float endProgress = 1f; + + public float EndProgress + { + get => endProgress; + set + { + if (endProgress == value) + return; + + endProgress = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float startProgress = 0f; + + public float StartProgress + { + get => startProgress; + set + { + if (startProgress == value) + return; + + startProgress = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float radius = 10f; + + public float PathRadius + { + get => radius; + set + { + if (radius == value) + return; + + radius = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float padding = 10f; + + public float PathPadding + { + get => padding; + set + { + if (padding == value) + return; + + padding = value; + Invalidate(Invalidation.DrawNode); + } + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPathBackground"); + } + + protected override void Update() + { + base.Update(); + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); + + private class ArgonBarPathDrawNode : SpriteDrawNode + { + protected new ArgonHealthDisplayBackground Source => (ArgonHealthDisplayBackground)base.Source; + + public ArgonBarPathDrawNode(ArgonHealthDisplayBackground source) + : base(source) + { + } + + private Vector2 size; + private float startProgress; + private float endProgress; + private float pathRadius; + private float padding; + + public override void ApplyState() + { + base.ApplyState(); + + size = Source.DrawSize; + endProgress = Source.endProgress; + startProgress = Math.Min(Source.startProgress, endProgress); + pathRadius = Source.PathRadius; + padding = Source.PathPadding; + } + + protected override void Draw(IRenderer renderer) + { + if (pathRadius == 0) + return; + + base.Draw(renderer); + } + + private IUniformBuffer parametersBuffer; + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new ArgonBarPathBackgroundParameters + { + Size = size, + StartProgress = startProgress, + EndProgress = endProgress, + PathRadius = pathRadius, + Padding = padding + }; + + shader.BindUniformBlock("m_ArgonBarPathBackgroundParameters", parametersBuffer); + } + + protected override bool CanDrawOpaqueInterior => false; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + parametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ArgonBarPathBackgroundParameters + { + public UniformVector2 Size; + public UniformFloat StartProgress; + public UniformFloat EndProgress; + public UniformFloat PathRadius; + public UniformFloat Padding; + private UniformPadding8 pad; + } + } + } +} From f1db7db2592403d5783a63335362cf07a243bda7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 11 Jan 2024 16:10:06 +0300 Subject: [PATCH 307/539] Implement ArgonHealthDisplayBar --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 211 +++------------- .../ArgonHealthDisplayBar.cs | 227 ++++++++++++++++++ 2 files changed, 265 insertions(+), 173 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 868ef9283d..9b91252a06 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -2,23 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Lines; using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts; using osu.Game.Skinning; using osuTK; @@ -41,16 +36,14 @@ namespace osu.Game.Screens.Play.HUD [SettingSource("Use relative size")] public BindableBool UseRelativeSize { get; } = new BindableBool(true); - private BarPath mainBar = null!; + private ArgonHealthDisplayBar mainBar = null!; /// /// Used to show a glow at the end of the main bar, or red "damage" area when missing. /// - private BarPath glowBar = null!; + private ArgonHealthDisplayBar glowBar = null!; - private BackgroundPath background = null!; - - private SliderPath barPath = null!; + private Container content = null!; private static readonly Colour4 main_bar_colour = Colour4.White; private static readonly Colour4 main_bar_glow_colour = Color4Extensions.FromHex("#7ED7FD").Opacity(0.5f); @@ -59,23 +52,15 @@ namespace osu.Game.Screens.Play.HUD private bool displayingMiss => resetMissBarDelegate != null; - private readonly List vertices = new List(); - private double glowBarValue; private double healthBarValue; public const float MAIN_PATH_RADIUS = 10f; - - private const float curve_start_offset = 70; - private const float curve_end_offset = 40; private const float padding = MAIN_PATH_RADIUS * 2; - private const float curve_smoothness = 10; private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); - private readonly Cached pathVerticesCache = new Cached(); - public ArgonHealthDisplay() { AddLayout(drawSizeLayout); @@ -93,42 +78,40 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Y; - InternalChild = new Container + InternalChild = content = new Container { - AutoSizeAxes = Axes.Both, Children = new Drawable[] { - background = new BackgroundPath - { - PathRadius = MAIN_PATH_RADIUS, - Alpha = 0, - AlwaysPresent = true - }, new ArgonHealthDisplayBackground { RelativeSizeAxes = Axes.Both, PathRadius = MAIN_PATH_RADIUS, PathPadding = MAIN_PATH_RADIUS }, - glowBar = new BarPath + new Container { - BarColour = Color4.White, - GlowColour = main_bar_glow_colour, - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), - PathRadius = 40f, - // Kinda hacky, but results in correct positioning with increased path radius. - Margin = new MarginPadding(-30f), - GlowPortion = 0.9f, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-30f), + Child = glowBar = new ArgonHealthDisplayBar + { + RelativeSizeAxes = Axes.Both, + BarColour = Color4.White, + GlowColour = main_bar_glow_colour, + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = 40f, + PathPadding = 40f, + GlowPortion = 0.9f, + } }, - mainBar = new BarPath + mainBar = new ArgonHealthDisplayBar { - AutoSizeAxes = Axes.None, RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, BarColour = main_bar_colour, GlowColour = main_bar_glow_colour, PathRadius = MAIN_PATH_RADIUS, + PathPadding = MAIN_PATH_RADIUS, GlowPortion = 0.6f, } } @@ -151,7 +134,7 @@ namespace osu.Game.Screens.Play.HUD UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); Width = previousWidth; - BarHeight.BindValueChanged(_ => updatePath(), true); + BarHeight.BindValueChanged(_ => updateContentSize(), true); } private void onNewJudgement(JudgementResult result) => pendingMissAnimation |= !result.IsHit; @@ -162,7 +145,7 @@ namespace osu.Game.Screens.Play.HUD if (!drawSizeLayout.IsValid) { - updatePath(); + updateContentSize(); drawSizeLayout.Validate(); } @@ -173,7 +156,7 @@ namespace osu.Game.Screens.Play.HUD mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, glowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); - updatePathVertices(); + updatePathProgress(); } protected override void HealthChanged(bool increase) @@ -203,10 +186,9 @@ namespace osu.Game.Screens.Play.HUD if (!displayingMiss) { - // TODO: REMOVE THIS. It's recreating textures. - glowBar.TransformTo(nameof(BarPath.GlowColour), Colour4.White, 30, Easing.OutQuint) + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), Colour4.White, 30, Easing.OutQuint) .Then() - .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); + .TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); } } @@ -221,13 +203,11 @@ namespace osu.Game.Screens.Play.HUD finishMissDisplay(); }, out resetMissBarDelegate); - // TODO: REMOVE THIS. It's recreating textures. - glowBar.TransformTo(nameof(BarPath.BarColour), new Colour4(255, 147, 147, 255), 100, Easing.OutQuint).Then() - .TransformTo(nameof(BarPath.BarColour), new Colour4(255, 93, 93, 255), 800, Easing.OutQuint); + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.BarColour), new Colour4(255, 147, 147, 255), 100, Easing.OutQuint).Then() + .TransformTo(nameof(ArgonHealthDisplayBar.BarColour), new Colour4(255, 93, 93, 255), 800, Easing.OutQuint); - // TODO: REMOVE THIS. It's recreating textures. - glowBar.TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255).Lighten(0.2f)) - .TransformTo(nameof(BarPath.GlowColour), new Colour4(253, 0, 0, 255), 800, Easing.OutQuint); + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), new Colour4(253, 0, 0, 255).Lighten(0.2f)) + .TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), new Colour4(253, 0, 0, 255), 800, Easing.OutQuint); } private void finishMissDisplay() @@ -237,53 +217,22 @@ namespace osu.Game.Screens.Play.HUD if (Current.Value > 0) { - // TODO: REMOVE THIS. It's recreating textures. - glowBar.TransformTo(nameof(BarPath.BarColour), main_bar_colour, 300, Easing.In); - glowBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.In); + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.BarColour), main_bar_colour, 300, Easing.In); + glowBar.TransformTo(nameof(ArgonHealthDisplayBar.GlowColour), main_bar_glow_colour, 300, Easing.In); } resetMissBarDelegate?.Cancel(); resetMissBarDelegate = null; } - private void updatePath() + private void updateContentSize() { float usableWidth = DrawWidth - padding; if (usableWidth < 0) enforceMinimumWidth(); - // the display starts curving at `curve_start_offset` units from the right and ends curving at `curve_end_offset`. - // to ensure that the curve is symmetric when it starts being narrow enough, add a `curve_end_offset` to the left side too. - const float rescale_cutoff = curve_start_offset + curve_end_offset; - - float barLength = Math.Max(DrawWidth - padding, rescale_cutoff); - float curveStart = barLength - curve_start_offset; - float curveEnd = barLength - curve_end_offset; - - Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized(); - - barPath = new SliderPath(new[] - { - new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), - new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.BEZIER), - new PathControlPoint(new Vector2(curveStart, 0)), - new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.LINEAR), - new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.BEZIER), - new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)), - new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.LINEAR), - new PathControlPoint(new Vector2(barLength, BarHeight.Value)), - }); - - if (DrawWidth - padding < rescale_cutoff) - rescalePathProportionally(); - - barPath.GetPathToProgress(vertices, 0.0, 1.0); - - background.Vertices = vertices; - mainBar.Vertices = vertices; - glowBar.Vertices = vertices; - - updatePathVertices(); + content.Size = new Vector2(DrawWidth, BarHeight.Value + padding); + updatePathProgress(); void enforceMinimumWidth() { @@ -296,35 +245,13 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = relativeAxes; } - - void rescalePathProportionally() - { - foreach (var point in barPath.ControlPoints) - point.Position = new Vector2(point.Position.X / barLength * (DrawWidth - padding), point.Position.Y); - } } - private void updatePathVertices() + private void updatePathProgress() { - barPath.GetPathToProgress(vertices, 0.0, healthBarValue); - if (vertices.Count == 0) vertices.Add(Vector2.Zero); - Vector2 initialVertex = vertices[0]; - for (int i = 0; i < vertices.Count; i++) - vertices[i] -= initialVertex; - - mainBar.Vertices = vertices; - mainBar.Position = initialVertex; - - barPath.GetPathToProgress(vertices, healthBarValue, Math.Max(glowBarValue, healthBarValue)); - if (vertices.Count == 0) vertices.Add(Vector2.Zero); - initialVertex = vertices[0]; - for (int i = 0; i < vertices.Count; i++) - vertices[i] -= initialVertex; - - glowBar.Vertices = vertices; - glowBar.Position = initialVertex; - - pathVerticesCache.Validate(); + mainBar.EndProgress = (float)healthBarValue; + glowBar.StartProgress = (float)healthBarValue; + glowBar.EndProgress = (float)Math.Max(glowBarValue, healthBarValue); } protected override void Dispose(bool isDisposing) @@ -334,67 +261,5 @@ namespace osu.Game.Screens.Play.HUD if (HealthProcessor.IsNotNull()) HealthProcessor.NewJudgement -= onNewJudgement; } - - private partial class BackgroundPath : SmoothPath - { - private static readonly Color4 colour_white = Color4.White.Opacity(0.8f); - private static readonly Color4 colour_black = Color4.Black.Opacity(0.2f); - - protected override Color4 ColourAt(float position) - { - if (position <= 0.16f) - return colour_white; - - return Interpolation.ValueAt(position, - colour_white, - colour_black, - -0.5f, 1f, Easing.OutQuint); - } - } - - private partial class BarPath : SmoothPath - { - private Colour4 barColour; - - public Colour4 BarColour - { - get => barColour; - set - { - if (barColour == value) - return; - - barColour = value; - InvalidateTexture(); - } - } - - private Colour4 glowColour; - - public Colour4 GlowColour - { - get => glowColour; - set - { - if (glowColour == value) - return; - - glowColour = value; - InvalidateTexture(); - } - } - - public float GlowPortion { get; init; } - - private static readonly Colour4 transparent_black = Colour4.Black.Opacity(0.0f); - - protected override Color4 ColourAt(float position) - { - if (position >= GlowPortion) - return BarColour; - - return Interpolation.ValueAt(position, transparent_black, GlowColour, 0.0, GlowPortion, Easing.InQuint); - } - } } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs new file mode 100644 index 0000000000..6bc2232776 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -0,0 +1,227 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts +{ + public partial class ArgonHealthDisplayBar : Box + { + private float endProgress = 1f; + + public float EndProgress + { + get => endProgress; + set + { + if (endProgress == value) + return; + + endProgress = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float startProgress = 0f; + + public float StartProgress + { + get => startProgress; + set + { + if (startProgress == value) + return; + + startProgress = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float radius = 10f; + + public float PathRadius + { + get => radius; + set + { + if (radius == value) + return; + + radius = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float padding = 10f; + + public float PathPadding + { + get => padding; + set + { + if (padding == value) + return; + + padding = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float glowPortion; + + public float GlowPortion + { + get => glowPortion; + set + { + if (glowPortion == value) + return; + + glowPortion = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Colour4 barColour = Color4.White; + + public Colour4 BarColour + { + get => barColour; + set + { + if (barColour == value) + return; + + barColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Colour4 glowColour = Color4.White.Opacity(0); + + public Colour4 GlowColour + { + get => glowColour; + set + { + if (glowColour == value) + return; + + glowColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPath"); + } + + protected override void Update() + { + base.Update(); + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); + + private class ArgonBarPathDrawNode : SpriteDrawNode + { + protected new ArgonHealthDisplayBar Source => (ArgonHealthDisplayBar)base.Source; + + public ArgonBarPathDrawNode(ArgonHealthDisplayBar source) + : base(source) + { + } + + private Vector2 size; + private float startProgress; + private float endProgress; + private float pathRadius; + private float padding; + private float glowPortion; + private Color4 barColour; + private Color4 glowColour; + + public override void ApplyState() + { + base.ApplyState(); + + size = Source.DrawSize; + endProgress = Source.endProgress; + startProgress = Math.Min(Source.startProgress, endProgress); + pathRadius = Source.PathRadius; + padding = Source.PathPadding; + glowPortion = Source.GlowPortion; + barColour = Source.barColour; + glowColour = Source.glowColour; + } + + protected override void Draw(IRenderer renderer) + { + if (pathRadius == 0) + return; + + base.Draw(renderer); + } + + private IUniformBuffer parametersBuffer; + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new ArgonBarPathParameters + { + BarColour = new Vector4(barColour.R, barColour.G, barColour.B, barColour.A), + GlowColour = new Vector4(glowColour.R, glowColour.G, glowColour.B, glowColour.A), + GlowPortion = glowPortion, + Size = size, + StartProgress = startProgress, + EndProgress = endProgress, + PathRadius = pathRadius, + Padding = padding + }; + + shader.BindUniformBlock("m_ArgonBarPathParameters", parametersBuffer); + } + + protected override bool CanDrawOpaqueInterior => false; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + parametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ArgonBarPathParameters + { + public UniformVector4 BarColour; + public UniformVector4 GlowColour; + public UniformVector2 Size; + public UniformFloat StartProgress; + public UniformFloat EndProgress; + public UniformFloat PathRadius; + public UniformFloat Padding; + public UniformFloat GlowPortion; + private UniformPadding4 pad; + } + } + } +} From d75bf55c586f72ec28ffc5ce228f6ec47338274f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 11 Jan 2024 18:28:00 +0300 Subject: [PATCH 308/539] CI fixes --- .../ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs | 4 ++-- .../Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs index a819fb6f30..781bd8e6e9 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } } - private float startProgress = 0f; + private float startProgress; public float StartProgress { @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts public UniformFloat EndProgress; public UniformFloat PathRadius; public UniformFloat Padding; - private UniformPadding8 pad; + private readonly UniformPadding8 pad; } } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs index 6bc2232776..ac73698f4c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } } - private float startProgress = 0f; + private float startProgress; public float StartProgress { @@ -220,7 +220,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts public UniformFloat PathRadius; public UniformFloat Padding; public UniformFloat GlowPortion; - private UniformPadding4 pad; + private readonly UniformPadding4 pad; } } } From bbb36da32312064bc2d154ae2637b44c670ef76c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 11 Jan 2024 18:58:12 +0300 Subject: [PATCH 309/539] Don't pass start and end progress to the background --- .../ArgonHealthDisplayBackground.cs | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs index 781bd8e6e9..ccb224eca6 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -18,36 +17,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts { public partial class ArgonHealthDisplayBackground : Box { - private float endProgress = 1f; - - public float EndProgress - { - get => endProgress; - set - { - if (endProgress == value) - return; - - endProgress = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float startProgress; - - public float StartProgress - { - get => startProgress; - set - { - if (startProgress == value) - return; - - startProgress = value; - Invalidate(Invalidation.DrawNode); - } - } - private float radius = 10f; public float PathRadius @@ -102,8 +71,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } private Vector2 size; - private float startProgress; - private float endProgress; private float pathRadius; private float padding; @@ -112,8 +79,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts base.ApplyState(); size = Source.DrawSize; - endProgress = Source.endProgress; - startProgress = Math.Min(Source.startProgress, endProgress); pathRadius = Source.PathRadius; padding = Source.PathPadding; } @@ -136,8 +101,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts parametersBuffer.Data = new ArgonBarPathBackgroundParameters { Size = size, - StartProgress = startProgress, - EndProgress = endProgress, PathRadius = pathRadius, Padding = padding }; @@ -157,11 +120,8 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts private record struct ArgonBarPathBackgroundParameters { public UniformVector2 Size; - public UniformFloat StartProgress; - public UniformFloat EndProgress; public UniformFloat PathRadius; public UniformFloat Padding; - private readonly UniformPadding8 pad; } } } From 101a26a53e3da3e16ec26a5c89891261a2b998c9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 12 Jan 2024 02:54:04 +0300 Subject: [PATCH 310/539] Update start and end progress in one go --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 5 +-- .../ArgonHealthDisplayBar.cs | 37 +++++-------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 9b91252a06..1f633c9e82 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -249,9 +249,8 @@ namespace osu.Game.Screens.Play.HUD private void updatePathProgress() { - mainBar.EndProgress = (float)healthBarValue; - glowBar.StartProgress = (float)healthBarValue; - glowBar.EndProgress = (float)Math.Max(glowBarValue, healthBarValue); + mainBar.ProgressRange = new Vector2(0f, (float)healthBarValue); + glowBar.ProgressRange = new Vector2((float)healthBarValue, (float)Math.Max(glowBarValue, healthBarValue)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs index ac73698f4c..f92ab529ef 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -20,32 +20,17 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts { public partial class ArgonHealthDisplayBar : Box { - private float endProgress = 1f; + private Vector2 progressRange = new Vector2(0f, 1f); - public float EndProgress + public Vector2 ProgressRange { - get => endProgress; + get => progressRange; set { - if (endProgress == value) + if (progressRange == value) return; - endProgress = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float startProgress; - - public float StartProgress - { - get => startProgress; - set - { - if (startProgress == value) - return; - - startProgress = value; + progressRange = value; Invalidate(Invalidation.DrawNode); } } @@ -149,8 +134,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } private Vector2 size; - private float startProgress; - private float endProgress; + private Vector2 progressRange; private float pathRadius; private float padding; private float glowPortion; @@ -162,8 +146,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts base.ApplyState(); size = Source.DrawSize; - endProgress = Source.endProgress; - startProgress = Math.Min(Source.startProgress, endProgress); + progressRange = new Vector2(Math.Min(Source.progressRange.X, Source.progressRange.Y), Source.progressRange.Y); pathRadius = Source.PathRadius; padding = Source.PathPadding; glowPortion = Source.GlowPortion; @@ -192,8 +175,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts GlowColour = new Vector4(glowColour.R, glowColour.G, glowColour.B, glowColour.A), GlowPortion = glowPortion, Size = size, - StartProgress = startProgress, - EndProgress = endProgress, + ProgressRange = progressRange, PathRadius = pathRadius, Padding = padding }; @@ -215,8 +197,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts public UniformVector4 BarColour; public UniformVector4 GlowColour; public UniformVector2 Size; - public UniformFloat StartProgress; - public UniformFloat EndProgress; + public UniformVector2 ProgressRange; public UniformFloat PathRadius; public UniformFloat Padding; public UniformFloat GlowPortion; From e861661037cce1079aee84e00b95efae50804873 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 12 Jan 2024 03:03:38 +0300 Subject: [PATCH 311/539] Remove invalidations in update oops --- .../ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs | 6 ------ .../HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs | 6 ------ 2 files changed, 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs index ccb224eca6..a98b3dc1f3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -53,12 +53,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPathBackground"); } - protected override void Update() - { - base.Update(); - Invalidate(Invalidation.DrawNode); - } - protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); private class ArgonBarPathDrawNode : SpriteDrawNode diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs index f92ab529ef..4f4af02d66 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -116,12 +116,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "ArgonBarPath"); } - protected override void Update() - { - base.Update(); - Invalidate(Invalidation.DrawNode); - } - protected override DrawNode CreateDrawNode() => new ArgonBarPathDrawNode(this); private class ArgonBarPathDrawNode : SpriteDrawNode From c40462811363c7432ede91c4475d7d6e7cd6d28b Mon Sep 17 00:00:00 2001 From: Nitrous Date: Fri, 12 Jan 2024 15:12:02 +0800 Subject: [PATCH 312/539] move creation of `PlaybackSettings` to `ReplayPlayer` --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 9 +++++---- osu.Game/Screens/Play/Player.cs | 3 --- .../Screens/Play/PlayerSettings/PlaybackSettings.cs | 6 ------ osu.Game/Screens/Play/ReplayPlayer.cs | 12 ++++++++++++ osu.Game/Screens/Play/SpectatorPlayer.cs | 2 -- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index dbb0456cd0..b7f3dc36c3 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -12,17 +12,19 @@ namespace osu.Game.Screens.Play.HUD { private const int fade_duration = 200; - public readonly PlaybackSettings PlaybackSettings; - public readonly VisualSettings VisualSettings; + protected override Container Content => content; + + private readonly FillFlowContainer content; + public PlayerSettingsOverlay() { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; AutoSizeAxes = Axes.Both; - Child = new FillFlowContainer + InternalChild = content = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -31,7 +33,6 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(0, 20), Children = new PlayerSettingsGroup[] { - PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } }, new AudioSettings { Expanded = { Value = false } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b87306b9a2..29c7849685 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -476,9 +476,6 @@ namespace osu.Game.Screens.Play skipOutroOverlay.Expire(); } - if (GameplayClockContainer is MasterGameplayClockContainer master) - HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; - return container; } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index e2859868bd..0c9f5bb6ee 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - public readonly Bindable AllowControls = new BindableBool(true); private readonly PlayerSliderBar rateSlider; @@ -73,7 +72,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, Action = () => seek(-1, seek_fast_amount), - Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -81,7 +79,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Backward, Action = () => seek(-1, seek_amount), - Enabled = { BindTarget = AllowControls }, }, play = new IconButton { @@ -100,7 +97,6 @@ namespace osu.Game.Screens.Play.PlayerSettings gameplayClock.Start(); } }, - Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -108,7 +104,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, Action = () => seek(1, seek_amount), - Enabled = { BindTarget = AllowControls }, }, new SeekButton { @@ -116,7 +111,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastForward, Action = () => seek(1, seek_fast_amount), - Enabled = { BindTarget = AllowControls }, }, }, }, diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ca71a89b48..788eb75283 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -15,6 +15,7 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Users; @@ -49,6 +50,17 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + protected override void LoadComplete() + { + base.LoadComplete(); + + var playerSettingsOverlay = new PlaybackSettings { Expanded = { Value = false } }; + HUDOverlay.PlayerSettingsOverlay.Add(playerSettingsOverlay); + + if (GameplayClockContainer is MasterGameplayClockContainer master) + playerSettingsOverlay.UserPlaybackRate.BindTarget = master.UserPlaybackRate; + } + protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index dd7418d563..2faead0ee1 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -68,8 +68,6 @@ namespace osu.Game.Screens.Play master.UserPlaybackRate.Value = 1; } }, true); - - HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.AllowControls.Value = false; } /// From c545a9c242d56a499ddd6be46e69fd0730eed97e Mon Sep 17 00:00:00 2001 From: Nitrous Date: Fri, 12 Jan 2024 15:13:38 +0800 Subject: [PATCH 313/539] remove extra new line --- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 0c9f5bb6ee..0fe3a08985 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - private readonly PlayerSliderBar rateSlider; private readonly OsuSpriteText multiplierText; From ccbba8a00b7bcb7404cb31333742eafd21a5e62b Mon Sep 17 00:00:00 2001 From: Nitrous Date: Fri, 12 Jan 2024 17:19:59 +0800 Subject: [PATCH 314/539] Avoid NRE due to a beatmap loading with no hit objects. --- osu.Game/Screens/Play/ReplayPlayer.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 788eb75283..f6e4ac489a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -54,11 +54,14 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - var playerSettingsOverlay = new PlaybackSettings { Expanded = { Value = false } }; - HUDOverlay.PlayerSettingsOverlay.Add(playerSettingsOverlay); + if (HUDOverlay != null) + { + var playerSettingsOverlay = new PlaybackSettings { Expanded = { Value = false } }; + HUDOverlay.PlayerSettingsOverlay.Add(playerSettingsOverlay); - if (GameplayClockContainer is MasterGameplayClockContainer master) - playerSettingsOverlay.UserPlaybackRate.BindTarget = master.UserPlaybackRate; + if (GameplayClockContainer is MasterGameplayClockContainer master) + playerSettingsOverlay.UserPlaybackRate.BindTarget = master.UserPlaybackRate; + } } protected override void PrepareReplay() From 8d4ba6d4664ead9e095b1b5145b92b2dc93ebb81 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 12 Jan 2024 15:30:16 +0300 Subject: [PATCH 315/539] Remove PathPadding property --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 6 +-- .../ArgonHealthDisplayBackground.cs | 49 +------------------ .../ArgonHealthDisplayBar.cs | 23 +-------- 3 files changed, 5 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 1f633c9e82..0af514a4f9 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -84,9 +84,7 @@ namespace osu.Game.Screens.Play.HUD { new ArgonHealthDisplayBackground { - RelativeSizeAxes = Axes.Both, - PathRadius = MAIN_PATH_RADIUS, - PathPadding = MAIN_PATH_RADIUS + RelativeSizeAxes = Axes.Both }, new Container { @@ -100,7 +98,6 @@ namespace osu.Game.Screens.Play.HUD Blending = BlendingParameters.Additive, Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), PathRadius = 40f, - PathPadding = 40f, GlowPortion = 0.9f, } }, @@ -111,7 +108,6 @@ namespace osu.Game.Screens.Play.HUD BarColour = main_bar_colour, GlowColour = main_bar_glow_colour, PathRadius = MAIN_PATH_RADIUS, - PathPadding = MAIN_PATH_RADIUS, GlowPortion = 0.6f, } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs index a98b3dc1f3..a96c2f97bd 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -17,36 +17,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts { public partial class ArgonHealthDisplayBackground : Box { - private float radius = 10f; - - public float PathRadius - { - get => radius; - set - { - if (radius == value) - return; - - radius = value; - Invalidate(Invalidation.DrawNode); - } - } - - private float padding = 10f; - - public float PathPadding - { - get => padding; - set - { - if (padding == value) - return; - - padding = value; - Invalidate(Invalidation.DrawNode); - } - } - [BackgroundDependencyLoader] private void load(ShaderManager shaders) { @@ -65,24 +35,12 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } private Vector2 size; - private float pathRadius; - private float padding; public override void ApplyState() { base.ApplyState(); size = Source.DrawSize; - pathRadius = Source.PathRadius; - padding = Source.PathPadding; - } - - protected override void Draw(IRenderer renderer) - { - if (pathRadius == 0) - return; - - base.Draw(renderer); } private IUniformBuffer parametersBuffer; @@ -94,9 +52,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts parametersBuffer ??= renderer.CreateUniformBuffer(); parametersBuffer.Data = new ArgonBarPathBackgroundParameters { - Size = size, - PathRadius = pathRadius, - Padding = padding + Size = size }; shader.BindUniformBlock("m_ArgonBarPathBackgroundParameters", parametersBuffer); @@ -114,8 +70,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts private record struct ArgonBarPathBackgroundParameters { public UniformVector2 Size; - public UniformFloat PathRadius; - public UniformFloat Padding; + private readonly UniformPadding8 pad; } } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs index 4f4af02d66..1938b97d5a 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -50,21 +50,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts } } - private float padding = 10f; - - public float PathPadding - { - get => padding; - set - { - if (padding == value) - return; - - padding = value; - Invalidate(Invalidation.DrawNode); - } - } - private float glowPortion; public float GlowPortion @@ -130,7 +115,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts private Vector2 size; private Vector2 progressRange; private float pathRadius; - private float padding; private float glowPortion; private Color4 barColour; private Color4 glowColour; @@ -142,7 +126,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts size = Source.DrawSize; progressRange = new Vector2(Math.Min(Source.progressRange.X, Source.progressRange.Y), Source.progressRange.Y); pathRadius = Source.PathRadius; - padding = Source.PathPadding; glowPortion = Source.GlowPortion; barColour = Source.barColour; glowColour = Source.glowColour; @@ -170,8 +153,7 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts GlowPortion = glowPortion, Size = size, ProgressRange = progressRange, - PathRadius = pathRadius, - Padding = padding + PathRadius = pathRadius }; shader.BindUniformBlock("m_ArgonBarPathParameters", parametersBuffer); @@ -193,9 +175,8 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts public UniformVector2 Size; public UniformVector2 ProgressRange; public UniformFloat PathRadius; - public UniformFloat Padding; public UniformFloat GlowPortion; - private readonly UniformPadding4 pad; + private readonly UniformPadding8 pad; } } } From c1e4e51a5fe02cdff146cd9a45d85ba5c5847992 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 12 Jan 2024 15:34:02 +0300 Subject: [PATCH 316/539] Add comment explaining negative container padding --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 0af514a4f9..dcc8f4feec 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -89,6 +89,7 @@ namespace osu.Game.Screens.Play.HUD new Container { RelativeSizeAxes = Axes.Both, + // since we are using bigger path radius we need to expand the draw area outwards to preserve the curve placement Padding = new MarginPadding(-30f), Child = glowBar = new ArgonHealthDisplayBar { From 6572fa4378811663971527a80ea86b9db1c94103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 12 Jan 2024 14:59:15 +0100 Subject: [PATCH 317/539] Only validate playback rate when in submission context Temporary workaround for https://github.com/ppy/osu/issues/26404. It appears that some audio files do not behave well with BASS, leading BASS to report a contradictory state of affairs (i.e. a track that is in playing state but also not progressing). This appears to be related to seeking specifically, therefore only enable the validation of playback rate in the most sensitive contexts, namely when any sort of score submission is involved. --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 6 ------ osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/SubmittingPlayer.cs | 5 +++++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 96cfcd61c3..f75a2656ef 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -21,7 +21,6 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; -using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -360,11 +359,6 @@ namespace osu.Game.Tests.Visual.Gameplay AllowImportCompletion = new SemaphoreSlim(1); } - protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) - { - ShouldValidatePlaybackRate = false, - }; - protected override async Task ImportScore(Score score) { ScoreImportStarted = true; diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 8b8bf87436..0d60ec4713 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play /// Whether the audio playback rate should be validated. /// Mostly disabled for tests. /// - internal bool ShouldValidatePlaybackRate { get; init; } = true; + internal bool ShouldValidatePlaybackRate { get; init; } /// /// Whether the audio playback is within acceptable ranges. diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index fb3481cbc4..04abb6162f 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -61,6 +61,11 @@ namespace osu.Game.Screens.Play AddInternal(new PlayerTouchInputDetector()); } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = true, + }; + protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); From 58ade18c06524365545aa9adb2a3264e8ad73f80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jan 2024 04:49:50 +0900 Subject: [PATCH 318/539] Update framework --- osu.Android.props | 2 +- .../Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 56931bbcb4..e39143ab93 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index c180baeab7..9afeaf7338 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 882f49039029b7dc3e287ccc302d04de89de10df Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 01:32:37 +0100 Subject: [PATCH 319/539] lazy load slider tail position --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++------ osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs | 8 ++++---- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 8 ++++++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 506145568e..2aca83ad03 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects set { repeatCount = value; - updateNestedPositions(); + endPositionCache.Invalidate(); } } @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Slider() { SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples(); - Path.Version.ValueChanged += _ => updateNestedPositions(); + Path.Version.ValueChanged += _ => endPositionCache.Invalidate(); } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) @@ -218,7 +218,6 @@ namespace osu.Game.Rulesets.Osu.Objects { RepeatIndex = e.SpanIndex, StartTime = e.Time, - Position = EndPosition, StackHeight = StackHeight, ClassicSliderBehaviour = ClassicSliderBehaviour, }); @@ -245,9 +244,6 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Position = Position; - - if (TailCircle != null) - TailCircle.Position = EndPosition; } protected void UpdateNestedSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index 88a34fcb8f..2d5a5b7727 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -14,16 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects /// public abstract class SliderEndCircle : HitCircle { - private readonly Slider slider; + protected readonly Slider Slider; protected SliderEndCircle(Slider slider) { - this.slider = slider; + Slider = slider; } public int RepeatIndex { get; set; } - public double SpanDuration => slider.SpanDuration; + public double SpanDuration => Slider.SpanDuration; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects else { // The first end circle should fade in with the slider. - TimePreempt += StartTime - slider.StartTime; + TimePreempt += StartTime - Slider.StartTime; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index ceee513412..abe2c7074b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,9 +1,11 @@ // 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -15,6 +17,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// public bool ClassicSliderBehaviour; + public override Vector2 Position + { + get => Slider.EndPosition; + set => throw new NotImplementedException(); + } + public SliderTailCircle(Slider slider) : base(slider) { From 5fa7f6ec53a4430b1db13f74e75e2e56b4a181fc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 02:21:32 +0100 Subject: [PATCH 320/539] make drawables that update from path version update once per frame --- .../PathControlPointConnectionPiece.cs | 16 ++++++++++++++-- .../Skinning/Default/PlaySliderBody.cs | 6 ++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 67685d21a7..004a091cae 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -51,14 +51,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components base.LoadComplete(); hitObjectPosition = hitObject.PositionBindable.GetBoundCopy(); - hitObjectPosition.BindValueChanged(_ => updateConnectingPath()); + hitObjectPosition.BindValueChanged(_ => pathRequiresUpdate = true); pathVersion = hitObject.Path.Version.GetBoundCopy(); - pathVersion.BindValueChanged(_ => updateConnectingPath()); + pathVersion.BindValueChanged(_ => pathRequiresUpdate = true); updateConnectingPath(); } + private bool pathRequiresUpdate; + + protected override void Update() + { + base.Update(); + + if (!pathRequiresUpdate) return; + + updateConnectingPath(); + pathRequiresUpdate = false; + } + /// /// Updates the path connecting this control point to the next one. /// diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index aa507cbaf0..cbc62a1c7c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected IBindable AccentColourBindable { get; private set; } = null!; - private IBindable pathVersion = null!; - [Resolved(CanBeNull = true)] private OsuRulesetConfigManager? config { get; set; } @@ -33,8 +31,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy(); ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); - pathVersion = drawableSlider.PathVersion.GetBoundCopy(); - pathVersion.BindValueChanged(_ => Refresh()); + drawableObject.DefaultsApplied += _ => Refresh(); + drawableObject.HitObjectApplied += _ => Refresh(); AccentColourBindable = drawableObject.AccentColour.GetBoundCopy(); AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true); From e1186080b87436d78a591bb888ff8bfa93559dbf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 02:24:33 +0100 Subject: [PATCH 321/539] simplify scheduling logic --- .../PathControlPointConnectionPiece.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 004a091cae..7e7d653dbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -51,26 +51,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components base.LoadComplete(); hitObjectPosition = hitObject.PositionBindable.GetBoundCopy(); - hitObjectPosition.BindValueChanged(_ => pathRequiresUpdate = true); + hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); pathVersion = hitObject.Path.Version.GetBoundCopy(); - pathVersion.BindValueChanged(_ => pathRequiresUpdate = true); + pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); updateConnectingPath(); } - private bool pathRequiresUpdate; - - protected override void Update() - { - base.Update(); - - if (!pathRequiresUpdate) return; - - updateConnectingPath(); - pathRequiresUpdate = false; - } - /// /// Updates the path connecting this control point to the next one. /// From 0934cff501bd580119e386bbc09e1073a12fb272 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jan 2024 05:22:41 +0900 Subject: [PATCH 322/539] Workaround implementation oversight See https://github.com/ppy/osu-framework/pull/6130. --- .../Settings/Sections/Input/KeyBindingRow_KeyButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs index 3493baa2ad..adf05a71b9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs @@ -140,7 +140,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// A generated from the full input state. /// The key which triggered this update, and should be used as the binding. public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => - UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).ToArray())); + // TODO: Distinct() can be removed after https://github.com/ppy/osu-framework/pull/6130 is merged. + UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).Distinct().ToArray())); public void UpdateKeyCombination(KeyCombination newCombination) { From 5303023e574c9e32239bd5afe4bc311241e07faa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 09:41:53 +0300 Subject: [PATCH 323/539] Add failing test case and fix selection assertion --- .../TestSceneFreeModSelectOverlay.cs | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index f1674401cd..a4feffddfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -10,12 +10,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; +using osu.Game.Utils; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer @@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene { private FreeModSelectOverlay freeModSelectOverlay; + private FooterButtonFreeMods footerButtonFreeMods; private readonly Bindable>> availableMods = new Bindable>>(); [BackgroundDependencyLoader] @@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestSelectAllViaFooterButtonThenDeselectFromOverlay() + { + createFreeModSelect(); + + AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType().Single().Enabled.Value); + AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); + + AddStep("click footer select all button", () => + { + InputManager.MoveMouseTo(footerButtonFreeMods); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("all mods selected", assertAllAvailableModsSelected); + AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "all")); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any()); + AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); + } + private void createFreeModSelect() { - AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay + AddStep("create free mod select screen", () => Children = new Drawable[] { - State = { Value = Visibility.Visible } + freeModSelectOverlay = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }, + footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + }, }); AddUntilStep("all column content loaded", () => freeModSelectOverlay.ChildrenOfType().Any() @@ -134,10 +172,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { var allAvailableMods = availableMods.Value .Where(pair => pair.Key != ModType.System) - .SelectMany(pair => pair.Value) + .SelectMany(pair => ModUtils.FlattenMods(pair.Value)) .Where(mod => mod.UserPlayable && mod.HasImplementation) .ToList(); + if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count) + return false; + foreach (var availableMod in allAvailableMods) { if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType())) From c476843a837882bd6d3eb7164a20203a9e6d1774 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 09:43:20 +0300 Subject: [PATCH 324/539] Mark system mods as invalid for selection in mod select overlay --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index baa7e594c1..f9875c5547 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -447,7 +447,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { foreach (var modState in AllAvailableMods) - modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); + modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } private void updateMultiplier() From aebf246f627b8658e94654d903c8d05bd23a190d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 09:43:50 +0300 Subject: [PATCH 325/539] Change select all mod buttons to check `ValidForSelection` instead of directly checking system mods --- osu.Game/Overlays/Mods/SelectAllModsButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index b6b3051a0d..1da762d164 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Mods private void updateEnabledState() { Enabled.Value = availableMods.Value - .Where(pair => pair.Key != ModType.System) .SelectMany(pair => pair.Value) + .Where(modState => modState.ValidForSelection.Value) .Any(modState => !modState.Active.Value && modState.Visible); } } From 4cde8685d340006824edf7a17ca2a67323c98279 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 11:16:22 +0300 Subject: [PATCH 326/539] Add failing test case --- .../Multiplayer/TestSceneMultiplayer.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 462286335a..201fac37c8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -29,7 +29,9 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -1009,6 +1011,54 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + [Test] + public void TestGameplayStartsWhileInSongSelectWithDifferentRuleset() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + QueueMode = { Value = QueueMode.AllPlayers }, + Playlist = + { + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + } + }); + + AddStep("join other user and make host", () => + { + multiplayerClient.AddUser(new APIUser { Id = 1234 }); + multiplayerClient.TransferHost(1234); + }); + + AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } })); + AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready)); + AddStep("press edit on second item", () => this.ChildrenOfType().Single(i => i.Item.RulesetID == 1) + .ChildrenOfType().Single().TriggerClick()); + + AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); + AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1); + + AddStep("start match by other user", () => + { + multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); + multiplayerClient.StartMatch().WaitSafely(); + }); + + AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing); + AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden))); + } + private void enterGameplay() { pressReadyButton(); From c514550dfa0d3521b74d43aa2e0054a8b4325bdf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 11:17:08 +0300 Subject: [PATCH 327/539] Fix multiplayer potentially selecting mods of wrong ruleset when starting match --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2cd8e45d28..f35b205bc4 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private BeatmapManager beatmapManager { get; set; } [Resolved] - private RulesetStore rulesets { get; set; } + protected RulesetStore Rulesets { get; private set; } [Resolved] private IAPIProvider api { get; set; } = null!; @@ -422,7 +422,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selected == null) return; - var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)); @@ -463,7 +463,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } @@ -473,7 +473,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Ruleset.Value = rulesets.GetRuleset(SelectedItem.Value.RulesetID); + Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 56256bb15c..8a85a46a2f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -241,8 +241,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList(); + var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } [Resolved(canBeNull: true)] From fca9b1f53630831c5ea3d56e09148c81b7ca9c57 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 12:50:39 +0100 Subject: [PATCH 328/539] Fix so it reacts to PathVersion with Scheduler --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 ++++++ osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 4099d47d61..2f012e33da 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -244,7 +244,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables else if (slidingSample.IsPlaying) slidingSample.Stop(); } + } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // It's important that this is done in UpdateAfterChildren so that the SliderBody has a chance to update its Size and PathOffset on Update. double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); Ball.UpdateProgress(completionProgress); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index cbc62a1c7c..fb31f88d3c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected IBindable AccentColourBindable { get; private set; } = null!; + private IBindable pathVersion = null!; + [Resolved(CanBeNull = true)] private OsuRulesetConfigManager? config { get; set; } @@ -31,8 +33,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy(); ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); - drawableObject.DefaultsApplied += _ => Refresh(); - drawableObject.HitObjectApplied += _ => Refresh(); + pathVersion = drawableSlider.PathVersion.GetBoundCopy(); + pathVersion.BindValueChanged(_ => Scheduler.AddOnce(Refresh)); AccentColourBindable = drawableObject.AccentColour.GetBoundCopy(); AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true); From ce643aa68f35369be1a975bb1ceb69fb54192cf2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 13:54:04 +0100 Subject: [PATCH 329/539] revert overwriting Position getter in SliderTailCircle It would have very weird implications when combined with the position bindable which would be all wrong and stuff --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++++ osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 8 -------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 2aca83ad03..032f105ded 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -218,6 +218,7 @@ namespace osu.Game.Rulesets.Osu.Objects { RepeatIndex = e.SpanIndex, StartTime = e.Time, + Position = EndPosition, StackHeight = StackHeight, ClassicSliderBehaviour = ClassicSliderBehaviour, }); @@ -244,6 +245,9 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Position = Position; + + if (TailCircle != null) + TailCircle.Position = EndPosition; } protected void UpdateNestedSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index abe2c7074b..ceee513412 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,11 +1,9 @@ // 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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -17,12 +15,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public bool ClassicSliderBehaviour; - public override Vector2 Position - { - get => Slider.EndPosition; - set => throw new NotImplementedException(); - } - public SliderTailCircle(Slider slider) : base(slider) { From b1fae2bc6a766a6301b8ebdabd3573697ef3020f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 16:24:48 +0300 Subject: [PATCH 330/539] Add failing test case --- .../TestScenePlayerScoreSubmission.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index f75a2656ef..833cb24f41 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -15,11 +15,13 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -34,12 +36,19 @@ namespace osu.Game.Tests.Visual.Gameplay private Func createCustomBeatmap; private Func createCustomRuleset; + private Func createCustomMods; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; protected override bool HasCustomSteps => true; - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false); + protected override TestPlayer CreatePlayer(Ruleset ruleset) + { + if (createCustomMods != null) + SelectedMods.Value = SelectedMods.Value.Concat(createCustomMods()).ToList(); + + return new FakeImportingPlayer(false); + } protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player; @@ -277,13 +286,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } - private void createPlayerTest(bool allowFail = false, Func createBeatmap = null, Func createRuleset = null) + [Test] + public void TestNoSubmissionWithModsOfDifferentRuleset() + { + prepareTestAPI(true); + + createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() }); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + + private void createPlayerTest(bool allowFail = false, Func createBeatmap = null, Func createRuleset = null, Func createMods = null) { CreateTest(() => AddStep("set up requirements", () => { this.allowFail = allowFail; createCustomBeatmap = createBeatmap; createCustomRuleset = createRuleset; + createCustomMods = createMods; })); } From 0c02062780312b56dd2f79301e097b1b21de146a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jan 2024 15:52:57 +0300 Subject: [PATCH 331/539] Add guard against starting gameplay with invalid mod instances Move guard to `Player` instead --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b87306b9a2..ffd585148b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -213,6 +213,12 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; + if (gameplayMods.Any(m => ruleset.AllMods.All(rulesetMod => m.GetType() != rulesetMod.GetType()))) + { + Logger.Log($@"Gameplay was started with a mod belonging to a ruleset different than '{ruleset.Description}'.", level: LogLevel.Important); + return; + } + mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) From f5d6d52d4c9160affc348e4dd81cbb9a1f5ed352 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 14:47:40 +0100 Subject: [PATCH 332/539] Move logic for caching segments and updating path types to PathControlPointVisualiser --- .../Components/PathControlPointPiece.cs | 42 ---------------- .../Components/PathControlPointVisualiser.cs | 48 +++++++++++++++++++ 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 03792d8520..9b40b39a9d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -5,19 +5,15 @@ using System; using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -56,27 +52,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private IBindable hitObjectPosition; private IBindable hitObjectScale; - [UsedImplicitly] - private readonly IBindable hitObjectVersion; - public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) { this.hitObject = hitObject; ControlPoint = controlPoint; - // we don't want to run the path type update on construction as it may inadvertently change the hit object. - cachePoints(hitObject); - - hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); - - // schedule ensure that updates are only applied after all operations from a single frame are applied. - // this avoids inadvertently changing the hit object path type for batch operations. - hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() => - { - cachePoints(hitObject); - updatePathType(); - })); - controlPoint.Changed += updateMarkerDisplay; Origin = Anchor.Centre; @@ -214,28 +194,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke(); - private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint); - - /// - /// Handles correction of invalid path types. - /// - private void updatePathType() - { - if (ControlPoint.Type != PathType.PERFECT_CURVE) - return; - - if (PointsInSegment.Count > 3) - ControlPoint.Type = PathType.BEZIER; - - if (PointsInSegment.Count != 3) - return; - - ReadOnlySpan points = PointsInSegment.Select(p => p.Position).ToArray(); - RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); - if (boundingBox.Width >= 640 || boundingBox.Height >= 480) - ControlPoint.Type = PathType.BEZIER; - } - /// /// Updates the state of the circular control point marker. /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 24e2210b45..aae6275d45 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,15 +9,18 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -52,6 +55,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } + [UsedImplicitly] + private readonly IBindable hitObjectVersion; + public PathControlPointVisualiser(T hitObject, bool allowSelection) { this.hitObject = hitObject; @@ -64,6 +70,48 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Connections = new Container> { RelativeSizeAxes = Axes.Both }, Pieces = new Container> { RelativeSizeAxes = Axes.Both } }; + + hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); + + // schedule ensure that updates are only applied after all operations from a single frame are applied. + // this avoids inadvertently changing the hit object path type for batch operations. + hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(cachePointsAndEnsureValidPathTypes)); + } + + /// + /// Caches the PointsInSegment of Pieces and handles correction of invalid path types. + /// + private void cachePointsAndEnsureValidPathTypes() + { + List pointsInCurrentSegment = new List(); + + foreach (var controlPoint in controlPoints) + { + if (controlPoint.Type != null) + pointsInCurrentSegment = new List(); + + pointsInCurrentSegment.Add(controlPoint); + + // Pieces might not be ordered so we need to find the piece corresponding to the current control point. + Pieces.Single(o => o.ControlPoint == controlPoint).PointsInSegment = pointsInCurrentSegment; + } + + foreach (var piece in Pieces) + { + if (piece.ControlPoint.Type != PathType.PERFECT_CURVE) + return; + + if (piece.PointsInSegment.Count > 3) + piece.ControlPoint.Type = PathType.BEZIER; + + if (piece.PointsInSegment.Count != 3) + return; + + ReadOnlySpan points = piece.PointsInSegment.Select(p => p.Position).ToArray(); + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + piece.ControlPoint.Type = PathType.BEZIER; + } } protected override void LoadComplete() From b4f9878b4656e83da770fcd00f1b70dd109e4a84 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 20:39:49 +0100 Subject: [PATCH 333/539] working jank solution --- .../Components/PathControlPointVisualiser.cs | 36 ++++++++++++------- osu.Game/Rulesets/Objects/SliderPath.cs | 4 +++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index aae6275d45..a02a07f2b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -72,10 +72,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components }; hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + + controlPoints.CollectionChanged += onControlPointsChanged; + controlPoints.BindTo(hitObject.Path.ControlPoints); // schedule ensure that updates are only applied after all operations from a single frame are applied. // this avoids inadvertently changing the hit object path type for batch operations. - hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(cachePointsAndEnsureValidPathTypes)); + hitObject.Path.Validating += cachePointsAndEnsureValidPathTypes; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + hitObject.Path.Validating -= cachePointsAndEnsureValidPathTypes; } /// @@ -88,7 +105,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components foreach (var controlPoint in controlPoints) { if (controlPoint.Type != null) + { + pointsInCurrentSegment.Add(controlPoint); pointsInCurrentSegment = new List(); + } pointsInCurrentSegment.Add(controlPoint); @@ -99,13 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components foreach (var piece in Pieces) { if (piece.ControlPoint.Type != PathType.PERFECT_CURVE) - return; + continue; if (piece.PointsInSegment.Count > 3) piece.ControlPoint.Type = PathType.BEZIER; if (piece.PointsInSegment.Count != 3) - return; + continue; ReadOnlySpan points = piece.PointsInSegment.Select(p => p.Position).ToArray(); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); @@ -114,16 +134,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - - controlPoints.CollectionChanged += onControlPointsChanged; - controlPoints.BindTo(hitObject.Path.ControlPoints); - } - /// /// Selects the corresponding to the given , /// and deselects all other s. diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index dc71608132..bafc62ceda 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects private readonly Bindable version = new Bindable(); + public event Action? Validating; + /// /// The user-set distance of the path. If non-null, will match this value, /// and the path will be shortened/lengthened to match this length. @@ -233,6 +235,8 @@ namespace osu.Game.Rulesets.Objects if (pathCache.IsValid) return; + Validating?.Invoke(); + calculatePath(); calculateLength(); From da4d83f8ca2c7ccf5ff651c80df656393b817b7f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 20:54:44 +0100 Subject: [PATCH 334/539] remove the need for caching points in segment --- .../Components/PathControlPointPiece.cs | 2 - .../Components/PathControlPointVisualiser.cs | 57 +++++++++---------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 9b40b39a9d..8ddc38c38e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action DragInProgress; public Action DragEnded; - public List PointsInSegment; - public readonly BindableBool IsSelected = new BindableBool(); public readonly PathControlPoint ControlPoint; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index a02a07f2b6..d057565c2b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,7 +9,6 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -55,9 +54,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } - [UsedImplicitly] - private readonly IBindable hitObjectVersion; - public PathControlPointVisualiser(T hitObject, bool allowSelection) { this.hitObject = hitObject; @@ -70,8 +66,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Connections = new Container> { RelativeSizeAxes = Axes.Both }, Pieces = new Container> { RelativeSizeAxes = Axes.Both } }; - - hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); } protected override void LoadComplete() @@ -85,20 +79,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // schedule ensure that updates are only applied after all operations from a single frame are applied. // this avoids inadvertently changing the hit object path type for batch operations. - hitObject.Path.Validating += cachePointsAndEnsureValidPathTypes; + hitObject.Path.Validating += ensureValidPathTypes; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - hitObject.Path.Validating -= cachePointsAndEnsureValidPathTypes; + hitObject.Path.Validating -= ensureValidPathTypes; } /// - /// Caches the PointsInSegment of Pieces and handles correction of invalid path types. + /// Handles correction of invalid path types. /// - private void cachePointsAndEnsureValidPathTypes() + private void ensureValidPathTypes() { List pointsInCurrentSegment = new List(); @@ -107,31 +101,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (controlPoint.Type != null) { pointsInCurrentSegment.Add(controlPoint); - pointsInCurrentSegment = new List(); + ensureValidPathType(pointsInCurrentSegment); + pointsInCurrentSegment.Clear(); } pointsInCurrentSegment.Add(controlPoint); - - // Pieces might not be ordered so we need to find the piece corresponding to the current control point. - Pieces.Single(o => o.ControlPoint == controlPoint).PointsInSegment = pointsInCurrentSegment; } - foreach (var piece in Pieces) - { - if (piece.ControlPoint.Type != PathType.PERFECT_CURVE) - continue; + ensureValidPathType(pointsInCurrentSegment); + } - if (piece.PointsInSegment.Count > 3) - piece.ControlPoint.Type = PathType.BEZIER; + private void ensureValidPathType(IReadOnlyList segment) + { + var first = segment[0]; - if (piece.PointsInSegment.Count != 3) - continue; + if (first.Type != PathType.PERFECT_CURVE) + return; - ReadOnlySpan points = piece.PointsInSegment.Select(p => p.Position).ToArray(); - RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); - if (boundingBox.Width >= 640 || boundingBox.Height >= 480) - piece.ControlPoint.Type = PathType.BEZIER; - } + if (segment.Count > 3) + first.Type = PathType.BEZIER; + + if (segment.Count != 3) + return; + + ReadOnlySpan points = segment.Select(p => p.Position).ToArray(); + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + first.Type = PathType.BEZIER; } /// @@ -298,7 +294,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// The path type we want to assign to the given control point piece. private void updatePathType(PathControlPointPiece piece, PathType? type) { - int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); + var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint); + int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint); if (type?.Type == SplineType.PerfectCurve) { @@ -307,8 +304,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // and one segment of the previous type. int thirdPointIndex = indexInSegment + 2; - if (piece.PointsInSegment.Count > thirdPointIndex + 1) - piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type; + if (pointsInSegment.Count > thirdPointIndex + 1) + pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; } hitObject.Path.ExpectedDistance.Value = null; From 1e3c332658a320fb2dbf49e4a97e2927cafc4dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jan 2024 21:39:32 +0100 Subject: [PATCH 335/539] Fix broken installer Closes https://github.com/ppy/osu/issues/26510. Time for a rant. Technically, this "broke" with 9e8d07d3144bd4b072d28bd9bd0e255fee410de0, but it is actually an end result of upstream behaviours that I am failing to find a better description for than "utterly broken". Squirrel (the installer we use) has unit tests. Which is great, power to them. However, the method in which that testing is implemented leads to epic levels of WTF breakage. To determine whether Squirrel is being tested right now, it is checking all currently loaded assemblies, and determining that if any loaded assembly contains the magic string of "NUNIT" - among others - it must be being tested right now: https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 If one assumes that there is no conceivable way that an NUnit assembly *may* be loaded *without* it being a test context, this *may* seem sane. Foreshadowing. (Now, to avoid being hypocritical, we also do this, *but* we do this by checking if the *entry* assembly is an NUnit: https://github.com/ppy/osu-framework/blob/92db55a52742047b42c0b4b25327ce28bd991b44/osu.Framework/Development/DebugUtils.cs#L16-L34 which seems *much* saner, no?) Now, why did this break with 9e8d07d3144bd4b072d28bd9bd0e255fee410de0 *specifically*, you might wonder? Well the reason is this line: https://github.com/ppy/osu/blob/3d3f58c2523f519504d9156a684538e2aa0c094c/osu.Desktop/NVAPI.cs#L183 Yes you are reading this correctly, it's not NVAPI anything itself that breaks this, it is *a log statement*. To be precise, what the log statement *does* to provoke this, is calling into framework. That causes the framework assembly to load, *which* transitively loads the `nunit.framework` assembly. (If you ever find yourself wanting to find out this sort of cursed knowledge - I hope you never need to - you can run something along the lines of dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:4 -- .\osu!.exe then open the resulting trace in PerfView, and then search the `Microsoft-Windows-DotNETRuntime/AssemblyLoader/Start` log for the cursed assembly. In this case, the relevant entry said something along the lines of HasStack="True" ThreadID="23,924" ProcessorNumber="0" ClrInstanceID="6" AssemblyName="nunit.framework, Version=3.13.3.0, Culture=neutral, PublicKeyToken=2638cd05610744eb" AssemblyPath="" RequestingAssembly="osu.Framework, Version=2024.113.0.0, Culture=neutral, PublicKeyToken=null" AssemblyLoadContext="Default" RequestingAssemblyLoadContext="Default" ActivityID="/#21032/1/26/" Either that or just comment the log line for kicks. But the above is *much* faster.) Now, what *happens* if Squirrel "detects" that it is being "tested"? Well, it will refuse to close after executing the "hooks" defined via `SquirrelAwareApp`: https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SquirrelAwareApp.cs#L85-L88 and it will also refuse to create version shortcuts: https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/UpdateManager.Shortcuts.cs#L63-L65 Sounds familiar, don't it? There are days on which I tire of computers. Today is one of them. --- osu.Desktop/Program.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index a652e31f62..6b95a82703 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,12 +30,19 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - // NVIDIA profiles are based on the executable name of a process. - // Lazer and stable share the same executable name. - // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. - NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; - - // run Squirrel first, as the app may exit after these run + /* + * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK! + * + * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it. + * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit, + * namely by checking loaded assemblies: + * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 + * + * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded - + * the app will then do completely broken things like: + * - not creating system shortcuts (as the logic is if'd out if "running tests") + * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests") + */ if (OperatingSystem.IsWindows()) { var windowsVersion = Environment.OSVersion.Version; @@ -59,6 +66,11 @@ namespace osu.Desktop setupSquirrel(); } + // NVIDIA profiles are based on the executable name of a process. + // Lazer and stable share the same executable name. + // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. + NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; From 39908f5425d99d04e4c09b833c170193f3715205 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 22:39:09 +0100 Subject: [PATCH 336/539] remove Validating event and instead call validation explicitly on edits --- .../Components/PathControlPointVisualiser.cs | 20 ++++++++----------- .../Sliders/SliderPlacementBlueprint.cs | 2 ++ .../Sliders/SliderSelectionBlueprint.cs | 4 ++++ osu.Game/Rulesets/Objects/SliderPath.cs | 4 ---- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index d057565c2b..b2d1709531 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -76,23 +76,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components controlPoints.CollectionChanged += onControlPointsChanged; controlPoints.BindTo(hitObject.Path.ControlPoints); - - // schedule ensure that updates are only applied after all operations from a single frame are applied. - // this avoids inadvertently changing the hit object path type for batch operations. - hitObject.Path.Validating += ensureValidPathTypes; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - hitObject.Path.Validating -= ensureValidPathTypes; } /// /// Handles correction of invalid path types. /// - private void ensureValidPathTypes() + public void EnsureValidPathTypes() { List pointsInCurrentSegment = new List(); @@ -113,6 +102,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void ensureValidPathType(IReadOnlyList segment) { + if (segment.Count == 0) + return; + var first = segment[0]; if (first.Type != PathType.PERFECT_CURVE) @@ -394,6 +386,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // Maintain the path types in case they got defaulted to bezier at some point during the drag. for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; + + EnsureValidPathTypes(); } public void DragEnded() => changeHandler?.EndChange(); @@ -467,6 +461,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); + + EnsureValidPathTypes(); }); if (countOfState == totalCount) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 28e972bacd..75ed818693 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -316,6 +316,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { + controlPointVisualiser.EnsureValidPathTypes(); + if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index b3efe1c495..3575e15d1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -254,6 +254,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); + ControlPointVisualiser?.EnsureValidPathTypes(); + HitObject.SnapTo(distanceSnapProvider); return pathControlPoint; @@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders controlPoints.Remove(c); } + ControlPointVisualiser?.EnsureValidPathTypes(); + // Snap the slider to the current beat divisor before checking length validity. HitObject.SnapTo(distanceSnapProvider); diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index bafc62ceda..dc71608132 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Objects private readonly Bindable version = new Bindable(); - public event Action? Validating; - /// /// The user-set distance of the path. If non-null, will match this value, /// and the path will be shortened/lengthened to match this length. @@ -235,8 +233,6 @@ namespace osu.Game.Rulesets.Objects if (pathCache.IsValid) return; - Validating?.Invoke(); - calculatePath(); calculateLength(); From 83e108071a123650991fded528f64ad80421d155 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 22:51:33 +0100 Subject: [PATCH 337/539] fix wrong path type being displayed --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 75ed818693..0fa84c91fc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders segmentStart.Type = PathType.BEZIER; break; } + + controlPointVisualiser.EnsureValidPathTypes(); } private void updateCursor() @@ -316,8 +318,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - controlPointVisualiser.EnsureValidPathTypes(); - if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else From 243b7b6fdad6aa28df1df8a06ecf574ebeff1238 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 13 Jan 2024 23:17:38 +0100 Subject: [PATCH 338/539] fix code quality --- .../Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 8ddc38c38e..e741d67e3b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; From 68496f7a0e865b3d1d74415d482e78a4747bd454 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Jan 2024 15:13:20 +0900 Subject: [PATCH 339/539] Fix scores not showing up on leaderboards during gameplay --- osu.Game/Screens/Select/PlaySongSelect.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 4951504ff5..7b7b8857f3 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -146,14 +146,6 @@ namespace osu.Game.Screens.Select } } - public override void OnSuspending(ScreenTransitionEvent e) - { - // Scores will be refreshed on arriving at this screen. - // Clear them to avoid animation overload on returning to song select. - playBeatmapDetailArea.Leaderboard.ClearScores(); - base.OnSuspending(e); - } - public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); From b7d74fda88b98cd8b78df7ae5e907cc8b8fb9c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 09:10:39 +0100 Subject: [PATCH 340/539] Revert "Keep editor in frame stable mode when possible" --- .../TestSceneEditorAutoplayFastStreams.cs | 51 ------------------- .../Editor/TestSceneOsuComposerSelection.cs | 2 - .../Edit/DrawableEditorRulesetWrapper.cs | 21 +------- 3 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs deleted file mode 100644 index cf5cd809ef..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneEditorAutoplayFastStreams.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Beatmaps; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Osu.Tests.Editor -{ - /// - /// This test covers autoplay working correctly in the editor on fast streams. - /// Might seem like a weird test, but frame stability being toggled can cause autoplay to operation incorrectly. - /// This is clearly a bug with the autoplay algorithm, but is worked around at an editor level for now. - /// - public partial class TestSceneEditorAutoplayFastStreams : EditorTestScene - { - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) - { - var testBeatmap = new TestBeatmap(ruleset, false); - testBeatmap.HitObjects.AddRange(new[] - { - new HitCircle { StartTime = 500 }, - new HitCircle { StartTime = 530 }, - new HitCircle { StartTime = 560 }, - new HitCircle { StartTime = 590 }, - new HitCircle { StartTime = 620 }, - }); - - return testBeatmap; - } - - protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - - [Test] - public void TestAllHit() - { - AddStep("start playback", () => EditorClock.Start()); - AddUntilStep("wait for all hit", () => - { - DrawableHitCircle[] hitCircles = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).ToArray(); - - return hitCircles.Length == 5 && hitCircles.All(h => h.IsHit); - }); - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs index 366f17daee..623cefff6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -46,12 +46,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToObject(() => slider); AddStep("seek after end", () => EditorClock.Seek(750)); - AddUntilStep("wait for seek", () => !EditorClock.IsSeeking); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); AddStep("seek to visible", () => EditorClock.Seek(650)); - AddUntilStep("wait for seek", () => !EditorClock.IsSeeking); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); } diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index ebf06bcc4e..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -1,7 +1,6 @@ // 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.Extensions.ObjectExtensions; @@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Edit [Resolved] private EditorBeatmap beatmap { get; set; } = null!; - [Resolved] - private EditorClock editorClock { get; set; } = null!; - public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -42,6 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { + drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -68,22 +65,6 @@ namespace osu.Game.Rulesets.Edit Scheduler.AddOnce(regenerateAutoplay); } - protected override void Update() - { - base.Update(); - - // Whenever possible, we want to stay in frame stability playback. - // Without doing so, we run into bugs with some gameplay elements not behaving as expected. - // - // Note that this is not using EditorClock.IsSeeking as that would exit frame stability - // on all seeks. The intention here is to retain frame stability for small seeks. - // - // I still think no gameplay elements should require frame stability in the first place, but maybe that ship has sailed already.. - bool shouldBypassFrameStability = Math.Abs(drawableRuleset.FrameStableClock.CurrentTime - editorClock.CurrentTime) > 1000; - - drawableRuleset.FrameStablePlayback = !shouldBypassFrameStability; - } - private void regenerateAutoplay() { var autoplayMod = drawableRuleset.Mods.OfType().Single(); From eecd868d66a2fe928b76756996d25e588b383309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 09:24:50 +0100 Subject: [PATCH 341/539] Use darker blue for `SliderTailHit` result --- osu.Game/Graphics/OsuColour.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 0d11d2d4ef..1b5877b966 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -95,6 +95,7 @@ namespace osu.Game.Graphics case HitResult.SmallTickHit: case HitResult.LargeTickHit: + case HitResult.SliderTailHit: case HitResult.Great: return Blue; From 1cd7656f331172a281b12552c62b1f52ca2bad1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 09:33:04 +0100 Subject: [PATCH 342/539] Reorder hit results so that `SliderTailHit` is next to `SmallTickHit` This addresses https://github.com/ppy/osu/discussions/26507. --- osu.Game/Rulesets/Scoring/HitResult.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index f307344347..7e58df3cfa 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"")] [EnumMember(Value = "none")] - [Order(14)] + [Order(15)] None, /// @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates small tick miss. /// [EnumMember(Value = "small_tick_miss")] - [Order(11)] + [Order(12)] SmallTickMiss, /// @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Scoring /// [EnumMember(Value = "large_tick_miss")] [Description("-")] - [Order(10)] + [Order(11)] LargeTickMiss, /// @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description("S Bonus")] [EnumMember(Value = "small_bonus")] - [Order(9)] + [Order(10)] SmallBonus, /// @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description("L Bonus")] [EnumMember(Value = "large_bonus")] - [Order(8)] + [Order(9)] LargeBonus, /// @@ -119,14 +119,14 @@ namespace osu.Game.Rulesets.Scoring /// [EnumMember(Value = "ignore_miss")] [Description("-")] - [Order(13)] + [Order(14)] IgnoreMiss, /// /// Indicates a hit that should be ignored for scoring purposes. /// [EnumMember(Value = "ignore_hit")] - [Order(12)] + [Order(13)] IgnoreHit, /// @@ -136,14 +136,14 @@ namespace osu.Game.Rulesets.Scoring /// May be paired with . /// [EnumMember(Value = "combo_break")] - [Order(15)] + [Order(16)] ComboBreak, /// /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. /// [EnumMember(Value = "slider_tail_hit")] - [Order(16)] + [Order(8)] SliderTailHit, /// From f85e6add8e7ba95463622904d26202e140ed4434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 12:55:07 +0100 Subject: [PATCH 343/539] Add failing test case --- .../Mods/TestSceneOsuModFlashlight.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index a353914cd5..defbb97c8a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -1,8 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { @@ -21,5 +31,51 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); + + [Test] + public void TestSliderDimsOnlyAfterStartTime() + { + bool sliderDimmedBeforeStartTime = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmedBeforeStartTime |= + Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, }, + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + BeatmapInfo = + { + StackLeniency = 0, + } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(990, new Vector2()), + new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } } } From 0b2b1fc58888b430c16e81119fe2a277567775de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 13:05:02 +0100 Subject: [PATCH 344/539] Fix flashlight dim being applied before slider start time Closes https://github.com/ppy/osu/issues/26515. Compare https://github.com/ppy/osu/pull/26053. --- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 252d7e2762..46c9bdd17f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { if (drawable is DrawableSlider s) - s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; + s.Tracking.ValueChanged += _ => flashlight.OnSliderTrackingChange(s); } private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition @@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods FlashlightSmoothness = 1.4f; } - public void OnSliderTrackingChange(ValueChangedEvent e) + public void OnSliderTrackingChange(DrawableSlider e) { // If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield. - FlashlightDim = e.NewValue ? 0.8f : 0.0f; + FlashlightDim = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 0.8f : 0.0f; } protected override bool OnMouseMove(MouseMoveEvent e) From baf3867e17c5d10e63433dcdf429095bce9deaa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jan 2024 13:48:47 +0100 Subject: [PATCH 345/539] Fix date failing to display on leaderboard for some scores with weird datetimes Addresses https://github.com/ppy/osu/discussions/26517. The score reported has a datetime of 0001/1/1 05:00:00 AM. Bit of a dodge fix but maybe fine? --- osu.Game/Extensions/TimeDisplayExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index 98633958ee..1b224cfeb7 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -59,7 +59,8 @@ namespace osu.Game.Extensions /// A short relative string representing the input time. public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff) { - if (time == default) + // covers all `DateTimeOffset` instances with the date portion of 0001-01-01. + if (time.Date == default) return "-"; var now = DateTime.Now; From fb4f8d0834ee2dee64eda3e5ba8054faa26130eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Jan 2024 22:31:48 +0900 Subject: [PATCH 346/539] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index e39143ab93..a7376aa5a7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 9afeaf7338..98e8b136e5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 724b4c9507b886fa8af21c6384d01ae0752305d4 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 14 Jan 2024 21:09:49 +0100 Subject: [PATCH 347/539] Expand click target of toolbar buttons and clock --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 92 ++++++++++--------- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 77 +++++++++------- .../Overlays/Toolbar/ToolbarHomeButton.cs | 2 +- .../Overlays/Toolbar/ToolbarMusicButton.cs | 2 +- .../Toolbar/ToolbarRulesetTabButton.cs | 2 +- .../Overlays/Toolbar/ToolbarSettingsButton.cs | 2 +- .../Overlays/Toolbar/ToolbarUserButton.cs | 2 +- 7 files changed, 97 insertions(+), 82 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 03a1cfc005..36ff703c82 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -63,6 +63,7 @@ namespace osu.Game.Overlays.Toolbar protected virtual Anchor TooltipAnchor => Anchor.TopLeft; + protected readonly Container ButtonContent; protected ConstrainedIconContainer IconContainer; protected SpriteText DrawableText; protected Box HoverBackground; @@ -80,59 +81,66 @@ namespace osu.Game.Overlays.Toolbar protected ToolbarButton() { - Width = Toolbar.HEIGHT; + AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding(3); - Children = new Drawable[] { - BackgroundContent = new Container + ButtonContent = new Container { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 6, - CornerExponent = 3f, - Children = new Drawable[] - { - HoverBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, - }, - } - }, - Flow = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 }, + Width = Toolbar.HEIGHT, RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + Padding = new MarginPadding(3), Children = new Drawable[] { - IconContainer = new ConstrainedIconContainer + BackgroundContent = new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(20), - Alpha = 0, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + HoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, - DrawableText = new OsuSpriteText + Flow = new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 }, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + IconContainer = new ConstrainedIconContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + Alpha = 0, + }, + DrawableText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, }, }, }, diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 67688155ae..e7959986ae 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -42,52 +42,59 @@ namespace osu.Game.Overlays.Toolbar clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); prefer24HourTime = config.GetBindable(OsuSetting.Prefer24HourTime); - Padding = new MarginPadding(3); - Children = new Drawable[] { new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 6, - CornerExponent = 3f, - Children = new Drawable[] - { - hoverBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - flashBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Colour = Color4.White.Opacity(100), - Blending = BlendingParameters.Additive, - }, - } - }, - new FillFlowContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Padding = new MarginPadding(10), + Padding = new MarginPadding(3), Children = new Drawable[] { - analog = new AnalogClockDisplay + new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + CornerExponent = 3f, + Children = new Drawable[] + { + hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + } }, - digital = new DigitalClockDisplay + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + analog = new AnalogClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + digital = new DigitalClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 70675c1b92..499ca804c9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarHomeButton() { - Width *= 1.4f; + ButtonContent.Width *= 1.4f; Hotkey = GlobalAction.Home; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 69597c6b46..5da0056787 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarMusicButton() { Hotkey = GlobalAction.ToggleNowPlaying; - AutoSizeAxes = Axes.X; + ButtonContent.AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 5500f1c879..98a0191e0a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar public RulesetButton() { - Padding = new MarginPadding(3) + ButtonContent.Padding = new MarginPadding(3) { Bottom = 5 }; diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 78df060252..899f58c9c0 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSettingsButton() { - Width *= 1.4f; + ButtonContent.Width *= 1.4f; Hotkey = GlobalAction.ToggleSettings; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 19b98628e6..230974cd59 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarUserButton() { - AutoSizeAxes = Axes.X; + ButtonContent.AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader] From 47b385c552462c98705e4a8a8c578cf6e8a42419 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 14 Jan 2024 21:13:00 +0100 Subject: [PATCH 348/539] Move toolbar button padding to a const --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 4 +++- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 36ff703c82..1da2e1b744 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Toolbar { public abstract partial class ToolbarButton : OsuClickableContainer, IKeyBindingHandler { + public const float PADDING = 3; + protected GlobalAction? Hotkey { get; set; } public void SetIcon(Drawable icon) @@ -90,7 +92,7 @@ namespace osu.Game.Overlays.Toolbar { Width = Toolbar.HEIGHT, RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding(3), + Padding = new MarginPadding(PADDING), Children = new Drawable[] { BackgroundContent = new Container diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index e7959986ae..e1d658b811 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Padding = new MarginPadding(3), + Padding = new MarginPadding(ToolbarButton.PADDING), Children = new Drawable[] { new Container diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 98a0191e0a..c224f0f9c9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar public RulesetButton() { - ButtonContent.Padding = new MarginPadding(3) + ButtonContent.Padding = new MarginPadding(PADDING) { Bottom = 5 }; From 0b5cc8fb10c88fb12650762a8b4fef78f406bde8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 14:01:03 +0900 Subject: [PATCH 349/539] Fix gameplay counter textures not being cached ahead of time Part of https://github.com/ppy/osu/issues/26535. --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 6 ++++++ osu.Game/Skinning/LegacySpriteText.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index a11f2f01cd..f16669f865 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -137,6 +137,12 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(-2f, 0f); Font = new FontUsage(font_name, 1); glyphStore = new GlyphStore(font_name, textures, getLookup); + + // cache common lookups ahead of time. + foreach (char c in new[] { '.', '%', 'x' }) + glyphStore.Get(font_name, c); + for (int i = 0; i < 10; i++) + glyphStore.Get(font_name, (char)('0' + i)); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 581e7534e4..fdd8716d5a 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -50,6 +50,12 @@ namespace osu.Game.Skinning Spacing = new Vector2(-skin.GetFontOverlap(font), 0); glyphStore = new LegacyGlyphStore(fontPrefix, skin, MaxSizePerGlyph); + + // cache common lookups ahead of time. + foreach (char c in FixedWidthExcludeCharacters) + glyphStore.Get(fontPrefix, c); + for (int i = 0; i < 10; i++) + glyphStore.Get(fontPrefix, (char)('0' + i)); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); From cd20561843874df1eab24e179201eb1742e77520 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 14:12:10 +0900 Subject: [PATCH 350/539] Adjust text slightly --- osu.Game/Localisation/GraphicsSettingsStrings.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index fc29352ae2..1d14b0a596 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -152,14 +152,13 @@ namespace osu.Game.Localisation /// /// "In order to change the renderer, the game will close. Please open it again." /// - public static LocalisableString ChangeRendererConfirmation => - new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again."); + public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again."); /// - /// "Minimise on focus loss" + /// "Minimise osu! when switching to another app" /// - public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise on focus loss"); + public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } From a6c309b61a1edc109dfe0b3b2ff0dca57564628d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 14:12:39 +0900 Subject: [PATCH 351/539] Add more keywords --- osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index adb572e245..71afec88d4 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { LabelText = GraphicsSettingsStrings.MinimiseOnFocusLoss, Current = config.GetBindable(FrameworkSetting.MinimiseOnFocusLossInFullscreen), - Keywords = new[] { "alt-tab" }, + Keywords = new[] { "alt-tab", "minimize", "focus", "hide" }, }, safeAreaConsiderationsCheckbox = new SettingsCheckbox { From 6940579b9edb8c275de74c0f1ba4780f45577aaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 14:30:27 +0900 Subject: [PATCH 352/539] Remember multiplayer room filter mode As proposed in https://github.com/ppy/osu/discussions/26218. --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++++ osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 6 +++++- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 23686db1f8..c05831d043 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -17,6 +17,7 @@ using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Mods.Input; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Skinning; @@ -191,6 +192,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorLimitedDistanceSnap, false); SetDefault(OsuSetting.EditorShowSpeedChanges, false); + SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All); + SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); @@ -423,5 +426,6 @@ namespace osu.Game.Configuration TouchDisableGameplayTaps, ModSelectTextSearchStartsActive, UserOnlineStatus, + MultiplayerRoomFilter } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index fc4a5357c6..6f5ff9c99c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -4,8 +4,8 @@ #nullable disable using System; -using System.Diagnostics; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -19,6 +19,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -77,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [CanBeNull] private LeasedBindable selectionLease; + [Resolved] + protected OsuConfigManager Config { get; private set; } + private readonly Bindable filter = new Bindable(new FilterCriteria()); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 4478179726..a3a6fd2d8e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -51,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer roomAccessTypeDropdown = new SlimEnumDropdown { RelativeSizeAxes = Axes.None, + Current = Config.GetBindable(OsuSetting.MultiplayerRoomFilter), Width = 160, }; From d8962ddff8e75a23114266db68de5f6e54eac264 Mon Sep 17 00:00:00 2001 From: Felipe Marins Date: Mon, 15 Jan 2024 03:22:52 -0300 Subject: [PATCH 353/539] Select all when pressing enter instead of every mod selection change --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 43f44d682d..8cb990757f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -565,9 +565,6 @@ namespace osu.Game.Overlays.Mods .ToArray(); SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); - - if (SearchTextBox.HasFocus) - SearchTextBox.SelectAll(); } #region Transition handling @@ -724,6 +721,8 @@ namespace osu.Game.Overlays.Mods if (firstMod is not null) firstMod.Active.Value = !firstMod.Active.Value; + SearchTextBox.SelectAll(); + return true; } } From 2d5a39b234b85f9fe235bb746f50185c0e336b55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 16:01:34 +0900 Subject: [PATCH 354/539] Add failing test coverage of duplicates in judgement counter display --- .../Visual/Gameplay/TestSceneJudgementCounter.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs index dae6dc7b4b..787c48fb26 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -147,6 +147,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); } + [Test] + public void TestNoDuplicates() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + AddAssert("Check no duplicates", + () => counterDisplay.CounterFlow.ChildrenOfType().Count(), + () => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType().Select(c => c.ResultName.Text).Distinct().Count())); + } + [Test] public void TestCycleDisplayModes() { From e6453853c256245a2d600904b9df29a963268635 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 15:54:00 +0900 Subject: [PATCH 355/539] De-dupe displayed hits in judgement counter --- .../Gameplay/TestSceneJudgementCounter.cs | 2 +- .../JudgementCountController.cs | 36 ++++++++++++++----- .../HUD/JudgementCounter/JudgementCounter.cs | 7 ++-- .../JudgementCounterDisplay.cs | 9 +++-- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs index 787c48fb26..894f08e5b2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay private int hiddenCount() { - var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit); + var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit)); return num.Result.ResultCount.Value; } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 43c2ae442a..8134c97bac 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -21,18 +22,30 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - public List Results = new List(); + private readonly Dictionary results = new Dictionary(); + + public IEnumerable Counters => counters; + + private readonly List counters = new List(); [BackgroundDependencyLoader] private void load(IBindable ruleset) { - foreach (var result in ruleset.Value.CreateInstance().GetHitResults()) + // Due to weirdness in judgements, some results have the same name and should be aggregated for display purposes. + // There's only one case of this right now ("slider end"). + foreach (var group in ruleset.Value.CreateInstance().GetHitResults().GroupBy(r => r.displayName)) { - Results.Add(new JudgementCount + var judgementCount = new JudgementCount { - Type = result.result, + DisplayName = group.Key, + Types = group.Select(r => r.result).ToArray(), ResultCount = new BindableInt() - }); + }; + + counters.Add(judgementCount); + + foreach (var r in group) + results[r.result] = judgementCount; } } @@ -46,13 +59,20 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter private void updateCount(JudgementResult judgement, bool revert) { - foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type)) - result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1; + if (!results.TryGetValue(judgement.Type, out var count)) + return; + + if (revert) + count.ResultCount.Value--; + else + count.ResultCount.Value++; } public struct JudgementCount { - public HitResult Type { get; set; } + public LocalisableString DisplayName { get; set; } + + public HitResult[] Types { get; set; } public BindableInt ResultCount { get; set; } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 6c417faac2..45ed8d749b 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -44,14 +45,14 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { Alpha = 0, Font = OsuFont.Numeric.With(size: 8), - Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type) + Text = Result.DisplayName, } } }; - var result = Result.Type; + var result = Result.Types.First(); - Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; + Colour = result.IsBasic() ? colours.ForHitResult(result) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 128897ddde..326be55222 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -49,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter AutoSizeAxes = Axes.Both }; - foreach (var result in judgementCountController.Results) + foreach (var result in judgementCountController.Counters) CounterFlow.Add(createCounter(result)); } @@ -88,7 +89,9 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter if (index == 0 && !ShowMaxJudgement.Value) return false; - if (counter.Result.Type.IsBasic()) + var hitResult = counter.Result.Types.First(); + + if (hitResult.IsBasic()) return true; switch (Mode.Value) @@ -97,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter return false; case DisplayMode.Normal: - return !counter.Result.Type.IsBonus(); + return !hitResult.IsBonus(); case DisplayMode.All: return true; From 2b45a9b7c629fd9f8df01fc35db37da7f8b511c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 17:10:16 +0900 Subject: [PATCH 356/539] Add failing test coverage showing collection dropdown crash --- .../Visual/SongSelect/TestSceneFilterControl.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 94c6130f15..58183acb93 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -76,6 +77,20 @@ namespace osu.Game.Tests.Visual.SongSelect assertCollectionDropdownContains("2"); } + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + [Test] public void TestCollectionRemovedFromDropdown() { From 0a522d260becbbcc2e7d2835b763208ea76a6d4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 17:10:30 +0900 Subject: [PATCH 357/539] Fix collection dropdown crashing when all collections are deleted at once --- osu.Game/Collections/CollectionDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 8d83ed3ec9..249a0c35e7 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -74,7 +74,7 @@ namespace osu.Game.Collections } else { - foreach (int i in changes.DeletedIndices) + foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) filters.RemoveAt(i + 1); foreach (int i in changes.InsertedIndices) From 52f8348ee31d1cecdc5a514565811588aaf31073 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 17:48:37 +0900 Subject: [PATCH 358/539] Fade hold-for-menu button out completely on non-touch devices --- .../Screens/Play/HUD/HoldForMenuButton.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 0921a9f18a..1cf3d25dad 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -44,6 +45,8 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); + + AlwaysPresent = true; } [BackgroundDependencyLoader(true)] @@ -66,9 +69,15 @@ namespace osu.Game.Screens.Play.HUD Action = () => Action(), } }; + AutoSizeAxes = Axes.Both; } + [Resolved] + private SessionStatics sessionStatics { get; set; } + + private Bindable touchActive; + protected override void LoadComplete() { button.HoldActivationDelay.BindValueChanged(v => @@ -78,7 +87,20 @@ namespace osu.Game.Screens.Play.HUD : "press for menu"; }, true); - text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint); + touchActive = sessionStatics.GetBindable(Static.TouchInputActive); + + if (touchActive.Value) + { + Alpha = 1f; + text.FadeInFromZero(500, Easing.OutQuint) + .Delay(1500) + .FadeOut(500, Easing.OutQuint); + } + else + { + Alpha = 0; + text.Alpha = 0f; + } base.LoadComplete(); } @@ -99,9 +121,11 @@ namespace osu.Game.Screens.Play.HUD Alpha = 1; else { + float minAlpha = touchActive.Value ? .08f : 0; + Alpha = Interpolation.ValueAt( Math.Clamp(Clock.ElapsedFrameTime, 0, 200), - Alpha, Math.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); + Alpha, Math.Clamp(1 - positionalAdjust, minAlpha, 1), 0, 200, Easing.OutQuint); } } From 2a2a4c416eaf11ac2212ebbc88c19567e6ec22b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 18:17:49 +0900 Subject: [PATCH 359/539] Only display offset toast when in local gameplay --- osu.Game/Screens/Play/Player.cs | 6 ------ osu.Game/Screens/Play/SubmittingPlayer.cs | 10 ++++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b87306b9a2..96530634fd 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -461,12 +461,6 @@ namespace osu.Game.Screens.Play OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, - new GameplayOffsetControl - { - Margin = new MarginPadding(20), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } }, }; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 04abb6162f..171ceea84f 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -59,6 +60,15 @@ namespace osu.Game.Screens.Play } AddInternal(new PlayerTouchInputDetector()); + + // We probably want to move this display to something more global. + // Probably using the OSD somehow. + AddInternal(new GameplayOffsetControl + { + Margin = new MarginPadding(20), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }); } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) From 0aa8a20d57a9d50bc39d7ecf2d847314a51d317e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 18:34:32 +0900 Subject: [PATCH 360/539] Fix regression in interaction when panels are not selectable --- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 823f6ea308..800c73cceb 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -365,7 +365,7 @@ namespace osu.Game.Screens.OnlinePlay AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 8, Right = 8 }, }, - mainFillFlow = new MainFlow(() => SelectedItem.Value == Model) + mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -671,13 +671,13 @@ namespace osu.Game.Screens.OnlinePlay public partial class MainFlow : FillFlowContainer { - private readonly Func isSelected; + private readonly Func allowInteraction; - public override bool PropagatePositionalInputSubTree => isSelected(); + public override bool PropagatePositionalInputSubTree => allowInteraction(); - public MainFlow(Func isSelected) + public MainFlow(Func allowInteraction) { - this.isSelected = isSelected; + this.allowInteraction = allowInteraction; } } } From 8e32780888cab9e3e035a8b9a3233c10e066f47e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 19:21:19 +0900 Subject: [PATCH 361/539] Fix background dim occasionally getting in a bad state when exiting gameplay --- .../Screens/Backgrounds/BackgroundScreenBeatmap.cs | 2 +- osu.Game/Screens/Play/Player.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 85ea881006..185e2cab99 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Backgrounds /// public readonly Bindable DimWhenUserSettingsIgnored = new Bindable(); - internal readonly IBindable IsBreakTime = new Bindable(); + internal readonly Bindable IsBreakTime = new Bindable(); private readonly DimmableBackground dimmable; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b87306b9a2..f0f0a58368 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1078,7 +1078,7 @@ namespace osu.Game.Screens.Play b.FadeColour(Color4.White, 250); // bind component bindables. - b.IsBreakTime.BindTo(breakTracker.IsBreakTime); + ((IBindable)b.IsBreakTime).BindTo(breakTracker.IsBreakTime); b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); @@ -1238,7 +1238,13 @@ namespace osu.Game.Screens.Play if (this.IsCurrentScreen()) { - ApplyToBackground(b => b.IgnoreUserSettings.Value = true); + ApplyToBackground(b => + { + b.IgnoreUserSettings.Value = true; + + b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); + b.IsBreakTime.Value = true; + }); storyboardReplacesBackground.Value = false; } } From 494c1be6553ed9b891b8b05dc37bbe6d50d3f3a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 15 Jan 2024 13:30:34 +0300 Subject: [PATCH 362/539] Fix blatant error in `TestSceneModAccuracyChallenge` --- osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs index 6bdb9132e1..c5e56c6453 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Mods public void TestMaximumAchievableAccuracy() => CreateModTest(new ModTestData { - Mod = new ModAccuracyChallenge + Mod = new OsuModAccuracyChallenge { MinimumAccuracy = { Value = 0.6 } }, @@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Mods public void TestStandardAccuracy() => CreateModTest(new ModTestData { - Mod = new ModAccuracyChallenge + Mod = new OsuModAccuracyChallenge { MinimumAccuracy = { Value = 0.6 }, AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard } From e3989c854d39ec82d1ea27ada8f70a9dea159633 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 15 Jan 2024 16:57:22 +0600 Subject: [PATCH 363/539] Change `LoginPanel` to use `LocalUser.Status` for the dropdown --- osu.Game/Overlays/Login/LoginPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 513088ba0c..7db3644de6 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.Login }, }; - panel.Status.BindValueChanged(_ => updateDropdownCurrent(), true); + api.LocalUser.Value.Status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); dropdown.Current.BindValueChanged(action => { @@ -172,9 +172,9 @@ namespace osu.Game.Overlays.Login ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); }); - private void updateDropdownCurrent() + private void updateDropdownCurrent(UserStatus? status) { - switch (panel.Status.Value) + switch (status) { case UserStatus.Online: dropdown.Current.Value = UserAction.Online; From 13517869f6bb373fbe6b44df5728d31bc696ef3d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 15 Jan 2024 14:03:23 +0300 Subject: [PATCH 364/539] Apply scale transitions in resume overlay cursor to local container --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 24 +++++++++++--------- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 20 ++++++++++++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 18351c20ce..710ca9ace7 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -57,17 +57,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor [BackgroundDependencyLoader] private void load() { - InternalChild = cursorScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }; + InternalChild = CreateCursorContent(); userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale(); @@ -84,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor cursorScale.Value = CalculateCursorScale(); } + protected virtual Drawable CreateCursorContent() => cursorScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, + }; + protected virtual float CalculateCursorScale() { float scale = userCursorScale.Value; diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index f5e83f46f2..8a84fe14e5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -65,17 +65,24 @@ namespace osu.Game.Rulesets.Osu.UI public override bool HandlePositionalInput => true; public Action ResumeRequested; + private Container scaleTransitionContainer; public OsuClickToResumeCursor() { RelativePositionAxes = Axes.Both; } - protected override float CalculateCursorScale() + protected override Container CreateCursorContent() => scaleTransitionContainer = new Container { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Child = base.CreateCursorContent(), + }; + + protected override float CalculateCursorScale() => // Force minimum cursor size so it's easily clickable - return Math.Max(1f, base.CalculateCursorScale()); - } + Math.Max(1f, base.CalculateCursorScale()); protected override bool OnHover(HoverEvent e) { @@ -98,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHovered) return false; - this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); + scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); ResumeRequested?.Invoke(); return true; @@ -114,7 +121,10 @@ namespace osu.Game.Rulesets.Osu.UI public void Appear() => Schedule(() => { updateColour(); - this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint); + + // importantly, we perform the scale transition on an underlying container rather than the whole cursor + // to prevent attempts of abuse by the scale change in the cursor's hitbox (see: https://github.com/ppy/osu/issues/26477). + scaleTransitionContainer.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint); }); private void updateColour() From 0a02050a8523c6c873b4fbf582f072d649efaec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 12:12:27 +0100 Subject: [PATCH 365/539] Fix test crashing at the end in visual test browser --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 201fac37c8..747805edc8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1033,12 +1033,6 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("join other user and make host", () => - { - multiplayerClient.AddUser(new APIUser { Id = 1234 }); - multiplayerClient.TransferHost(1234); - }); - AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } })); AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready)); AddStep("press edit on second item", () => this.ChildrenOfType().Single(i => i.Item.RulesetID == 1) @@ -1047,14 +1041,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1); - AddStep("start match by other user", () => - { - multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); - multiplayerClient.StartMatch().WaitSafely(); - }); + AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad); - AddStep("set player loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing); AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden))); } From 96c472b870790825e6acbc692a425612f97efb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 12:33:25 +0100 Subject: [PATCH 366/539] Remove unused using directive --- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 58183acb93..a639d50eee 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; From 86382f4408baa4bc292b2c510a9db49a8af0f6c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Jan 2024 12:49:40 +0100 Subject: [PATCH 367/539] Clarify comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 2f012e33da..f5840248fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -250,7 +250,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateAfterChildren(); - // It's important that this is done in UpdateAfterChildren so that the SliderBody has a chance to update its Size and PathOffset on Update. + // During slider path editing, the PlaySliderBody is scheduled to refresh once in the Update phase. + // It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity + // to update its Size and PathOffset beforehand, ensuring correct placement. + double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); Ball.UpdateProgress(completionProgress); From 6b844ed8b64db49554031bd397eaa310d4b54601 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 20:29:08 +0900 Subject: [PATCH 368/539] Split out judgement pooling concepts from `OsuPlayfield` for reuse --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 64 +++++--------------- osu.Game/Rulesets/UI/JudgementPooler.cs | 77 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 50 deletions(-) create mode 100644 osu.Game/Rulesets/UI/JudgementPooler.cs diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c94057cf6d..411a02c5af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -4,13 +4,11 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -35,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; + private readonly JudgementPooler judgementPooler; + public SmokeContainer Smoke { get; } public FollowPointRenderer FollowPoints { get; } @@ -42,8 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly IDictionary> poolDictionary = new Dictionary>(); - private readonly Container judgementAboveHitObjectLayer; public OsuPlayfield() @@ -65,24 +63,15 @@ namespace osu.Game.Rulesets.Osu.UI HitPolicy = new StartTimeOrderedHitPolicy(); - foreach (var result in Enum.GetValues().Where(r => - { - switch (r) - { - case HitResult.Great: - case HitResult.Ok: - case HitResult.Meh: - case HitResult.Miss: - case HitResult.LargeTickMiss: - case HitResult.IgnoreMiss: - return true; - } - - return false; - })) - poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded)); - - AddRangeInternal(poolDictionary.Values); + AddInternal(judgementPooler = new JudgementPooler(new[] + { + HitResult.Great, + HitResult.Ok, + HitResult.Meh, + HitResult.Miss, + HitResult.LargeTickMiss, + HitResult.IgnoreMiss, + }, onJudgementLoaded)); NewResult += onNewResult; } @@ -182,10 +171,10 @@ namespace osu.Game.Rulesets.Osu.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - if (!poolDictionary.TryGetValue(result.Type, out var pool)) - return; + var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject)); - DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject)); + if (explosion == null) + return; judgementLayer.Add(explosion); @@ -201,31 +190,6 @@ namespace osu.Game.Rulesets.Osu.UI public void Add(Drawable proxy) => AddInternal(proxy); } - private partial class DrawableJudgementPool : DrawablePool - { - private readonly HitResult result; - private readonly Action onLoaded; - - public DrawableJudgementPool(HitResult result, Action onLoaded) - : base(20) - { - this.result = result; - this.onLoaded = onLoaded; - } - - protected override DrawableOsuJudgement CreateNewDrawable() - { - var judgement = base.CreateNewDrawable(); - - // just a placeholder to initialise the correct drawable hierarchy for this pool. - judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); - - onLoaded?.Invoke(judgement); - - return judgement; - } - } - private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry { public OsuHitObjectLifetimeEntry(HitObject hitObject) diff --git a/osu.Game/Rulesets/UI/JudgementPooler.cs b/osu.Game/Rulesets/UI/JudgementPooler.cs new file mode 100644 index 0000000000..efec760f15 --- /dev/null +++ b/osu.Game/Rulesets/UI/JudgementPooler.cs @@ -0,0 +1,77 @@ +// 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.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Handles the task of preparing poolable drawable judgements for gameplay usage. + /// + /// The drawable judgement type. + public partial class JudgementPooler : CompositeComponent + where T : DrawableJudgement, new() + { + private readonly IDictionary> poolDictionary = new Dictionary>(); + + private readonly IEnumerable usableHitResults; + private readonly Action? onJudgementInitialLoad; + + public JudgementPooler(IEnumerable usableHitResults, Action? onJudgementInitialLoad = null) + { + this.usableHitResults = usableHitResults; + this.onJudgementInitialLoad = onJudgementInitialLoad; + } + + public T? Get(HitResult result, Action? setupAction) + { + if (!poolDictionary.TryGetValue(result, out var pool)) + return null; + + return pool.Get(setupAction); + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (HitResult result in usableHitResults) + { + var pool = new DrawableJudgementPool(result, onJudgementInitialLoad); + poolDictionary.Add(result, pool); + AddInternal(pool); + } + } + + private partial class DrawableJudgementPool : DrawablePool + { + private readonly HitResult result; + private readonly Action? onLoaded; + + public DrawableJudgementPool(HitResult result, Action? onLoaded) + : base(20) + { + this.result = result; + this.onLoaded = onLoaded; + } + + protected override T CreateNewDrawable() + { + var judgement = base.CreateNewDrawable(); + + // just a placeholder to initialise the correct drawable hierarchy for this pool. + judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); + + onLoaded?.Invoke(judgement); + + return judgement; + } + } + } +} From e9812fac7ae09d4f33302bce80e20a353de9f8e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 20:37:33 +0900 Subject: [PATCH 369/539] Fix osu!mania judgments not being pooled correctly --- osu.Game.Rulesets.Mania/UI/Stage.cs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index fa9af6d157..36286940a8 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -1,22 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; - private readonly DrawablePool judgementPool; + private readonly JudgementPooler judgementPooler; private readonly Drawable barLineContainer; @@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly int firstColumnIndex; + private ISkinSource currentSkin = null!; + public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) { this.firstColumnIndex = firstColumnIndex; @@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI InternalChildren = new Drawable[] { - judgementPool = new DrawablePool(2), new Container { Anchor = Anchor.TopCentre, @@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Y, }, - new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null) + new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground)) { RelativeSizeAxes = Axes.Both }, @@ -137,11 +139,13 @@ namespace osu.Game.Rulesets.Mania.UI AddNested(column); } + var hitWindows = new ManiaHitWindows(); + + AddInternal(judgementPooler = new JudgementPooler(Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)))); + RegisterPool(50, 200); } - private ISkinSource currentSkin; - [BackgroundDependencyLoader] private void load(ISkinSource skin) { @@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); - if (currentSkin != null) + if (currentSkin.IsNotNull()) currentSkin.SourceChanged -= onSkinChanged; } @@ -196,13 +200,13 @@ namespace osu.Game.Rulesets.Mania.UI return; judgements.Clear(false); - judgements.Add(judgementPool.Get(j => + judgements.Add(judgementPooler.Get(result.Type, j => { j.Apply(result, judgedObject); j.Anchor = Anchor.Centre; j.Origin = Anchor.Centre; - })); + })!); } protected override void Update() From 0fbca59523cea2bd7036581c5445c7173ff8123f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jan 2024 20:49:00 +0900 Subject: [PATCH 370/539] Fix osu!taiko judgments not being pooled correctly They weren't being initialised correctly on initial pool. --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 49 ++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 31f8171290..7e3ed7a4d4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -10,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -42,29 +39,29 @@ namespace osu.Game.Rulesets.Taiko.UI public Container UnderlayElements { get; private set; } = null!; - private Container hitExplosionContainer; - private Container kiaiExplosionContainer; - private JudgementContainer judgementContainer; - private ScrollingHitObjectContainer drumRollHitContainer; - internal Drawable HitTarget; - private SkinnableDrawable mascot; + private Container hitExplosionContainer = null!; + private Container kiaiExplosionContainer = null!; + private JudgementContainer judgementContainer = null!; + private ScrollingHitObjectContainer drumRollHitContainer = null!; + internal Drawable HitTarget = null!; + private SkinnableDrawable mascot = null!; - private readonly IDictionary> judgementPools = new Dictionary>(); + private JudgementPooler judgementPooler = null!; private readonly IDictionary explosionPools = new Dictionary(); - private ProxyContainer topLevelHitContainer; - private InputDrum inputDrum; - private Container rightArea; + private ProxyContainer topLevelHitContainer = null!; + private InputDrum inputDrum = null!; + private Container rightArea = null!; /// /// is purposefully not called on this to prevent i.e. being able to interact /// with bar lines in the editor. /// - private BarLinePlayfield barLinePlayfield; + private BarLinePlayfield barLinePlayfield = null!; - private Container barLineContent; - private Container hitObjectContent; - private Container overlayContent; + private Container barLineContent = null!; + private Container hitObjectContent = null!; + private Container overlayContent = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -202,13 +199,12 @@ namespace osu.Game.Rulesets.Taiko.UI var hitWindows = new TaikoHitWindows(); - foreach (var result in Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r))) - { - judgementPools.Add(result, new DrawablePool(15)); - explosionPools.Add(result, new HitExplosionPool(result)); - } + HitResult[] usableHitResults = Enum.GetValues().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray(); - AddRangeInternal(judgementPools.Values); + AddInternal(judgementPooler = new JudgementPooler(usableHitResults)); + + foreach (var result in usableHitResults) + explosionPools.Add(result, new HitExplosionPool(result)); AddRangeInternal(explosionPools.Values); } @@ -339,7 +335,12 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.Type.IsScorable()) break; - judgementContainer.Add(judgementPools[result.Type].Get(j => j.Apply(result, judgedObject))); + var judgement = judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject)); + + if (judgement == null) + return; + + judgementContainer.Add(judgement); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; addExplosion(judgedObject, result.Type, type); From 96ffe8e737e327517e4d585d1a5dd8dff754032b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Jan 2024 12:51:08 +0100 Subject: [PATCH 371/539] change wording --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f5840248fb..ed4c02a4a9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -250,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateAfterChildren(); - // During slider path editing, the PlaySliderBody is scheduled to refresh once in the Update phase. + // During slider path editing, the PlaySliderBody is scheduled to refresh once on Update. // It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity // to update its Size and PathOffset beforehand, ensuring correct placement. From 9bc9db9138ee6d8e103c1f2a8dd87b671cb6a5b8 Mon Sep 17 00:00:00 2001 From: wooster0 Date: Mon, 15 Jan 2024 20:58:43 +0900 Subject: [PATCH 372/539] simplify the code even more --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 697d62ad6e..da1609c2c4 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -248,7 +248,7 @@ namespace osu.Game.Screens.Ranking lastFetchCompleted = true; - if (ScorePanelList.IsEmpty) + if (!scores.Any()) { // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 95c90e35a0..b75f3d86ff 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -49,8 +49,6 @@ namespace osu.Game.Screens.Ranking public bool AllPanelsVisible => flow.All(p => p.IsPresent); - public bool IsEmpty => flow.Count == 0; - /// /// The current scroll position. /// From 1d7b63e204bfac6a5350b3221b3c951f2e4e5510 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 15 Jan 2024 14:57:17 +0300 Subject: [PATCH 373/539] Move checking logic inside `ModUtils` and somewhat optimise --- osu.Game/Screens/Play/Player.cs | 3 ++- osu.Game/Utils/ModUtils.cs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ffd585148b..5f52f59ad2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -38,6 +38,7 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Screens.Play @@ -213,7 +214,7 @@ namespace osu.Game.Screens.Play if (playableBeatmap == null) return; - if (gameplayMods.Any(m => ruleset.AllMods.All(rulesetMod => m.GetType() != rulesetMod.GetType()))) + if (!ModUtils.CheckModsBelongToRuleset(ruleset, gameplayMods)) { Logger.Log($@"Gameplay was started with a mod belonging to a ruleset different than '{ruleset.Description}'.", level: LogLevel.Important); return; diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 252579a186..827b199b16 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -229,6 +229,38 @@ namespace osu.Game.Utils return proposedWereValid; } + /// + /// Verifies all mods provided belong to the given ruleset. + /// + /// The ruleset to check the proposed mods against. + /// The mods proposed for checking. + /// Whether all belong to the given . + public static bool CheckModsBelongToRuleset(Ruleset ruleset, IEnumerable proposedMods) + { + var rulesetModsTypes = ruleset.AllMods.Select(m => m.GetType()).ToList(); + + foreach (var proposedMod in proposedMods) + { + bool found = false; + + var proposedModType = proposedMod.GetType(); + + foreach (var rulesetModType in rulesetModsTypes) + { + if (rulesetModType == proposedModType) + { + found = true; + break; + } + } + + if (!found) + return true; + } + + return true; + } + /// /// Given a value of a score multiplier, returns a string version with special handling for a value near 1.00x. /// From d346dd065027d08d374d76f045302c356d05fb9a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 15 Jan 2024 15:01:13 +0300 Subject: [PATCH 374/539] Fix `TestModReinstantiation` failing due to custom mod being used --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 8 +------- osu.Game/Utils/ModUtils.cs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index dbd1ce1f6e..f97372e9b6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -13,7 +13,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; @@ -487,13 +486,8 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private class TestMod : Mod, IApplicableToScoreProcessor + private class TestMod : OsuModDoubleTime, IApplicableToScoreProcessor { - public override string Name => string.Empty; - public override string Acronym => string.Empty; - public override double ScoreMultiplier => 1; - public override LocalisableString Description => string.Empty; - public bool Applied { get; private set; } public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 827b199b16..7a9f93e06f 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -247,7 +247,7 @@ namespace osu.Game.Utils foreach (var rulesetModType in rulesetModsTypes) { - if (rulesetModType == proposedModType) + if (rulesetModType.IsAssignableFrom(proposedModType)) { found = true; break; From 399fc8195a7441c250b528ffb6b8953aedba1aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 13:22:22 +0100 Subject: [PATCH 375/539] Fix cursor ripple pool not loading pooled drawables ahead of time Increment the counter over at https://github.com/ppy/osu-framework/pull/6136. --- osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs index 52486b701a..4cb91aa103 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void load(OsuRulesetConfigManager? rulesetConfig) { rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples); + + AddInternal(ripplePool); } public bool OnPressed(KeyBindingPressEvent e) From c5351bd14df70f61ab72c782b1dcab43c2d673b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 14:20:07 +0100 Subject: [PATCH 376/539] Fix back-to-front set --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f0f0a58368..4eb69a9893 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1243,7 +1243,7 @@ namespace osu.Game.Screens.Play b.IgnoreUserSettings.Value = true; b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); - b.IsBreakTime.Value = true; + b.IsBreakTime.Value = false; }); storyboardReplacesBackground.Value = false; } From 91f8144f98adc6f79156d4514840d7b668ced376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 14:58:46 +0100 Subject: [PATCH 377/539] Add test coverage --- .../TestSceneDrawableRoomPlaylist.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 312135402f..d40283ac74 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -19,8 +19,10 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Models; using osu.Game.Online.API; +using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -302,6 +304,36 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestSelectableMouseHandling() + { + bool resultsRequested = false; + + AddStep("reset flag", () => resultsRequested = false); + createPlaylist(p => + { + p.AllowSelection = true; + p.AllowShowingResults = true; + p.RequestResults = _ => resultsRequested = true; + }); + + AddStep("move mouse to first item title", () => + { + var drawQuad = playlist.ChildrenOfType().First().ScreenSpaceDrawQuad; + var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); + InputManager.MoveMouseTo(location); + }); + AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); + AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); + // implies being clickable. + AddUntilStep("first item title hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.True); + + AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(5))); + AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); + AddUntilStep("results requested", () => resultsRequested); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); From d8d1d9264c7bd1b933a21e67e26efebd2107707e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 18:29:51 +0100 Subject: [PATCH 378/539] Fix test failure --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index d40283ac74..6446ebd35f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -323,6 +323,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); InputManager.MoveMouseTo(location); }); + AddUntilStep("wait for text load", () => playlist.ChildrenOfType().Any()); AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); From 8a839f64ed18cfabd6ca8ae98e7ba80441641803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 18:47:46 +0100 Subject: [PATCH 379/539] Add failing test coverage for placeholder shown when it shouldn't be --- .../TestScenePlaylistsResultsScreen.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 727d9da50b..ff1b351b9f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Placeholders; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; @@ -139,6 +140,37 @@ namespace osu.Game.Tests.Visual.Playlists } } + [Test] + public void TestNoMoreScoresToTheRight() + { + AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); + + createResults(); + waitForDisplay(); + + int beforePanelCount = 0; + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); + AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); + AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); + AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + [Test] public void TestFetchWhenScrolledToTheLeft() { From 9d2c82452cfee1cfd7b23dc81c78d902938b5945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 18:47:49 +0100 Subject: [PATCH 380/539] Revert "simplify the code even more" This reverts commit 9bc9db9138ee6d8e103c1f2a8dd87b671cb6a5b8. --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index da1609c2c4..697d62ad6e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -248,7 +248,7 @@ namespace osu.Game.Screens.Ranking lastFetchCompleted = true; - if (!scores.Any()) + if (ScorePanelList.IsEmpty) { // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index b75f3d86ff..95c90e35a0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.Ranking public bool AllPanelsVisible => flow.All(p => p.IsPresent); + public bool IsEmpty => flow.Count == 0; + /// /// The current scroll position. /// From 988794cf90d8bbe803c6a24a1e8c0c8a3ffaf2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 18:49:41 +0100 Subject: [PATCH 381/539] Improve test coverage for empty results --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index ff1b351b9f..e805b03542 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -201,7 +201,8 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind user score info handler", () => bindHandler(noScores: true)); createResults(); - AddAssert("no scores visible", () => resultsScreen.ScorePanelList.GetScorePanels().Count() == 0); + AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); + AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } private void createResults(Func getScore = null) From e8394e6f74351f7b136fe16d2a53033814d1bd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 19:05:33 +0100 Subject: [PATCH 382/539] Add test coverage --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4b101a52f4..7958b68fb5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -542,6 +542,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press enter", () => InputManager.Key(Key.Enter)); AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value); + AddAssert("all text selected in textbox", () => + { + var textBox = modSelectOverlay.ChildrenOfType().Single(); + return textBox.SelectedText == textBox.Text; + }); AddStep("press enter again", () => InputManager.Key(Key.Enter)); AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value); From 8661edfc2fbc3cb9a5df62f8f31a7e1c928eb941 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 15 Jan 2024 21:07:09 +0300 Subject: [PATCH 383/539] Organize consts better --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index dcc8f4feec..6fc957874a 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -58,6 +58,8 @@ namespace osu.Game.Screens.Play.HUD public const float MAIN_PATH_RADIUS = 10f; private const float padding = MAIN_PATH_RADIUS * 2; + private const float glow_path_radius = 40f; + private const float main_path_glow_portion = 0.6f; private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); @@ -90,7 +92,7 @@ namespace osu.Game.Screens.Play.HUD { RelativeSizeAxes = Axes.Both, // since we are using bigger path radius we need to expand the draw area outwards to preserve the curve placement - Padding = new MarginPadding(-30f), + Padding = new MarginPadding(MAIN_PATH_RADIUS - glow_path_radius), Child = glowBar = new ArgonHealthDisplayBar { RelativeSizeAxes = Axes.Both, @@ -98,8 +100,8 @@ namespace osu.Game.Screens.Play.HUD GlowColour = main_bar_glow_colour, Blending = BlendingParameters.Additive, Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), - PathRadius = 40f, - GlowPortion = 0.9f, + PathRadius = glow_path_radius, + GlowPortion = (glow_path_radius - MAIN_PATH_RADIUS * (1f - main_path_glow_portion)) / glow_path_radius, } }, mainBar = new ArgonHealthDisplayBar @@ -109,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD BarColour = main_bar_colour, GlowColour = main_bar_glow_colour, PathRadius = MAIN_PATH_RADIUS, - GlowPortion = 0.6f, + GlowPortion = main_path_glow_portion } } }; From c0d4ed4789fbd7de96c131e2e7882ac673d03e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 19:07:52 +0100 Subject: [PATCH 384/539] Add test coverage of select-all-text-in-search when nothing matched --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 7958b68fb5..046954db47 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -551,6 +551,14 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press enter again", () => InputManager.Key(Key.Enter)); AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value); + AddStep("apply search matching nothing", () => modSelectOverlay.SearchTerm = "ZZZ"); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("all text not selected in textbox", () => + { + var textBox = modSelectOverlay.ChildrenOfType().Single(); + return textBox.SelectedText != textBox.Text; + }); + AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); From c46615839dcc40bca80f401d8559422492c43cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 19:09:50 +0100 Subject: [PATCH 385/539] Only select all text in mod search text box if enter press selected anything --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8cb990757f..a774bb9167 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -719,9 +719,10 @@ namespace osu.Game.Overlays.Mods ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); if (firstMod is not null) + { firstMod.Active.Value = !firstMod.Active.Value; - - SearchTextBox.SelectAll(); + SearchTextBox.SelectAll(); + } return true; } From 97e08f50715e980d4a987d9f8ca52f7d6d178df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Jan 2024 21:39:33 +0100 Subject: [PATCH 386/539] Fix `ColourHitErrorMeter` not loading pooled drawables ahead of time --- .../HUD/HitErrorMeters/ColourHitErrorMeter.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 65f4b50dde..0f2f9dc323 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -15,7 +15,6 @@ using osu.Game.Localisation.HUD; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { @@ -42,16 +41,21 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [SettingSource(typeof(ColourHitErrorMeterStrings), nameof(ColourHitErrorMeterStrings.JudgementShape), nameof(ColourHitErrorMeterStrings.JudgementShapeDescription))] public Bindable JudgementShape { get; } = new Bindable(); + private readonly DrawablePool judgementShapePool; private readonly JudgementFlow judgementsFlow; public ColourHitErrorMeter() { AutoSizeAxes = Axes.Both; - InternalChild = judgementsFlow = new JudgementFlow + InternalChildren = new Drawable[] { - JudgementShape = { BindTarget = JudgementShape }, - JudgementSpacing = { BindTarget = JudgementSpacing }, - JudgementCount = { BindTarget = JudgementCount } + judgementShapePool = new DrawablePool(50), + judgementsFlow = new JudgementFlow + { + JudgementShape = { BindTarget = JudgementShape }, + JudgementSpacing = { BindTarget = JudgementSpacing }, + JudgementCount = { BindTarget = JudgementCount } + } }; } @@ -60,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters if (!judgement.Type.IsScorable() || judgement.Type.IsBonus()) return; - judgementsFlow.Push(GetColourForHitResult(judgement.Type)); + judgementsFlow.Push(judgementShapePool.Get(shape => shape.Colour = GetColourForHitResult(judgement.Type))); } public override void Clear() @@ -105,15 +109,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private readonly DrawablePool judgementLinePool = new DrawablePool(50); - public void Push(Color4 colour) + public void Push(HitErrorShape shape) { - judgementLinePool.Get(shape => - { - shape.Colour = colour; - Add(shape); - - removeExtraJudgements(); - }); + Add(shape); + removeExtraJudgements(); } private void removeExtraJudgements() From 09e5b2fb46748d7c03ac7764e9170d2c860ac7ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 13:18:43 +0900 Subject: [PATCH 387/539] Add test coverage of incorrect default state for `Rank` when hidden is applied --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 567bf6968f..73465fae08 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -18,10 +18,12 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Beatmaps; @@ -385,6 +387,42 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1)); } + [Test] + public void TestNormalGrades() + { + scoreProcessor.ApplyBeatmap(new Beatmap()); + + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.S)); + } + + [Test] + public void TestSilverGrades() + { + scoreProcessor.ApplyBeatmap(new Beatmap()); + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X)); + + scoreProcessor.Mods.Value = new[] { new OsuModHidden() }; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); + } + + [Test] + public void TestSilverGradesModsAppliedFirst() + { + scoreProcessor.Mods.Value = new[] { new OsuModHidden() }; + scoreProcessor.ApplyBeatmap(new Beatmap()); + + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH)); + + scoreProcessor.Accuracy.Value = 0.99f; + Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); + } + private class TestJudgement : Judgement { public override HitResult MaxResult { get; } From 902a5436f3e2c2f330bcaae176d07f252f26e14a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 13:12:22 +0900 Subject: [PATCH 388/539] Fix silver S/SS not being awarded correctly --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 14fa928224..80e751422e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -186,16 +186,7 @@ namespace osu.Game.Rulesets.Scoring Ruleset = ruleset; Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); - Accuracy.ValueChanged += accuracy => - { - // Once failed, we shouldn't update the rank anymore. - if (rank.Value == ScoreRank.F) - return; - - rank.Value = RankFromAccuracy(accuracy.NewValue); - foreach (var mod in Mods.Value.OfType()) - rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); - }; + Accuracy.ValueChanged += _ => updateRank(); Mods.ValueChanged += mods => { @@ -205,6 +196,7 @@ namespace osu.Game.Rulesets.Scoring scoreMultiplier *= m.ScoreMultiplier; updateScore(); + updateRank(); }; } @@ -372,6 +364,17 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); } + private void updateRank() + { + // Once failed, we shouldn't update the rank anymore. + if (rank.Value == ScoreRank.F) + return; + + rank.Value = RankFromAccuracy(Accuracy.Value); + foreach (var mod in Mods.Value.OfType()) + rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); + } + protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) { return 500000 * Accuracy.Value * comboProgress + @@ -417,8 +420,8 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = 0; Accuracy.Value = 1; Combo.Value = 0; - rank.Value = ScoreRank.X; HighestCombo.Value = 0; + updateRank(); } /// From e75f113a06c4c4d9ec229857ed580d5747c1f143 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 14:14:04 +0900 Subject: [PATCH 389/539] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 07a638f04a..f33ddac66f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 34905b20527bf7adf618506975feab634f7632b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 14:17:21 +0900 Subject: [PATCH 390/539] Apply NRT to new classes --- .../ArgonHealthDisplayBackground.cs | 12 +++--------- .../ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs | 6 ++---- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs index a96c2f97bd..b486465cb0 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBackground.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -29,6 +27,8 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts { protected new ArgonHealthDisplayBackground Source => (ArgonHealthDisplayBackground)base.Source; + private IUniformBuffer? parametersBuffer; + public ArgonBarPathDrawNode(ArgonHealthDisplayBackground source) : base(source) { @@ -39,21 +39,15 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts public override void ApplyState() { base.ApplyState(); - size = Source.DrawSize; } - private IUniformBuffer parametersBuffer; - protected override void BindUniformResources(IShader shader, IRenderer renderer) { base.BindUniformResources(shader, renderer); parametersBuffer ??= renderer.CreateUniformBuffer(); - parametersBuffer.Data = new ArgonBarPathBackgroundParameters - { - Size = size - }; + parametersBuffer.Data = new ArgonBarPathBackgroundParameters { Size = size }; shader.BindUniformBlock("m_ArgonBarPathBackgroundParameters", parametersBuffer); } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs index 1938b97d5a..28e56183bf 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplayParts/ArgonHealthDisplayBar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.InteropServices; using osu.Framework.Allocation; @@ -107,6 +105,8 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts { protected new ArgonHealthDisplayBar Source => (ArgonHealthDisplayBar)base.Source; + private IUniformBuffer? parametersBuffer; + public ArgonBarPathDrawNode(ArgonHealthDisplayBar source) : base(source) { @@ -139,8 +139,6 @@ namespace osu.Game.Screens.Play.HUD.ArgonHealthDisplayParts base.Draw(renderer); } - private IUniformBuffer parametersBuffer; - protected override void BindUniformResources(IShader shader, IRenderer renderer) { base.BindUniformResources(shader, renderer); From 451ba1c86131e61245d4d3a9eb232cc47c32ff7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 15:12:23 +0900 Subject: [PATCH 391/539] Ensure `PresentGameplay` doesn't get stuck in loop if no beatmaps available --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index bedaf12c9b..40cd31934f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -136,10 +136,15 @@ namespace osu.Game.Overlays.SkinEditor globallyReenableBeatmapSkinSetting(); } - public void PresentGameplay() + public void PresentGameplay() => presentGameplay(false); + + private void presentGameplay(bool attemptedBeatmapSwitch) { performer?.PerformFromScreen(screen => { + if (State.Value != Visibility.Visible) + return; + if (beatmap.Value is DummyWorkingBeatmap) { // presume we don't have anything good to play and just bail. @@ -149,8 +154,12 @@ namespace osu.Game.Overlays.SkinEditor // If we're playing the intro, switch away to another beatmap. if (beatmap.Value.BeatmapSetInfo.Protected) { - music.NextTrack(); - Schedule(PresentGameplay); + if (!attemptedBeatmapSwitch) + { + music.NextTrack(); + Schedule(() => presentGameplay(true)); + } + return; } From e3ab7b1e9fd60ac15ae3e3e838425290512cf517 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 16 Jan 2024 17:22:22 +1100 Subject: [PATCH 392/539] Update disctop to use turnratio like stable --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 67a6d5e41a..d4a0f243e4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -135,7 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void Update() { base.Update(); - spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation; + + float turnRatio = spinningMiddle.Texture != null ? 0.5f : 1; + discTop.Rotation = DrawableSpinner.RotationTracker.Rotation * turnRatio; + spinningMiddle.Rotation = DrawableSpinner.RotationTracker.Rotation; + discBottom.Rotation = discTop.Rotation / 3; glow.Alpha = DrawableSpinner.Progress; From 0e41d0c9cf1cc46da66bd1c0d11dbc4091ea644a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 15:21:43 +0900 Subject: [PATCH 393/539] Add failing test coverage of skin getting nuked This doesn't fail in headless unfortunately. Run a few times manually to confirm. --- .../Navigation/TestSceneScreenNavigation.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8cb993eff2..f59fbc75ac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; @@ -834,6 +835,24 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } + [Test] + public void TestQuickSkinEditorDoesntNukeSkin() + { + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddStep("open", () => InputManager.Key(Key.Space)); + AddStep("skin", () => InputManager.Key(Key.E)); + AddStep("editor", () => InputManager.Key(Key.S)); + AddStep("and close immediately", () => InputManager.Key(Key.Escape)); + + AddStep("open again", () => InputManager.Key(Key.S)); + + Player player = null; + + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType().Any()); + } + [Test] public void TestTouchScreenDetectionAtSongSelect() { From cd02d00c0352eed86d4f2e5ec9aaf3bcd61a0102 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 15:23:07 +0900 Subject: [PATCH 394/539] Fix skin potentially being lost when opening and closing skin editor rapidly --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index f972186333..d3af928907 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.SkinEditor protected override bool StartHidden => true; - private Drawable targetScreen = null!; + private Drawable? targetScreen; private OsuTextFlowContainer headerText = null!; @@ -541,8 +541,14 @@ namespace osu.Game.Overlays.SkinEditor if (!hasBegunMutating) return; + if (targetScreen?.IsLoaded != true) + return; + SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); + if (!targetContainers.All(c => c.ComponentsLoaded)) + return; + foreach (var t in targetContainers) currentSkin.Value.UpdateDrawableTarget(t); From 57a6025a2c492c897531cbb56ae0bdaf136dd03c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 15:52:05 +0900 Subject: [PATCH 395/539] Add helper method to bypass judgement woes --- osu.Game/Rulesets/Judgements/JudgementResult.cs | 5 +++++ osu.Game/Rulesets/Scoring/HealthProcessor.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 1b915d52b7..b781a13929 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -94,6 +94,11 @@ namespace osu.Game.Rulesets.Judgements /// public bool IsHit => Type.IsHit(); + /// + /// The increase in health resulting from this judgement result. + /// + public double HealthIncrease => Judgement.HealthIncreaseFor(this); + /// /// Creates a new . /// diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 3e0b6433c2..b5eb755650 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The . /// The health increase. - protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result); + protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease; /// /// The default conditions for failing. From 2be8d66d4c75cee98d9aa0e5b60c99f475bb555c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 15:52:18 +0900 Subject: [PATCH 396/539] Fix argon health bar showing "miss" bar for bananas --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 6fc957874a..71996718d9 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -136,7 +136,12 @@ namespace osu.Game.Screens.Play.HUD BarHeight.BindValueChanged(_ => updateContentSize(), true); } - private void onNewJudgement(JudgementResult result) => pendingMissAnimation |= !result.IsHit; + private void onNewJudgement(JudgementResult result) + { + // Check the health increase because cases like osu!catch bananas fire `IgnoreMiss`, + // which counts as a miss but doesn't actually subtract any health. + pendingMissAnimation |= !result.IsHit && result.HealthIncrease < 0; + } protected override void Update() { From cdc6621f33f1365cb212e8663e0f948e7dc34bf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 16:37:05 +0900 Subject: [PATCH 397/539] Allow adjusting volume controls via a drag --- osu.Game/Overlays/Volume/VolumeMeter.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index d366f0bddb..6ad314c601 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -309,10 +309,24 @@ namespace osu.Game.Overlays.Volume private const double max_acceleration = 5; private const double acceleration_multiplier = 1.8; + private const float mouse_drag_divisor = 20; + private ScheduledDelegate accelerationDebounce; private void resetAcceleration() => accelerationModifier = 1; + protected override bool OnDragStart(DragStartEvent e) + { + adjust(-e.Delta.Y / mouse_drag_divisor, true); + return true; + } + + protected override void OnDrag(DragEvent e) + { + adjust(-e.Delta.Y / mouse_drag_divisor, true); + base.OnDrag(e); + } + private void adjust(double delta, bool isPrecise) { if (delta == 0) From ee26329353ec41e9a9859aad8fc7000760bfac66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 16:44:50 +0900 Subject: [PATCH 398/539] Fix some elements not showing on leaderboard scores when almost off-screen --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index a76f4ae955..1c2a26eafb 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -164,7 +164,8 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), Children = new Drawable[] From c45daa373e24070c78379c71c57d6c1ede6c6c31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jan 2024 17:15:25 +0900 Subject: [PATCH 399/539] Fix cursor scale animation not matching stable on classic skins --- .../Skinning/Argon/ArgonCursor.cs | 2 +- .../Skinning/Legacy/LegacyCursor.cs | 16 +++++++++- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 11 +++---- .../UI/Cursor/OsuCursorSprite.cs | 19 ------------ .../UI/Cursor/SkinnableCursor.cs | 31 +++++++++++++++++++ 5 files changed, 51 insertions(+), 28 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs create mode 100644 osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs index 4ca6abfdf7..15838f3e1b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs @@ -13,7 +13,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Argon { - public partial class ArgonCursor : OsuCursorSprite + public partial class ArgonCursor : SkinnableCursor { public ArgonCursor() { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs index b0c01d2925..375d81049d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs @@ -9,8 +9,11 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public partial class LegacyCursor : OsuCursorSprite + public partial class LegacyCursor : SkinnableCursor { + private const float pressed_scale = 1.3f; + private const float released_scale = 1f; + private readonly ISkin skin; private bool spin; @@ -51,5 +54,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (spin) ExpandTarget.Spin(10000, RotationDirection.Clockwise); } + + public override void Expand() + { + ExpandTarget?.ScaleTo(released_scale) + .ScaleTo(pressed_scale, 100, Easing.Out); + } + + public override void Contract() + { + ExpandTarget?.ScaleTo(released_scale, 100, Easing.Out); + } } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 710ca9ace7..d8f50c1f5d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -24,15 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public const float SIZE = 28; - private const float pressed_scale = 1.2f; - private const float released_scale = 1f; - private bool cursorExpand; private SkinnableDrawable cursorSprite; private Container cursorScaleContainer = null!; - private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite; + private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable; public IBindable CursorScale => cursorScale; @@ -108,10 +105,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { if (!cursorExpand) return; - expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf); + skinnableCursor.Expand(); } - public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad); + public void Contract() => skinnableCursor.Contract(); /// /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. @@ -119,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public static float GetScaleForCircleSize(float circleSize) => 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; - private partial class DefaultCursor : OsuCursorSprite + private partial class DefaultCursor : SkinnableCursor { public DefaultCursor() { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs deleted file mode 100644 index aaf8949084..0000000000 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Rulesets.Osu.UI.Cursor -{ - public abstract partial class OsuCursorSprite : CompositeDrawable - { - /// - /// The an optional piece of the cursor to expand when in a clicked state. - /// If null, the whole cursor will be affected by expansion. - /// - public Drawable ExpandTarget { get; protected set; } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs new file mode 100644 index 0000000000..09e6f989a4 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/Cursor/SkinnableCursor.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.UI.Cursor +{ + public abstract partial class SkinnableCursor : CompositeDrawable + { + private const float pressed_scale = 1.2f; + private const float released_scale = 1f; + + public virtual void Expand() + { + ExpandTarget?.ScaleTo(released_scale) + .ScaleTo(pressed_scale, 400, Easing.OutElasticHalf); + } + + public virtual void Contract() + { + ExpandTarget?.ScaleTo(released_scale, 400, Easing.OutQuad); + } + + /// + /// The an optional piece of the cursor to expand when in a clicked state. + /// If null, the whole cursor will be affected by expansion. + /// + public Drawable? ExpandTarget { get; protected set; } + } +} From 2935bd4809d1e071d8feb93604629fa94fb2cbbc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 16 Jan 2024 14:27:24 +0300 Subject: [PATCH 400/539] Add test cases for helper method --- osu.Game.Tests/Mods/ModUtilsTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 13da69871e..decb0a31ac 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -7,7 +7,9 @@ using Moq; using NUnit.Framework; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Utils; namespace osu.Game.Tests.Mods @@ -310,6 +312,16 @@ namespace osu.Game.Tests.Mods Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [Test] + public void TestModBelongsToRuleset() + { + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), Array.Empty())); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime() })); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new OsuModAccuracyChallenge() })); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new ModAccuracyChallenge() }), Is.False); + Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new TaikoModFlashlight() }), Is.False); + } + [Test] public void TestFormatScoreMultiplier() { From 90da39c65d252e157b8a550429e18c944cd65380 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 16 Jan 2024 14:27:30 +0300 Subject: [PATCH 401/539] Fix helper method returning incorrect result --- osu.Game/Utils/ModUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 7a9f93e06f..2c9eef41e3 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -255,7 +255,7 @@ namespace osu.Game.Utils } if (!found) - return true; + return false; } return true; From 485195d4c25d3b8f13f52f6da7faf72472a21b0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 16 Jan 2024 14:27:40 +0300 Subject: [PATCH 402/539] Fix submission test not asserting properly --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 833cb24f41..e2ce3a014c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -294,6 +294,7 @@ namespace osu.Game.Tests.Visual.Gameplay createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() }); AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + AddAssert("gameplay not loaded", () => Player.DrawableRuleset == null); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); From 7920e93fa91dbe14a610591cb4446a3c08d63819 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 16 Jan 2024 17:36:46 +0300 Subject: [PATCH 403/539] Rework GlowingSpriteText --- .../Graphics/Sprites/GlowingSpriteText.cs | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 1355bfc272..669c5da01e 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -5,95 +5,90 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Utils; using osuTK; namespace osu.Game.Graphics.Sprites { - public partial class GlowingSpriteText : Container, IHasText + public partial class GlowingSpriteText : BufferedContainer, IHasText { - private readonly OsuSpriteText spriteText, blurredText; + private const float blur_sigma = 3f; + + // Inflate draw quad to prevent glow from trimming at the edges. + // Padding won't suffice since it will affect text position in cases when it's not centered. + protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma)); + + private readonly OsuSpriteText text; public LocalisableString Text { - get => spriteText.Text; - set => blurredText.Text = spriteText.Text = value; + get => text.Text; + set => text.Text = value; } public FontUsage Font { - get => spriteText.Font; - set => blurredText.Font = spriteText.Font = value.With(fixedWidth: true); + get => text.Font; + set => text.Font = value.With(fixedWidth: true); } public Vector2 TextSize { - get => spriteText.Size; - set => blurredText.Size = spriteText.Size = value; + get => text.Size; + set => text.Size = value; } public ColourInfo TextColour { - get => spriteText.Colour; - set => spriteText.Colour = value; + get => text.Colour; + set => text.Colour = value; } public ColourInfo GlowColour { - get => blurredText.Colour; - set => blurredText.Colour = value; + get => EffectColour; + set + { + EffectColour = value; + BackgroundColour = value.MultiplyAlpha(0f); + } } public Vector2 Spacing { - get => spriteText.Spacing; - set => spriteText.Spacing = blurredText.Spacing = value; + get => text.Spacing; + set => text.Spacing = value; } public bool UseFullGlyphHeight { - get => spriteText.UseFullGlyphHeight; - set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value; + get => text.UseFullGlyphHeight; + set => text.UseFullGlyphHeight = value; } public Bindable Current { - get => spriteText.Current; - set => spriteText.Current = value; + get => text.Current; + set => text.Current = value; } public GlowingSpriteText() + : base(cachedFrameBuffer: true) { AutoSizeAxes = Axes.Both; - - Children = new Drawable[] + BlurSigma = new Vector2(blur_sigma); + RedrawOnScale = false; + DrawOriginal = true; + EffectBlending = BlendingParameters.Additive; + EffectPlacement = EffectPlacement.InFront; + Child = text = new OsuSpriteText { - new BufferedContainer(cachedFrameBuffer: true) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BlurSigma = new Vector2(4), - RedrawOnScale = false, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Size = new Vector2(3f), - Children = new[] - { - blurredText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - }, - }, - }, - spriteText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, }; } } From dfdf700560726d0f8bd87408b3cecd9b0e373f4c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 16 Jan 2024 17:39:37 +0300 Subject: [PATCH 404/539] Don't use glowing text in ScoreComponentLabel --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 1c2a26eafb..964f065813 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -358,14 +358,12 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new GlowingSpriteText + new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - TextColour = Color4.White, - GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) }, }, }; From 7a7548e89d91849992461c759a01a7e4f5cf97fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 17:09:04 +0100 Subject: [PATCH 405/539] Add failing test coverage --- .../Mods/TestSceneOsuModTouchDevice.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index cd51ccd751..5b5ad07fde 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods public void TestUserAlreadyHasTouchDeviceActive() { loadPlayer(); - // it is presumed that a previous screen (i.e. song select) will set this up AddStep("set up touchscreen user", () => { currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray(); @@ -69,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } + [Test] + public void TestTouchActivePriorToPlayerLoad() + { + AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true)); + loadPlayer(); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + [Test] public void TestTouchDuringBreak() { From a9d086c11954c618e12301eb762edf2df8931187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 17:12:40 +0100 Subject: [PATCH 406/539] Fix touch device not always activating when it should See https://github.com/ppy/osu/discussions/26574 --- osu.Game/Screens/Play/PlayerTouchInputDetector.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs index 69c3cd0ded..12fb748e7d 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -20,12 +20,16 @@ namespace osu.Game.Screens.Play private GameplayState gameplayState { get; set; } = null!; private IBindable touchActive = new BindableBool(); + private IBindable isBreakTime = null!; [BackgroundDependencyLoader] private void load(SessionStatics statics) { touchActive = statics.GetBindable(Static.TouchInputActive); touchActive.BindValueChanged(_ => updateState()); + + isBreakTime = player.IsBreakTime.GetBoundCopy(); + isBreakTime.BindValueChanged(_ => updateState(), true); } private void updateState() @@ -39,7 +43,7 @@ namespace osu.Game.Screens.Play if (gameplayState.Score.ScoreInfo.Mods.OfType().Any()) return; - if (player.IsBreakTime.Value) + if (isBreakTime.Value) return; var touchDeviceMod = gameplayState.Ruleset.GetTouchDeviceMod(); From b16152811d0266905db40187cfbaefe6ebe8c434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 17:54:49 +0100 Subject: [PATCH 407/539] Use until step instead --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index 5b5ad07fde..fdb1cac3e5 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true)); loadPlayer(); - AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } [Test] From e9e5a0b08cfd0fca03443cd0d25e00b14382934d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 18:41:49 +0100 Subject: [PATCH 408/539] Add failing test case --- .../Mods/TestSceneOsuModFlashlight.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index defbb97c8a..1a786b99e8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -77,5 +77,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, }); } + + [Test] + public void TestSliderDoesDimAfterStartTimeIfHitEarly() + { + bool sliderDimmed = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmed |= + Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && sliderDimmed; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } } } From d0e9402761a37a393d3048689ef72bb287f26237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 18:47:00 +0100 Subject: [PATCH 409/539] Fix flashlight not dimming if slider head is hit early Closes https://github.com/ppy/osu/issues/26551 Fix is a bit nuclear (`OnUpdate` should be considered last resort), but I don't see many better alternatives here as `ApplyCustomUpdateState` does not work... --- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 46c9bdd17f..5a6cc50082 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { if (drawable is DrawableSlider s) - s.Tracking.ValueChanged += _ => flashlight.OnSliderTrackingChange(s); + s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s); } private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition From 24741e96156ebaed5e1abd4f26c090b254d869ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 18:54:22 +0100 Subject: [PATCH 410/539] Add some more test coverage for good measure --- .../Mods/TestSceneOsuModFlashlight.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index 1a786b99e8..776d5854d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -116,5 +116,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, }); } + + [Test] + public void TestSliderDoesDimAfterStartTimeIfHitLate() + { + bool sliderDimmed = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModFlashlight(), + PassCondition = () => + { + sliderDimmed |= + Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType.Flashlight>().Single().FlashlightDim > 0; + return Player.GameplayState.HasPassed && sliderDimmed; + }, + Beatmap = new OsuBeatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(new[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + }) + } + }, + }, + ReplayFrames = new List + { + new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton), + new OsuReplayFrame(2001, new Vector2(100)), + }, + Autoplay = false, + }); + } } } From 265a56bede4c66e0632e6a6dad6cf6ed0d7bedd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 19:40:01 +0100 Subject: [PATCH 411/539] Add basic test coverage of taiko health processor --- .../TaikoHealthProcessorTest.cs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs new file mode 100644 index 0000000000..51dcf1ba91 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TaikoHealthProcessorTest + { + [Test] + public void TestHitsOnlyGreat() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Great }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestHitsAboveThreshold() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.GreaterThan(0.5)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestHitsBelowThreshold() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Hit { StartTime = 1000 }, + new Hit { StartTime = 2000 }, + new Hit { StartTime = 3000 }, + new Hit { StartTime = 4000 }, + } + }; + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok }); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.LessThan(0.5)); + Assert.That(healthProcessor.HasFailed, Is.True); + }); + } + } +} From 190f30a4b3c56927c312f6163b5f7920fd014adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 19:46:00 +0100 Subject: [PATCH 412/539] Add failing test cases --- .../TaikoHealthProcessorTest.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs index 51dcf1ba91..f4a1e888c9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs @@ -106,5 +106,71 @@ namespace osu.Game.Rulesets.Taiko.Tests Assert.That(healthProcessor.HasFailed, Is.True); }); } + + [Test] + public void TestDrumRollOnly() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new DrumRoll { Duration = 2000 } + } + }; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + foreach (var nested in beatmap.HitObjects[0].NestedHitObjects) + { + var nestedJudgement = nested.CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult }); + } + + var judgement = beatmap.HitObjects[0].CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } + + [Test] + public void TestSwellOnly() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new DrumRoll { Duration = 2000 } + } + }; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + foreach (var nested in beatmap.HitObjects[0].NestedHitObjects) + { + var nestedJudgement = nested.CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult }); + } + + var judgement = beatmap.HitObjects[0].CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(1)); + Assert.That(healthProcessor.HasFailed, Is.False); + }); + } } } From fc37c5e4c2bb4daa7a61714555a68b43930fc211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 20:18:32 +0100 Subject: [PATCH 413/539] Fix taiko maps containing only drum rolls / swells not being passable without mods Closes https://github.com/ppy/osu/issues/26370. As was noted in the issue thread stable does not attempt to account for such maps, and the maps are impassable in stable without No Fail. Nevertheless that seems like a pretty anti-player behaviour and I honestly believe that it is fine to change this in lazer. --- .../Scoring/TaikoHealthProcessor.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index d75906f3aa..dfca4ac8c7 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -31,11 +31,39 @@ namespace osu.Game.Rulesets.Taiko.Scoring /// private double hpMissMultiplier; + /// + /// Sum of all achievable health increases throughout the map. + /// Used to determine if there are any objects that give health. + /// If there are none, health will be forcibly pulled up to 1 to avoid cases of impassable maps. + /// + private double sumOfMaxHealthIncreases; + public TaikoHealthProcessor() : base(0.5) { } + protected override void ApplyResultInternal(JudgementResult result) + { + base.ApplyResultInternal(result); + sumOfMaxHealthIncreases += result.Judgement.MaxHealthIncrease; + } + + protected override void RevertResultInternal(JudgementResult result) + { + base.RevertResultInternal(result); + sumOfMaxHealthIncreases -= result.Judgement.MaxHealthIncrease; + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + if (storeResults && sumOfMaxHealthIncreases == 0) + Health.Value = 1; + sumOfMaxHealthIncreases = 0; + } + public override void ApplyBeatmap(IBeatmap beatmap) { base.ApplyBeatmap(beatmap); From 1075b87fb8bfb84ce3404bf93253940992e86671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 22:19:20 +0100 Subject: [PATCH 414/539] Add failing test coverage --- .../Formats/LegacyScoreDecoderTest.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 4e281cf28e..6e7c8c3631 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -3,14 +3,18 @@ #nullable disable +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Legacy; +using osu.Game.IO.Legacy; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -247,6 +251,123 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + [Test] + public void AccuracyAndRankOfStableScorePreserved() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)198); // count300 + sw.Write((ushort)1); // count100 + sw.Write((ushort)0); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)1); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + }); + } + + [Test] + public void AccuracyAndRankOfLazerScorePreserved() + { + var ruleset = new OsuRuleset().RulesetInfo; + + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + scoreInfo.Mods = new Mod[] { new OsuModFlashlight() }; + scoreInfo.Statistics = new Dictionary + { + [HitResult.Great] = 199, + [HitResult.Miss] = 1, + [HitResult.LargeTickHit] = 1, + }; + scoreInfo.MaximumStatistics = new Dictionary + { + [HitResult.Great] = 200, + [HitResult.LargeTickHit] = 1, + }; + + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + }; + + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + }); + } + + [Test] + public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestEffortFallbackToLegacy() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because we want to emulate an old lazer score here + // that does not have everything that `LegacyScoreEncoder` now writes to the replay + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(LegacyScoreEncoder.FIRST_LAZER_VERSION); // version + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)198); // count300 + sw.Write((ushort)0); // count100 + sw.Write((ushort)1); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)1); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + // importantly, no compressed `LegacyReplaySoloScoreInfo` here + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 50) / (200 * 300))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + }); + } + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) { var encodeStream = new MemoryStream(); From 17b9d842ab9a051ff6bc43d4ac5eb3d90dedfd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jan 2024 21:08:02 +0100 Subject: [PATCH 415/539] Fix incorrect accuracy and rank population when decoding lazer replays Closes https://github.com/ppy/osu/issues/24061. The gist of this change is that if the `LegacyReplaySoloScoreInfo` bolt-on is present in the replay, then it can (and is) used to recompute the accuracy, and rank is computed based on that. This was the missing part of https://github.com/ppy/osu/issues/24061#issuecomment-1888438151. The accuracy would change on import before that because the encode process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not used, as the legacy format only has 6 fields for encoding judgement counts, and some judgements that affect accuracy in lazer do not fit into that. Note that this _only_ fixes _relatively_ new lazer scores looking wrong after reimport. - Very old lazer scores, i.e. ones that don't have the `LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it to repopulate. There's really not much good that can be done there, so the stable pathways are used as a fallback that always works. - For stable replays, `ScoreImporter` recalculates the accuracy of the score _again_ in https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110 as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes _both_ total score and accuracy. This makes a _semblance_ of sense as it attempts to make the accuracy of stable and lazer replays comparable. In most cases it also won't matter, as the only ruleset where accuracy changed between the legacy implementation and current lazer accuracy is mania. But it is also an inaccurate process (as, again, some of the required data is not in the replay, namely judgement counts of ticks and so on). For whatever's worth, a similar thing happens server-side in https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319 - However, _ranks_ of stable scores will still use the local stable reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset will be an A rather than an S. See importer: https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237 (it's the same method which is renamed to `PopulateLegacyAccuracyAndRank()` in this commit). That is all a bit of a mess honestly, but I'm not sure where to even begin there... --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index ed11691674..b30fc7aee1 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; +using osu.Game.Database; using osu.Game.IO.Legacy; using osu.Game.Online.API.Requests.Responses; using osu.Game.Replays; @@ -37,6 +38,7 @@ namespace osu.Game.Scoring.Legacy }; WorkingBeatmap workingBeatmap; + byte[] compressedScoreInfo = null; using (SerializationReader sr = new SerializationReader(stream)) { @@ -105,8 +107,6 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.LegacyOnlineID = sr.ReadInt32(); - byte[] compressedScoreInfo = null; - if (version >= 30000001) compressedScoreInfo = sr.ReadByteArray(); @@ -130,7 +130,10 @@ namespace osu.Game.Scoring.Legacy } } - PopulateAccuracy(score.ScoreInfo); + if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) + PopulateLegacyAccuracyAndRank(score.ScoreInfo); + else + populateLazerAccuracyAndRank(score.ScoreInfo); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. @@ -174,7 +177,7 @@ namespace osu.Game.Scoring.Legacy /// Legacy use only. /// /// The to populate. - public static void PopulateAccuracy(ScoreInfo score) + public static void PopulateLegacyAccuracyAndRank(ScoreInfo score) { int countMiss = score.GetCountMiss() ?? 0; int count50 = score.GetCount50() ?? 0; @@ -273,6 +276,18 @@ namespace osu.Game.Scoring.Legacy } } + private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo) + { + scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); + + var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy); + + foreach (var mod in scoreInfo.Mods.OfType()) + rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + + scoreInfo.Rank = rank; + } + private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; From 9cde04c30c964a62d7e1e92c0c05f3726b8acf17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 14:12:12 +0900 Subject: [PATCH 416/539] Change drag adjustments to be linear (and account for partial deltas) --- osu.Game/Overlays/Volume/VolumeMeter.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 6ad314c601..a5b496b7d8 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -309,24 +309,37 @@ namespace osu.Game.Overlays.Volume private const double max_acceleration = 5; private const double acceleration_multiplier = 1.8; - private const float mouse_drag_divisor = 20; - private ScheduledDelegate accelerationDebounce; private void resetAcceleration() => accelerationModifier = 1; + private float dragDelta = 0; + protected override bool OnDragStart(DragStartEvent e) { - adjust(-e.Delta.Y / mouse_drag_divisor, true); + dragDelta = 0; + adjustFromDrag(e.Delta); return true; } protected override void OnDrag(DragEvent e) { - adjust(-e.Delta.Y / mouse_drag_divisor, true); + adjustFromDrag(e.Delta); base.OnDrag(e); } + private void adjustFromDrag(Vector2 delta) + { + const float mouse_drag_divisor = 200; + + dragDelta += delta.Y / mouse_drag_divisor; + + if (Math.Abs(dragDelta) < 0.01) return; + + Volume -= dragDelta; + dragDelta = 0; + } + private void adjust(double delta, bool isPrecise) { if (delta == 0) From f11682d44f769fe480ae1bfcfc7f324208fd49a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 08:10:02 +0300 Subject: [PATCH 417/539] Add failing test case --- .../Visual/Editing/TestSceneTimingScreen.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 40aadc8164..56c8c05fe6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -4,11 +4,13 @@ #nullable disable using System; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -177,6 +179,43 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType().First().IsScrolledToEnd()); } + [Test] + public void TestEditThenClickAway() + { + AddStep("Add two control points", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.ControlPointInfo.Add(1000, new TimingControlPoint()); + editorBeatmap.ControlPointInfo.Add(2000, new TimingControlPoint()); + }); + + AddStep("Select second timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("Scroll to end", () => timingScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("Modify time signature", () => + { + var timeSignatureTextBox = Child.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(timeSignatureTextBox); + InputManager.Click(MouseButton.Left); + + Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1")); + timeSignatureTextBox.Current.Value = "1"; + }); + + AddStep("Select first timing point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Second timing point changed time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.Last().TimeSignature.Numerator == 1); + AddAssert("First timing point preserved time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.First().TimeSignature.Numerator == 4); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; From 46429c5074b05447f8934dd25bd26fcce5e46dec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 08:10:37 +0300 Subject: [PATCH 418/539] Schedule control point switch for settings modifications to apply first --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 7a27056da3..219575a380 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -44,11 +44,12 @@ namespace osu.Game.Screens.Edit.Timing { BackgroundFlow.Add(new RowBackground(group) { - Action = () => + // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. + Action = () => Schedule(() => { SetSelectedRow(group); clock.SeekSmoothlyTo(group.Time); - } + }) }); } From a34d2a3424eec99dec0efb44ae91844348f46f22 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 08:28:47 +0300 Subject: [PATCH 419/539] Add small volume meter to test feel --- .../Visual/UserInterface/TestSceneVolumePieces.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs index 311bae0d50..b85e4c19d1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface { VolumeMeter meter; MuteButton mute; - Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) }); + Add(meter = new VolumeMeter("MASTER", 125, Color4.Green) { Position = new Vector2(10) }); AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); Add(new VolumeMeter("BIG", 250, Color4.Red) @@ -22,6 +22,15 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(10), + Margin = new MarginPadding { Left = 250 }, + }); + + Add(new VolumeMeter("SML", 125, Color4.Blue) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(10), + Margin = new MarginPadding { Right = 500 }, }); Add(mute = new MuteButton From 1790a5df03719e0483d4abede98460f4874bb65f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 08:29:55 +0300 Subject: [PATCH 420/539] Remove redundant value to appease CI --- osu.Game/Overlays/Volume/VolumeMeter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index a5b496b7d8..9ca4c25ab9 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -313,7 +313,7 @@ namespace osu.Game.Overlays.Volume private void resetAcceleration() => accelerationModifier = 1; - private float dragDelta = 0; + private float dragDelta; protected override bool OnDragStart(DragStartEvent e) { From 05729706eb2633b74dbb97d3e2615f85cd2721e5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 08:53:24 +0300 Subject: [PATCH 421/539] Fix android build CI failing No idea why this is only in android, but okay. --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 56c8c05fe6..f3d2b5d518 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.MoveMouseTo(timeSignatureTextBox); InputManager.Click(MouseButton.Left); - Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1")); + Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1", StringComparison.Ordinal)); timeSignatureTextBox.Current.Value = "1"; }); From 42f64c2c44f34bceb9fbbadb8a5cb6d2f8ed04aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 14:48:33 +0900 Subject: [PATCH 422/539] Move load procedure to async method and simplify code --- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 ++ osu.Game/Screens/Play/ReplayPlayer.cs | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index b7f3dc36c3..8c74b0254d 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -41,5 +41,7 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); + + public void Insert(int i, PlayerSettingsGroup drawable) => content.Insert(i, drawable); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index f6e4ac489a..39df223a9d 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -50,18 +51,19 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - if (HUDOverlay != null) + var playbackSettings = new PlaybackSettings { - var playerSettingsOverlay = new PlaybackSettings { Expanded = { Value = false } }; - HUDOverlay.PlayerSettingsOverlay.Add(playerSettingsOverlay); + Depth = float.MaxValue, + Expanded = { Value = false } + }; - if (GameplayClockContainer is MasterGameplayClockContainer master) - playerSettingsOverlay.UserPlaybackRate.BindTarget = master.UserPlaybackRate; - } + if (GameplayClockContainer is MasterGameplayClockContainer master) + playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); + + HUDOverlay.PlayerSettingsOverlay.Insert(-1, playbackSettings); } protected override void PrepareReplay() From 042e852a3ec027721a80817a7d3a9538df5904ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 14:53:49 +0900 Subject: [PATCH 423/539] Adjust playback speed range to allow slower minimum speed --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 8b8bf87436..1b7588d81a 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -35,9 +35,9 @@ namespace osu.Game.Screens.Play public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { - MinValue = 0.5, + MinValue = 0.05, MaxValue = 2, - Precision = 0.1, + Precision = 0.01, }; /// diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 0fe3a08985..2f37b8877f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -22,9 +22,9 @@ namespace osu.Game.Screens.Play.PlayerSettings public readonly Bindable UserPlaybackRate = new BindableDouble(1) { - MinValue = 0.5, + MinValue = 0.05, MaxValue = 2, - Precision = 0.1, + Precision = 0.01, }; private readonly PlayerSliderBar rateSlider; @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override void LoadComplete() { base.LoadComplete(); - rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); + rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true); if (gameplayClock != null) isPaused.BindTarget = gameplayClock.IsPaused; From e53989faebebb0bf323cac4a0f76e6bebd042939 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 14:54:14 +0900 Subject: [PATCH 424/539] Change replay playback adjustment to skew frequency, not tempo --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 1b7588d81a..1159e5f6d9 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -274,7 +274,7 @@ namespace osu.Game.Screens.Play track.BindAdjustments(AdjustmentsFromMods); track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.AddAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = true; } @@ -286,7 +286,7 @@ namespace osu.Game.Screens.Play track.UnbindAdjustments(AdjustmentsFromMods); track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); - track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.RemoveAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = false; } From 2788bd912e87068c370b27ff40333bda3c85139a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 15:12:33 +0900 Subject: [PATCH 425/539] Add tooltips and localisation --- .../PlayerSettingsOverlayStrings.cs | 24 +++++++++++++++++++ .../Play/PlayerSettings/PlaybackSettings.cs | 19 ++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Localisation/PlayerSettingsOverlayStrings.cs diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs new file mode 100644 index 0000000000..1aedd9fc5b --- /dev/null +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class PlayerSettingsOverlayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings"; + + /// + /// "Seek backward {0} seconds" + /// + public static LocalisableString SeekBackwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_backward_seconds"), @"Seek backward {0} seconds", arg0); + + /// + /// "Seek forward {0} seconds" + /// + public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 2f37b8877f..44cfa8d811 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Timing; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Play.PlayerSettings { @@ -71,6 +72,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, Action = () => seek(-1, seek_fast_amount), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000), }, new SeekButton { @@ -78,6 +80,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Backward, Action = () => seek(-1, seek_amount), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), }, play = new IconButton { @@ -103,6 +106,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, Action = () => seek(1, seek_amount), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000), }, new SeekButton { @@ -110,6 +114,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastForward, Action = () => seek(1, seek_fast_amount), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000), }, }, }, @@ -137,7 +142,19 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; - isPaused.BindValueChanged(e => play.Icon = !e.NewValue ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle, true); + isPaused.BindValueChanged(paused => + { + if (!paused.NewValue) + { + play.TooltipText = ToastStrings.PauseTrack; + play.Icon = FontAwesome.Regular.PauseCircle; + } + else + { + play.TooltipText = ToastStrings.PlayTrack; + play.Icon = FontAwesome.Regular.PlayCircle; + } + }, true); void seek(int direction, double amount) { From e7732caaf79a90c15354502ab307e8927373a46e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 15:13:59 +0900 Subject: [PATCH 426/539] Make `PlayerSettingsOverlay`'s api more stringent --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 8c74b0254d..a2b49f6302 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -42,6 +42,6 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); - public void Insert(int i, PlayerSettingsGroup drawable) => content.Insert(i, drawable); + public void AddAtStart(PlayerSettingsGroup drawable) => content.Insert(-1, drawable); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 39df223a9d..69913c6555 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play if (GameplayClockContainer is MasterGameplayClockContainer master) playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); - HUDOverlay.PlayerSettingsOverlay.Insert(-1, playbackSettings); + HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); } protected override void PrepareReplay() From 266c7b28e807978ad2e71e664afd454ce5c4dc3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 15:38:51 +0900 Subject: [PATCH 427/539] Show an empty baetmap instead of nothing --- osu.Game.Tournament/Components/SongBar.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index cc1d00f62f..ae59e92e33 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Screens.Menu; using osuTK; @@ -101,11 +102,25 @@ namespace osu.Game.Tournament.Components private void refreshContent() { - if (beatmap == null) + beatmap ??= new BeatmapInfo { - flow.Clear(); - return; - } + Metadata = new BeatmapMetadata + { + Artist = "unknown", + Title = "no beatmap selected", + Author = new RealmUser { Username = "unknown" }, + }, + DifficultyName = "unknown", + BeatmapSet = new BeatmapSetInfo(), + StarRating = 0, + Difficulty = new BeatmapDifficulty + { + CircleSize = 0, + DrainRate = 0, + OverallDifficulty = 0, + ApproachRate = 0, + }, + }; double bpm = beatmap.BPM; double length = beatmap.Length; From 06aa35a10e106997521389f26d59aadd1047c7d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 16:26:01 +0900 Subject: [PATCH 428/539] Fix tournament beatmap backgrounds occasionally not loading --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 4e0adb30ac..514ba482c4 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -194,7 +194,7 @@ namespace osu.Game.Tournament.Components // Use DelayedLoadWrapper to avoid content unloading when switching away to another screen. protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadWrapper(createContentFunc, timeBeforeLoad); + => new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad); } } } From e97b31d82e9aa6c9dace49742ff3c246d27c3368 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 16:40:45 +0900 Subject: [PATCH 429/539] Fix test failures --- osu.Game/Screens/Play/ReplayPlayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 69913c6555..805f907466 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -54,6 +54,9 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { + if (!LoadedBeatmapSuccessfully) + return; + var playbackSettings = new PlaybackSettings { Depth = float.MaxValue, From fe06402951ada9980fe5c02308cab9423bacfa20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 16:50:48 +0900 Subject: [PATCH 430/539] Fix incorrect bindable usage --- osu.Game/Overlays/Login/LoginPanel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 7db3644de6..ea65656eb1 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -38,6 +38,7 @@ namespace osu.Game.Overlays.Login public Action? RequestHide; private readonly IBindable apiState = new Bindable(); + private readonly Bindable userStatus = new Bindable(); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -139,7 +140,8 @@ namespace osu.Game.Overlays.Login }, }; - api.LocalUser.Value.Status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); + userStatus.BindTo(api.LocalUser.Value.Status); + userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); dropdown.Current.BindValueChanged(action => { From 66c7a29e794197478a597922bbc6c7028515b304 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 16:57:22 +0900 Subject: [PATCH 431/539] Add comment about messy methods --- osu.Game/Users/UserPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index ef5c95c422..b88619c8b7 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -98,6 +98,8 @@ namespace osu.Game.Users }; } + // TODO: this whole api is messy. half these Create methods are expected to by the implementation and half are implictly called. + protected abstract Drawable CreateLayout(); /// From 353df2f31227a71da752e40dbdcb650e288e889d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 17:00:30 +0900 Subject: [PATCH 432/539] Fix one more incorrect bindable flow and simplify string setters --- osu.Game/Users/UserRankPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index a2e234cf82..a38962dfc7 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; @@ -30,6 +30,8 @@ namespace osu.Game.Users private ProfileValueDisplay globalRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!; + private readonly IBindable statistics = new Bindable(); + public UserRankPanel(APIUser user) : base(user) { @@ -42,11 +44,12 @@ namespace osu.Game.Users { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; - api.Statistics.ValueChanged += e => + statistics.BindTo(api.Statistics); + statistics.BindValueChanged(stats => { - globalRankDisplay.Content = e.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - countryRankDisplay.Content = e.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - }; + globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + }, true); } protected override Drawable CreateLayout() @@ -184,12 +187,10 @@ namespace osu.Game.Users globalRankDisplay = new ProfileValueDisplay(true) { Title = UsersStrings.ShowRankGlobalSimple, - Content = User.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" }, countryRankDisplay = new ProfileValueDisplay(true) { Title = UsersStrings.ShowRankCountrySimple, - Content = User.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-" } } } From 45e52854ca50d31182845f3de3ca9b6bd87cf86d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 17:37:35 +0900 Subject: [PATCH 433/539] Change key overlay to use the ordering provided by rulesets osu!mania already goes out of its way to order things correctly. Arguably, osu!taiko just did it wrong. --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 35d05b87c0..041c7a13ae 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -167,7 +167,6 @@ namespace osu.Game.Rulesets.UI var triggers = KeyBindingContainer.DefaultKeyBindings .Select(b => b.GetAction()) .Distinct() - .OrderBy(action => action) .Select(action => new KeyCounterActionTrigger(action)) .ToArray(); From a4c0bfd099bb6a5140e1513cb5ac640c085082bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 17:37:20 +0900 Subject: [PATCH 434/539] Change taiko's default key ordering to better match display order Fixes the issue originally fixed via https://github.com/ppy/osu/pull/10553 in a slightly different way, allowing more flexibility. --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ad3b7b09f7..b80d8218f5 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -69,11 +69,11 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { - new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), - new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), new KeyBinding(InputKey.D, TaikoAction.LeftRim), new KeyBinding(InputKey.F, TaikoAction.LeftCentre), + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), new KeyBinding(InputKey.J, TaikoAction.RightCentre), + new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), new KeyBinding(InputKey.K, TaikoAction.RightRim), }; From 123e36a999e90a2698c52b03bb55c0054235d1a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 17:51:46 +0900 Subject: [PATCH 435/539] Change display text for beatmap audio offset adjust to make more sense --- osu.Game/Localisation/BeatmapOffsetControlStrings.cs | 4 ++-- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs index 632a1ad0ea..b905b7ae1c 100644 --- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl"; /// - /// "Beatmap offset" + /// "Audio offset (this beatmap)" /// - public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset"); + public static LocalisableString AudioOffsetThisBeatmap => new TranslatableString(getKey(@"beatmap_offset"), @"Audio offset (this beatmap)"); /// /// "Previous play:" diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 8efb80e771..7ac9c7186e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new OffsetSliderBar { KeyboardStep = 5, - LabelText = BeatmapOffsetControlStrings.BeatmapOffset, + LabelText = BeatmapOffsetControlStrings.AudioOffsetThisBeatmap, Current = Current, }, referenceScoreContainer = new FillFlowContainer From cc7be137bc22415dbc3637189a638fb70910f014 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 17:57:49 +0900 Subject: [PATCH 436/539] Show tooltip on global offset adjust slider bar --- .../Settings/Sections/Audio/AudioOffsetAdjustControl.cs | 9 ++++++++- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 90f5a59215..ef1691534f 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -12,12 +12,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Screens.Play.PlayerSettings; using osuTK; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -67,7 +69,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Direction = FillDirection.Vertical, Children = new Drawable[] { - new TimeSlider + new OffsetSliderBar { RelativeSizeAxes = Axes.X, Current = { BindTarget = Current }, @@ -157,6 +159,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio : $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms."; applySuggestion.Enabled.Value = SuggestedOffset.Value != null; } + + private partial class OffsetSliderBar : RoundedSliderBar + { + public override LocalisableString TooltipText => BeatmapOffsetControl.GetOffsetExplanatoryText(Current.Value); + } } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7ac9c7186e..9039604471 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -307,7 +307,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } } - public partial class OffsetSliderBar : PlayerSliderBar + private partial class OffsetSliderBar : PlayerSliderBar { protected override Drawable CreateControl() => new CustomSliderBar(); From a66ddc78136a877a697fec6d5232206db328439a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 18:18:53 +0900 Subject: [PATCH 437/539] Change rolling counters to use quicker easing types --- osu.Game/Graphics/UserInterface/PercentageCounter.cs | 2 +- osu.Game/Graphics/UserInterface/RollingCounter.cs | 2 +- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs | 2 +- osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs | 3 +-- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 3 +-- osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 3 +-- osu.Game/Screens/Play/HUD/BPMCounter.cs | 2 +- .../Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs | 2 +- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- osu.Game/Screens/Play/HUD/UnstableRateCounter.cs | 2 +- .../Ranking/Expanded/Statistics/AccuracyStatistic.cs | 7 ++++--- 13 files changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index ed06211e8f..1f9103b3bd 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -14,7 +14,7 @@ namespace osu.Game.Graphics.UserInterface /// public partial class PercentageCounter : RollingCounter { - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; private float epsilon => 1e-10f; diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index b80c0e3b58..e69727e047 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface /// /// Easing for the counter rollover animation. /// - protected virtual Easing RollingEasing => Easing.OutQuint; + protected virtual Easing RollingEasing => Easing.OutQuad; private T displayedCount; diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 94dd96ec1c..b9e4896b21 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -196,7 +196,7 @@ namespace osu.Game.Overlays.Mods private partial class BPMDisplay : RollingCounter { - protected override double RollingDuration => 500; + protected override double RollingDuration => 250; protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM"); diff --git a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs index b599b53082..a86eba81e4 100644 --- a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Mods private partial class EffectCounter : RollingCounter { - protected override double RollingDuration => 500; + protected override double RollingDuration => 250; protected override LocalisableString FormatCount(double count) => ModUtils.FormatScoreMultiplier(count); diff --git a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs index 33b7eaae1c..a3e24b486f 100644 --- a/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs +++ b/osu.Game/Overlays/Mods/VerticalAttributeDisplay.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Mods private partial class EffectCounter : RollingCounter { - protected override double RollingDuration => 500; + protected override double RollingDuration => 250; protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0#"); diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 171aa3f44b..ca00ab12c7 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -17,8 +17,7 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable { - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.OutQuint; + protected override double RollingDuration => 250; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 1d6ca3c893..369c753cb0 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -21,8 +21,7 @@ namespace osu.Game.Screens.Play.HUD { private ArgonCounterTextComponent text = null!; - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.OutQuint; + protected override double RollingDuration => 250; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index f7ca218767..348327d710 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -18,8 +18,7 @@ namespace osu.Game.Screens.Play.HUD { private ArgonScoreTextComponent scoreText = null!; - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.OutQuint; + protected override double RollingDuration => 250; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) diff --git a/osu.Game/Screens/Play/HUD/BPMCounter.cs b/osu.Game/Screens/Play/HUD/BPMCounter.cs index cd24237493..9cd285db4c 100644 --- a/osu.Game/Screens/Play/HUD/BPMCounter.cs +++ b/osu.Game/Screens/Play/HUD/BPMCounter.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.HUD { public partial class BPMCounter : RollingCounter, ISerialisableDrawable { - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; [Resolved] private IBindable beatmap { get; set; } = null!; diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 9b5ea309b0..a1cccdef0a 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond [Resolved] private ClicksPerSecondController controller { get; set; } = null!; - protected override double RollingDuration => 350; + protected override double RollingDuration => 175; public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 82f116b4ae..f041e120f6 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD protected override bool IsRollingProportional => true; - protected override double RollingDuration => 1000; + protected override double RollingDuration => 500; private const float alpha_when_invalid = 0.3f; diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 701b8a8732..ab7ab6b3a0 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - protected override double RollingDuration => 750; + protected override double RollingDuration => 375; private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 4b8c057235..f1f2c47e20 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -44,9 +44,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private partial class Counter : RollingCounter { - protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; - - protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + // FormatAccuracy doesn't round, which means if we use the OutPow10 easing the number will stick 0.01% short for some time. + // To avoid that let's use a shorter easing which looks roughly the same. + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION / 2; + protected override Easing RollingEasing => Easing.OutQuad; protected override LocalisableString FormatCount(double count) => count.FormatAccuracy(); From 5ad2918a75d59c24ecc1d8a66cf6d4292b259bbd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 11:21:44 +0300 Subject: [PATCH 438/539] Allow disabling high resolution texture lookups in `LegacySkin` --- .../Skinning/LegacySkinTextureFallbackTest.cs | 21 +++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 26 ++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index ca0d4d3cf3..3a7b9af0cb 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -127,8 +127,29 @@ namespace osu.Game.Tests.NonVisual.Skinning Assert.IsNull(texture); } + [Test] + public void TestDisallowHighResolutionSprites() + { + var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x"); + var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = false }; + + var texture = legacySkin.GetTexture("hitcircle"); + + Assert.IsNotNull(texture); + Assert.That(texture.ScaleAdjust, Is.EqualTo(1)); + + texture = legacySkin.GetTexture("hitcircle@2x"); + + Assert.IsNotNull(texture); + Assert.That(texture.ScaleAdjust, Is.EqualTo(1)); + } + private class TestLegacySkin : LegacySkin { + public bool HighResolutionSprites { get; set; } = true; + + protected override bool AllowHighResolutionSprites => HighResolutionSprites; + public TestLegacySkin(IResourceStore textureStore) : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 8f0cd59b68..10913e7369 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -477,6 +477,11 @@ namespace osu.Game.Skinning return null; } + /// + /// Whether high-resolution textures ("@2x"-suffixed) are allowed to be used by when available. + /// + protected virtual bool AllowHighResolutionSprites => true; + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { switch (componentName) @@ -488,15 +493,22 @@ namespace osu.Game.Skinning foreach (string name in getFallbackNames(componentName)) { - // some component names (especially user-controlled ones, like `HitX` in mania) - // may contain `@2x` scale specifications. - // stable happens to check for that and strip them, so do the same to match stable behaviour. - string lookupName = name.Replace(@"@2x", string.Empty); + string lookupName = name; + Texture? texture = null; + float ratio = 1; - float ratio = 2; - string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + if (AllowHighResolutionSprites) + { + // some component names (especially user-controlled ones, like `HitX` in mania) + // may contain `@2x` scale specifications. + // stable happens to check for that and strip them, so do the same to match stable behaviour. + lookupName = name.Replace(@"@2x", string.Empty); - var texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); + string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + + texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); + ratio = 2; + } if (texture == null) { From 98c65f36c9b81b352f4d2eb575eba0cc7fdf1ed3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 11:21:56 +0300 Subject: [PATCH 439/539] Disable high resolution texture lookup for legacy beatmap skins to match stable --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d6ba6ea332..9cd072b607 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -22,6 +22,11 @@ namespace osu.Game.Skinning protected override bool AllowManiaConfigLookups => false; protected override bool UseCustomSampleBanks => true; + // matches stable. references: + // 1. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L115-L137 (beatmap skin textures lookup) + // 2. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L158-L196 (user skin textures lookup) + protected override bool AllowHighResolutionSprites => false; + /// /// Construct a new legacy beatmap skin instance. /// From 1af5d1434c0abc615984fb64ef72d9d83d803740 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Jan 2024 19:53:23 +0900 Subject: [PATCH 440/539] Rename test method slightly --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index f3d2b5d518..6181024230 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -180,7 +180,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestEditThenClickAway() + public void TestEditThenClickAwayAppliesChanges() { AddStep("Add two control points", () => { From a8970d7642a6a2aa1834f4008ddc900997629dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 12:24:48 +0100 Subject: [PATCH 441/539] Jiggle ordering a bit further to fix revert-to-default buttons showing up --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index b80d8218f5..24b0ec5d57 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -69,11 +69,11 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { - new KeyBinding(InputKey.D, TaikoAction.LeftRim), - new KeyBinding(InputKey.F, TaikoAction.LeftCentre), - new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), - new KeyBinding(InputKey.J, TaikoAction.RightCentre), new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), + new KeyBinding(InputKey.D, TaikoAction.LeftRim), + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), + new KeyBinding(InputKey.F, TaikoAction.LeftCentre), + new KeyBinding(InputKey.J, TaikoAction.RightCentre), new KeyBinding(InputKey.K, TaikoAction.RightRim), }; From bcc7dd6af574ecbfb4004f5aaa6265f149578d86 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 15:58:27 +0300 Subject: [PATCH 442/539] Add extended test coverage --- .../Skinning/LegacySkinTextureFallbackTest.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 3a7b9af0cb..ff954211eb 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -138,10 +138,31 @@ namespace osu.Game.Tests.NonVisual.Skinning Assert.IsNotNull(texture); Assert.That(texture.ScaleAdjust, Is.EqualTo(1)); - texture = legacySkin.GetTexture("hitcircle@2x"); + var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x"); + + Assert.IsNotNull(twoTimesTexture); + Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(1)); + + Assert.AreNotEqual(texture, twoTimesTexture); + } + + [Test] + public void TestAllowHighResolutionSprites() + { + var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x"); + var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = true }; + + var texture = legacySkin.GetTexture("hitcircle"); Assert.IsNotNull(texture); - Assert.That(texture.ScaleAdjust, Is.EqualTo(1)); + Assert.That(texture.ScaleAdjust, Is.EqualTo(2)); + + var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x"); + + Assert.IsNotNull(twoTimesTexture); + Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(2)); + + Assert.AreEqual(texture, twoTimesTexture); } private class TestLegacySkin : LegacySkin From e54d20ea93514a017889ee560f92e06d7a28d37f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 13:27:44 +0300 Subject: [PATCH 443/539] Remove ancient osu-resources lookup path from legacy skin textures --- .../Skinning/Legacy/LegacyApproachCircle.cs | 2 +- .../Skinning/Legacy/LegacyReverseArrow.cs | 8 +-- .../Skinning/GameplaySkinComponentLookup.cs | 4 -- osu.Game/Skinning/LegacySkin.cs | 57 ++++++++----------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index eea6606233..d31c763c2c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableHitObject drawableObject { get; set; } = null!; public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) + : base(@"approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) { } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index 780084115d..ad1fb98aef 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -34,19 +34,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject, ISkinSource skinSource) { + const string lookup_name = @"reversearrow"; + drawableRepeat = (DrawableSliderRepeat)drawableObject; AutoSizeAxes = Axes.Both; - string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName; - - var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); + var skin = skinSource.FindProvider(s => s.GetTexture(lookup_name) != null); InternalChild = arrow = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), + Texture = skin?.GetTexture(lookup_name)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2), }; textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs index a44bf3a43d..ec159873f8 100644 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -28,8 +27,5 @@ namespace osu.Game.Skinning protected virtual string RulesetPrefix => string.Empty; protected virtual string ComponentName => Component.ToString(); - - public string LookupName => - string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 10913e7369..cfa5f972d2 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -491,39 +491,30 @@ namespace osu.Game.Skinning break; } - foreach (string name in getFallbackNames(componentName)) + Texture? texture = null; + float ratio = 1; + + if (AllowHighResolutionSprites) { - string lookupName = name; - Texture? texture = null; - float ratio = 1; + // some component names (especially user-controlled ones, like `HitX` in mania) + // may contain `@2x` scale specifications. + // stable happens to check for that and strip them, so do the same to match stable behaviour. + componentName = componentName.Replace(@"@2x", string.Empty); - if (AllowHighResolutionSprites) - { - // some component names (especially user-controlled ones, like `HitX` in mania) - // may contain `@2x` scale specifications. - // stable happens to check for that and strip them, so do the same to match stable behaviour. - lookupName = name.Replace(@"@2x", string.Empty); + string twoTimesFilename = $"{Path.ChangeExtension(componentName, null)}@2x{Path.GetExtension(componentName)}"; - string twoTimesFilename = $"{Path.ChangeExtension(lookupName, null)}@2x{Path.GetExtension(lookupName)}"; + texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); - texture = Textures?.Get(twoTimesFilename, wrapModeS, wrapModeT); + if (texture != null) ratio = 2; - } - - if (texture == null) - { - ratio = 1; - texture = Textures?.Get(lookupName, wrapModeS, wrapModeT); - } - - if (texture == null) - continue; - - texture.ScaleAdjust = ratio; - return texture; } - return null; + texture ??= Textures?.Get(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust = ratio; + + return texture; } public override ISample? GetSample(ISampleInfo sampleInfo) @@ -534,7 +525,7 @@ namespace osu.Game.Skinning lookupNames = getLegacyLookupNames(hitSample); else { - lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackNames); + lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackSampleNames); } foreach (string lookup in lookupNames) @@ -552,7 +543,7 @@ namespace osu.Game.Skinning private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) { - var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames); + var lookupNames = hitSample.LookupNames.SelectMany(getFallbackSampleNames); if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) { @@ -571,13 +562,13 @@ namespace osu.Game.Skinning yield return hitSample.Name; } - private IEnumerable getFallbackNames(string componentName) + private IEnumerable getFallbackSampleNames(string name) { - // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. - yield return componentName; + // May be something like "Gameplay/normal-hitnormal" from lazer. + yield return name; - // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). - yield return componentName.Split('/').Last(); + // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/normal-hitnormal" -> "normal-hitnormal"). + yield return name.Split('/').Last(); } } } From ed1e66b8f99032828b3e52a02b1baf5b10accaeb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 16:35:39 +0300 Subject: [PATCH 444/539] Fix enabling beatmap skin cause hitobjects to use `LegacyApproachCircle` regardless of selected skin --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index c01d28c8e1..86ca5fff8c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; @@ -163,7 +164,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.ApproachCircle: - return new LegacyApproachCircle(); + if (IsProvidingLegacyResources) + return new LegacyApproachCircle(); + + return null; default: throw new UnsupportedSkinComponentException(lookup); From ede0a2269329cde6afbbc82d185b664f17591327 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 16:58:47 +0300 Subject: [PATCH 445/539] Remove existing test coverage --- .../Skinning/LegacySkinTextureFallbackTest.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 3a7b9af0cb..89103cd4d9 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -56,24 +56,6 @@ namespace osu.Game.Tests.NonVisual.Skinning "Gameplay/osu/followpoint", 1 }, new object[] - { - new[] { "followpoint@2x", "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint@2x" }, - "Gameplay/osu/followpoint", - "followpoint@2x", 2 - }, - new object[] - { - new[] { "followpoint" }, - "Gameplay/osu/followpoint", - "followpoint", 1 - }, - new object[] { // Looking up a filename with extension specified should work. new[] { "followpoint.png" }, From eaa748f075948e0162db1b38503df2a4ed203ada Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 18:35:34 +0300 Subject: [PATCH 446/539] Remove unused using directive --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 86ca5fff8c..b01c18031c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; From 5597b48882b4ae4a00e3869b9bb42133db036ae2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jan 2024 16:38:36 +0300 Subject: [PATCH 447/539] Fix storyboard animations stripping path directory on skin lookup --- osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index cefd51b2aa..fae9ec7f2e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -129,7 +129,7 @@ namespace osu.Game.Storyboards.Drawables // When reading from a skin, we match stables weird behaviour where `FrameCount` is ignored // and resources are retrieved until the end of the animation. - var skinTextures = skin.GetTextures(Path.GetFileNameWithoutExtension(Animation.Path)!, default, default, true, string.Empty, null, out _); + var skinTextures = skin.GetTextures(Path.ChangeExtension(Animation.Path, null), default, default, true, string.Empty, null, out _); if (skinTextures.Length > 0) { From 42e4c933d374bc3ebc863ec38bc7951dd0498054 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 18 Jan 2024 04:06:02 +0300 Subject: [PATCH 448/539] Fix ConstrainedIconContainer always using masking --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 1 - .../Containers/ConstrainedIconContainer.cs | 18 ------------------ .../Toolbar/ToolbarRulesetTabButton.cs | 11 +---------- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 1665ec52fa..eecf79aa34 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -92,7 +92,6 @@ namespace osu.Game.Beatmaps.Drawables EdgeEffect = new EdgeEffectParameters { Colour = Color4.Black.Opacity(0.06f), - Type = EdgeEffectType.Shadow, Radius = 3, }, diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs index 7722374c69..63ac84fcf7 100644 --- a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osuTK; namespace osu.Game.Graphics.Containers @@ -17,21 +16,9 @@ namespace osu.Game.Graphics.Containers public Drawable Icon { get => InternalChild; - set => InternalChild = value; } - /// - /// Determines an edge effect of this . - /// Edge effects are e.g. glow or a shadow. - /// Only has an effect when is true. - /// - public new EdgeEffectParameters EdgeEffect - { - get => base.EdgeEffect; - set => base.EdgeEffect = value; - } - protected override void Update() { base.Update(); @@ -49,10 +36,5 @@ namespace osu.Game.Graphics.Containers InternalChild.Origin = Anchor.Centre; } } - - public ConstrainedIconContainer() - { - Masking = true; - } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index c224f0f9c9..3287ac6eaa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -58,15 +57,7 @@ namespace osu.Game.Overlays.Toolbar { set => Scheduler.AddOnce(() => { - if (value) - { - IconContainer.Colour = Color4Extensions.FromHex("#00FFAA"); - } - else - { - IconContainer.Colour = colours.GrayF; - IconContainer.EdgeEffect = new EdgeEffectParameters(); - } + IconContainer.Colour = value ? Color4Extensions.FromHex("#00FFAA") : colours.GrayF; }); } From c362a93a3688cd13b8befc11ca1a75a59d8172cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 02:18:20 +0900 Subject: [PATCH 449/539] Change frame stable catch-up method to allow for much faster sync --- .../Gameplay/TestSceneFrameStabilityContainer.cs | 4 +--- .../Visual/Gameplay/TestSceneSongProgress.cs | 5 +---- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 10 ++++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 534348bed3..98a97e1d23 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -129,10 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay checkRate(1); } - private const int max_frames_catchup = 50; - private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => - mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) { MaxCatchUpFrames = max_frames_catchup } + mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) .WithChild(consumer = new ClockConsumingChild())); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index e975a85401..19bb5cdde1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -45,10 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay }, gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) { - Child = frameStabilityContainer = new FrameStabilityContainer - { - MaxCatchUpFrames = 1 - } + Child = frameStabilityContainer = new FrameStabilityContainer() } }); diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 2af9916a6b..c07bd3aef5 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.UI public ReplayInputHandler? ReplayInputHandler { get; set; } /// - /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. + /// The number of CPU milliseconds to spend at most during seek catch-up. /// - public int MaxCatchUpFrames { get; set; } = 5; + private const double max_catchup_milliseconds = 10; /// /// Whether to enable frame-stable playback. @@ -61,6 +61,8 @@ namespace osu.Game.Rulesets.UI /// private readonly FramedClock framedClock; + private readonly Stopwatch stopwatch = new Stopwatch(); + /// /// The current direction of playback to be exposed to frame stable children. /// @@ -99,7 +101,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - int loops = MaxCatchUpFrames; + stopwatch.Restart(); do { @@ -112,7 +114,7 @@ namespace osu.Game.Rulesets.UI base.UpdateSubTree(); UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); - } while (state == PlaybackState.RequiresCatchUp && loops-- > 0); + } while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds); return true; } From fb4efd992de542ae5ad57ce192d9626de692e8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 11:52:28 +0900 Subject: [PATCH 450/539] Add a fake load to visual test to restore previous testing behaviour --- .../Visual/Gameplay/TestSceneSongProgress.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 19bb5cdde1..99f0ffb9d0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -45,7 +46,10 @@ namespace osu.Game.Tests.Visual.Gameplay }, gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) { - Child = frameStabilityContainer = new FrameStabilityContainer() + Child = frameStabilityContainer = new FrameStabilityContainer + { + Child = new FakeLoad() + } } }); @@ -53,6 +57,15 @@ namespace osu.Game.Tests.Visual.Gameplay Dependencies.CacheAs(frameStabilityContainer); } + private partial class FakeLoad : Drawable + { + protected override void Update() + { + base.Update(); + Thread.Sleep(1); + } + } + [SetUpSteps] public void SetupSteps() { From 799c74cfe533da28509699dd2feea84179a2b8f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 02:40:34 +0900 Subject: [PATCH 451/539] Simplify gameplay pause sequence --- .../Visual/Gameplay/TestScenePause.cs | 5 +- .../Play/MasterGameplayClockContainer.cs | 66 ------------------- 2 files changed, 1 insertion(+), 70 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ec3b3e0822..d55af2777f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -93,15 +93,12 @@ namespace osu.Game.Tests.Visual.Gameplay double currentTime = masterClock.CurrentTime; - bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime); + bool goingForward = currentTime >= lastStopTime; alwaysGoingForward &= goingForward; if (!goingForward) Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); - - if (masterClock.LastStopTime != null) - lastStopTime = masterClock.LastStopTime.Value; }; }); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 12f541167f..10451963a1 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -60,17 +59,6 @@ namespace osu.Game.Screens.Play private readonly double skipTargetTime; - /// - /// Stores the time at which the last call was triggered. - /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp. - /// - /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled - /// to avoid fails occurring after the pause screen has been shown. - /// - /// In the future I want to change this. - /// - internal double? LastStopTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -113,71 +101,17 @@ namespace osu.Game.Screens.Play return time; } - protected override void StopGameplayClock() - { - LastStopTime = GameplayClock.CurrentTime; - - if (IsLoaded) - { - // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => - { - if (IsPaused.Value) - base.StopGameplayClock(); - }); - } - else - { - base.StopGameplayClock(); - - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 0; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } - } - public override void Seek(double time) { - // Safety in case the clock is seeked while stopped. - LastStopTime = null; elapsedValidationTime = null; base.Seek(time); } - protected override void PrepareStart() - { - if (LastStopTime != null) - { - Seek(LastStopTime.Value); - LastStopTime = null; - } - else - base.PrepareStart(); - } - protected override void StartGameplayClock() { addAdjustmentsToTrack(); - base.StartGameplayClock(); - - if (IsLoaded) - { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); - } - else - { - // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = 1; - - // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. - // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); - } } /// From 8ab8c90e338f00c784e05fc406540b872cefa5a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 14:21:02 +0900 Subject: [PATCH 452/539] Remove "pause rate adjust" flow --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 11 +++-------- .../Screens/Play/MasterGameplayClockContainer.cs | 2 -- osu.Game/Screens/Play/OffsetCorrectionClock.cs | 14 +++----------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 587e6bbeed..d0ffbdd459 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -27,11 +27,6 @@ namespace osu.Game.Beatmaps { private readonly bool applyOffsets; - /// - /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. - /// - public readonly BindableDouble ExternalPauseFrequencyAdjust = new BindableDouble(1); - private readonly OffsetCorrectionClock? userGlobalOffsetClock; private readonly OffsetCorrectionClock? platformOffsetClock; private readonly OffsetCorrectionClock? userBeatmapOffsetClock; @@ -69,13 +64,13 @@ namespace osu.Game.Beatmaps { // Audio timings in general with newer BASS versions don't match stable. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock); // User per-beatmap offset will be applied to this final clock. - finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, ExternalPauseFrequencyAdjust); + finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock); } else { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 10451963a1..93bdcb1cab 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -207,7 +207,6 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); track.BindAdjustments(AdjustmentsFromMods); - track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.AddAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = true; @@ -219,7 +218,6 @@ namespace osu.Game.Screens.Play return; track.UnbindAdjustments(AdjustmentsFromMods); - track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.RemoveAdjustment(AdjustableProperty.Frequency, UserPlaybackRate); speedAdjustmentsApplied = false; diff --git a/osu.Game/Screens/Play/OffsetCorrectionClock.cs b/osu.Game/Screens/Play/OffsetCorrectionClock.cs index 207980f45c..e83ed7e464 100644 --- a/osu.Game/Screens/Play/OffsetCorrectionClock.cs +++ b/osu.Game/Screens/Play/OffsetCorrectionClock.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Timing; namespace osu.Game.Screens.Play { public class OffsetCorrectionClock : FramedOffsetClock { - private readonly BindableDouble pauseRateAdjust; - private double offset; public new double Offset @@ -28,10 +25,9 @@ namespace osu.Game.Screens.Play public double RateAdjustedOffset => base.Offset; - public OffsetCorrectionClock(IClock source, BindableDouble pauseRateAdjust) + public OffsetCorrectionClock(IClock source) : base(source) { - this.pauseRateAdjust = pauseRateAdjust; } public override void ProcessFrame() @@ -42,12 +38,8 @@ namespace osu.Game.Screens.Play private void updateOffset() { - // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. - if (pauseRateAdjust.Value == 1) - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - base.Offset = Offset * Rate; - } + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + base.Offset = Offset * Rate; } } } From a870f99877fa3758307d33fae01e46f5f067af3b Mon Sep 17 00:00:00 2001 From: mouzedrift <43358824+mouzedrift@users.noreply.github.com> Date: Thu, 18 Jan 2024 07:19:29 +0100 Subject: [PATCH 453/539] ease out input drum animation and add delay before fading out --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs index 8ad419f8bd..d9e94ede4a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs @@ -159,8 +159,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const float up_time = 50; target.Animate( - t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time) - ).Then( + t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.Out) + ).Delay(100).Then( t => t.FadeOut(up_time) ); } From ee3de9c522685b5f85eeb9c021f9c6eab8b9d9a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 15:44:27 +0900 Subject: [PATCH 454/539] Fix gameplay elements not correcly offsetting to avoid per-ruleset skin layout Closes https://github.com/ppy/osu/issues/26601. --- osu.Game/Screens/Play/HUDOverlay.cs | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 128f8d5ffd..a9fe393395 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -99,6 +99,9 @@ namespace osu.Game.Screens.Play private readonly SkinComponentsContainer mainComponents; + [CanBeNull] + private readonly SkinComponentsContainer rulesetComponents; + /// /// A flow which sits at the left side of the screen to house leaderboard (and related) components. /// Will automatically be positioned to avoid colliding with top scoring elements. @@ -111,7 +114,6 @@ namespace osu.Game.Screens.Play public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { - Drawable rulesetComponents; this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -125,8 +127,8 @@ namespace osu.Game.Screens.Play clicksPerSecondController = new ClicksPerSecondController(), InputCountController = new InputCountController(), mainComponents = new HUDComponentsContainer { AlwaysPresent = true, }, - rulesetComponents = drawableRuleset != null - ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } + drawableRuleset != null + ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), playfieldComponents = drawableRuleset != null ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } @@ -256,13 +258,37 @@ namespace osu.Game.Screens.Play // LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. foreach (var element in mainComponents.Components.Cast()) + processDrawable(element); + + if (rulesetComponents != null) + { + foreach (var element in rulesetComponents.Components.Cast()) + processDrawable(element); + } + + if (lowestTopScreenSpaceRight.HasValue) + topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); + else + topRightElements.Y = 0; + + if (lowestTopScreenSpaceLeft.HasValue) + LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); + else + LeaderboardFlow.Y = 0; + + if (highestBottomScreenSpace.HasValue) + bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + else + bottomRightElements.Y = 0; + + void processDrawable(Drawable element) { // for now align some top components with the bottom-edge of the lowest top-anchored hud element. if (element.Anchor.HasFlagFast(Anchor.y0)) { // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. if (element is LegacyHealthDisplay) - continue; + return; float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y; @@ -288,21 +314,6 @@ namespace osu.Game.Screens.Play highestBottomScreenSpace = topLeft; } } - - if (lowestTopScreenSpaceRight.HasValue) - topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); - else - topRightElements.Y = 0; - - if (lowestTopScreenSpaceLeft.HasValue) - LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); - else - LeaderboardFlow.Y = 0; - - if (highestBottomScreenSpace.HasValue) - bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); - else - bottomRightElements.Y = 0; } private void updateVisibility() From cbfc6091af4265462e89016c3dc6210c58cff2d8 Mon Sep 17 00:00:00 2001 From: mouzedrift <43358824+mouzedrift@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:22:26 +0100 Subject: [PATCH 455/539] apply proposed changes --- .../Skinning/Legacy/LegacyInputDrum.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs index d9e94ede4a..28415bb72a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs @@ -1,7 +1,6 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -153,16 +152,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (target != null) { - const float alpha_amount = 1; - const float down_time = 80; const float up_time = 50; - target.Animate( - t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.Out) - ).Delay(100).Then( - t => t.FadeOut(up_time) - ); + target + .FadeTo(1, down_time * (1 - target.Alpha), Easing.Out) + .Delay(100).FadeOut(up_time); } return false; From ef0b5c94064654f798727ad0dbc12f919e6284d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 16:31:20 +0900 Subject: [PATCH 456/539] Stop playing back samples for nested screens in multiplayer I'd argue this sounds better. --- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index b527bf98a2..ea855c0474 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; + protected override bool PlayExitSound => false; + [Resolved] protected IRoomManager? RoomManager { get; private set; } From fd9c7184e449ffeeadeb3768e80bdb059bcaa92c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 16:31:33 +0900 Subject: [PATCH 457/539] Only play back sample once when traversing up multiple screens in stack --- osu.Game/Screens/OsuScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 490a1ae6b8..f719ef67c9 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -223,7 +223,12 @@ namespace osu.Game.Screens public override bool OnExiting(ScreenExitEvent e) { - if (ValidForResume && PlayExitSound) + // Only play the exit sound if we are the last screen in the exit sequence. + // This stops many sample playbacks from stacking when a huge screen purge happens (ie. returning to menu via the home button + // from a deeply nested screen). + bool arrivingAtFinalDestination = e.Next == e.Destination; + + if (ValidForResume && PlayExitSound && arrivingAtFinalDestination) sampleExit?.Play(); if (ValidForResume && logo != null) From 1527ab89ef1dee1994f4629a5ebbcbcf2c9edc8a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jan 2024 10:34:23 +0300 Subject: [PATCH 458/539] Refactor `DefaultApproachCircle`/`LegacyApproachCircle` to make sense --- .../Skinning/Default/DefaultApproachCircle.cs | 34 +++++++----------- .../Skinning/Legacy/LegacyApproachCircle.cs | 36 ++++++++----------- .../Legacy/OsuLegacySkinTransformer.cs | 2 +- 3 files changed, 28 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs index b65f46c414..272f4b5658 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs @@ -3,47 +3,39 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public partial class DefaultApproachCircle : SkinnableSprite + public partial class DefaultApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public DefaultApproachCircle() - : base("Gameplay/osu/approachcircle") - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - accentColour.BindTo(drawableObject.AccentColour); + Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); - accentColour.BindValueChanged(colour => Colour = colour.NewValue, true); - } - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // Although this is a non-legacy component, osu-resources currently stores approach circle as a legacy-like texture. - // See LegacyApproachCircle for documentation as to why this is required. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; + accentColour = drawableObject.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index d31c763c2c..0bdea0cab1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; @@ -12,40 +13,31 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - // todo: this should probably not be a SkinnableSprite, as this is always created for legacy skins and is recreated on skin change. - public partial class LegacyApproachCircle : SkinnableSprite + public partial class LegacyApproachCircle : Sprite { - private readonly IBindable accentColour = new Bindable(); - [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; - public LegacyApproachCircle() - : base(@"approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) - { - } + private IBindable accentColour = null!; [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { - accentColour.BindTo(drawableObject.AccentColour); + var texture = skin.GetTexture(@"approachcircle"); + Debug.Assert(texture != null); + Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + Scale = new Vector2(128 / 118f); } protected override void LoadComplete() { base.LoadComplete(); + + accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } - - protected override Drawable CreateDefault(ISkinComponentLookup lookup) - { - var drawable = base.CreateDefault(lookup); - - // account for the sprite being used for the default approach circle being taken from stable, - // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. - drawable.Scale = new Vector2(128 / 118f); - - return drawable; - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index b01c18031c..d2ebc68c52 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.ApproachCircle: - if (IsProvidingLegacyResources) + if (GetTexture(@"approachcircle") != null) return new LegacyApproachCircle(); return null; From 82e7643df55323a7e66fb9dc3351becf3357c469 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 19:45:35 +0900 Subject: [PATCH 459/539] Update IPC usages Of note, I've disabled IPC on visual test runners as we generally don't use IPC in these cases. Having it set means that the game will not open while visual tests are open, which has been a complaint from devs in the past. --- .../osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs | 2 +- .../osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs | 2 +- osu.Desktop/Program.cs | 2 +- .../Visual/Navigation/TestSceneInterProcessCommunication.cs | 2 +- osu.Game.Tournament.Tests/TournamentTestRunner.cs | 2 +- osu.Game/OsuGame.cs | 2 ++ osu.Game/Tests/CleanRunHeadlessGameHost.cs | 2 +- osu.Game/Tests/VisualTestRunner.cs | 2 +- 10 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs index 03ee7c9204..63c481a623 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs index b45505678c..5beb6616a7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index 55c0cf6a3b..c44cbb845b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu")) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6b95a82703..a7453dc0e0 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -102,7 +102,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null })) { if (!host.IsPrimaryInstance) { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs index 1ecd38e1d3..83430b5665 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("create IPC sender channels", () => { - ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true }); + ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPort = OsuGame.IPC_PORT }); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost); }); diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 5f642b14f5..e09d1be22c 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0d6eb1ea44..7138bf2175 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -80,6 +80,8 @@ namespace osu.Game [Cached(typeof(OsuGame))] public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { + public const int IPC_PORT = 44823; + /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). /// diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index f3c69201e2..00e5b38b1a 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests [CallerMemberName] string callingMethodName = @"") : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions { - BindIPC = bindIPC, + IPCPort = bindIPC ? OsuGame.IPC_PORT : null, }, bypassCleanup: bypassCleanupOnDispose, realtime: realtime) { this.bypassCleanupOnSetup = bypassCleanupOnSetup; diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index e04c71d193..1a9e03b2a4 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development")) { host.Run(new OsuTestBrowser()); return 0; From c4e9bcd140bbb59a094c282fc7b21ac04e8ec5d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 20:06:53 +0900 Subject: [PATCH 460/539] Remove test guarantee of audio time not advancing --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index d55af2777f..73aa3be73d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); - AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime)); + AddAssert("continued playing forward", () => Player.LastResumeTime, () => Is.GreaterThanOrEqualTo(Player.LastPauseTime)); AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 4def1d36bb..c039d1e535 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -78,8 +78,6 @@ namespace osu.Game.Screens.Play isPaused.Value = false; - PrepareStart(); - // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), // this means that the first frame ever exposed to children may have a non-zero current time. @@ -99,14 +97,6 @@ namespace osu.Game.Screens.Play }); } - /// - /// When is called, this will be run to give an opportunity to prepare the clock at the correct - /// start location. - /// - protected virtual void PrepareStart() - { - } - /// /// Seek to a specific time in gameplay. /// From 4532a409d23bc0cb2fffbecda32991c5278c4ea8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 18 Jan 2024 04:06:02 +0300 Subject: [PATCH 461/539] Fix ConstrainedIconContainer always using masking --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 1 - .../Containers/ConstrainedIconContainer.cs | 18 ------------------ .../Toolbar/ToolbarRulesetTabButton.cs | 11 +---------- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 1665ec52fa..eecf79aa34 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -92,7 +92,6 @@ namespace osu.Game.Beatmaps.Drawables EdgeEffect = new EdgeEffectParameters { Colour = Color4.Black.Opacity(0.06f), - Type = EdgeEffectType.Shadow, Radius = 3, }, diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs index 7722374c69..63ac84fcf7 100644 --- a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osuTK; namespace osu.Game.Graphics.Containers @@ -17,21 +16,9 @@ namespace osu.Game.Graphics.Containers public Drawable Icon { get => InternalChild; - set => InternalChild = value; } - /// - /// Determines an edge effect of this . - /// Edge effects are e.g. glow or a shadow. - /// Only has an effect when is true. - /// - public new EdgeEffectParameters EdgeEffect - { - get => base.EdgeEffect; - set => base.EdgeEffect = value; - } - protected override void Update() { base.Update(); @@ -49,10 +36,5 @@ namespace osu.Game.Graphics.Containers InternalChild.Origin = Anchor.Centre; } } - - public ConstrainedIconContainer() - { - Masking = true; - } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index c224f0f9c9..3287ac6eaa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -58,15 +57,7 @@ namespace osu.Game.Overlays.Toolbar { set => Scheduler.AddOnce(() => { - if (value) - { - IconContainer.Colour = Color4Extensions.FromHex("#00FFAA"); - } - else - { - IconContainer.Colour = colours.GrayF; - IconContainer.EdgeEffect = new EdgeEffectParameters(); - } + IconContainer.Colour = value ? Color4Extensions.FromHex("#00FFAA") : colours.GrayF; }); } From 2afa4c7e1c761ce998199e39e86cd5d1e9228f49 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 14:10:16 +0900 Subject: [PATCH 462/539] Remove redundant `RequiresChildrenUpdate` usage We are already manually calling `base.UpdateSubTree` when we need to. Changing this flag is doing nothing and just adds to the complexity of the implementation. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 2af9916a6b..b6bf5ff53a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback { get; set; } = true; - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - private readonly Bindable isCatchingUp = new Bindable(); private readonly Bindable waitingOnFrames = new Bindable(); From e73910571f35e69e409cec18cc597d232829bfa5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 14:15:22 +0900 Subject: [PATCH 463/539] Allow `FrameStabilityContainer` to continue updating while paused during replay playback --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index b6bf5ff53a..ee19baaf65 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.UI // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; } - else if (IsPaused.Value) + else if (IsPaused.Value && !hasReplayAttached) { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; From dafff26d0ecf6de7f27bb5a329455729ee3ce084 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 14:44:31 +0900 Subject: [PATCH 464/539] Tidy up implementation of `PlaybackSettings` --- .../Play/PlayerSettings/PlaybackSettings.cs | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 44cfa8d811..34f086da33 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -11,9 +11,9 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Screens.Edit.Timing; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.Play.PlayerSettings { @@ -28,26 +28,31 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.01, }; - private readonly PlayerSliderBar rateSlider; + private PlayerSliderBar rateSlider = null!; - private readonly OsuSpriteText multiplierText; + private OsuSpriteText multiplierText = null!; - private readonly BindableBool isPaused = new BindableBool(); + private readonly IBindable isPaused = new BindableBool(); [Resolved] - private GameplayClockContainer? gameplayClock { get; set; } + private GameplayClockContainer gameplayClock { get; set; } = null!; [Resolved] - private GameplayState? gameplayState { get; set; } + private GameplayState gameplayState { get; set; } = null!; + + private IconButton pausePlay = null!; public PlaybackSettings() : base("playback") + { + } + + [BackgroundDependencyLoader] + private void load() { const double seek_amount = 5000; const double seek_fast_amount = 10000; - IconButton play; - Children = new Drawable[] { new FillFlowContainer @@ -82,7 +87,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Action = () => seek(-1, seek_amount), TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), }, - play = new IconButton + pausePlay = new IconButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -91,13 +96,10 @@ namespace osu.Game.Screens.Play.PlayerSettings Icon = FontAwesome.Regular.PlayCircle, Action = () => { - if (gameplayClock != null) - { - if (gameplayClock.IsRunning) - gameplayClock.Stop(); - else - gameplayClock.Start(); - } + if (gameplayClock.IsRunning) + gameplayClock.Stop(); + else + gameplayClock.Start(); }, }, new SeekButton @@ -141,26 +143,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }, }; - - isPaused.BindValueChanged(paused => - { - if (!paused.NewValue) - { - play.TooltipText = ToastStrings.PauseTrack; - play.Icon = FontAwesome.Regular.PauseCircle; - } - else - { - play.TooltipText = ToastStrings.PlayTrack; - play.Icon = FontAwesome.Regular.PlayCircle; - } - }, true); - - void seek(int direction, double amount) - { - double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0); - gameplayClock?.Seek(target); - } } protected override void LoadComplete() @@ -168,8 +150,26 @@ namespace osu.Game.Screens.Play.PlayerSettings base.LoadComplete(); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true); - if (gameplayClock != null) - isPaused.BindTarget = gameplayClock.IsPaused; + isPaused.BindTo(gameplayClock.IsPaused); + isPaused.BindValueChanged(paused => + { + if (!paused.NewValue) + { + pausePlay.TooltipText = ToastStrings.PauseTrack; + pausePlay.Icon = FontAwesome.Regular.PauseCircle; + } + else + { + pausePlay.TooltipText = ToastStrings.PlayTrack; + pausePlay.Icon = FontAwesome.Regular.PlayCircle; + } + }, true); + } + + private void seek(int direction, double amount) + { + double target = Math.Clamp(gameplayClock.CurrentTime + (direction * amount), 0, gameplayState.Beatmap.GetLastObjectTime()); + gameplayClock.Seek(target); } private partial class SeekButton : IconButton From 60e972cd159dcfe653ed19cc2d96ed179701205d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 14:59:35 +0900 Subject: [PATCH 465/539] Add ability to step forward/backwards single frames --- .../PlayerSettingsOverlayStrings.cs | 10 ++++++ .../Play/PlayerSettings/PlaybackSettings.cs | 33 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 1aedd9fc5b..60874da561 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -9,6 +9,16 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings"; + /// + /// "Step backward one frame" + /// + public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame"); + + /// + /// "Step forward one frame" + /// + public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 34f086da33..b946805be8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -87,13 +88,20 @@ namespace osu.Game.Screens.Play.PlayerSettings Action = () => seek(-1, seek_amount), TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), }, + new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepBackward, + Action = () => seekFrame(-1), + TooltipText = PlayerSettingsOverlayStrings.StepBackward, + }, pausePlay = new IconButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.4f), IconScale = new Vector2(1.4f), - Icon = FontAwesome.Regular.PlayCircle, Action = () => { if (gameplayClock.IsRunning) @@ -103,6 +111,14 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }, new SeekButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.StepForward, + Action = () => seekFrame(1), + TooltipText = PlayerSettingsOverlayStrings.StepForward, + }, + new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -166,6 +182,21 @@ namespace osu.Game.Screens.Play.PlayerSettings }, true); } + private void seekFrame(int direction) + { + gameplayClock.Stop(); + + var frames = gameplayState.Score.Replay.Frames; + + if (frames.Count == 0) + return; + + gameplayClock.Seek(direction < 0 + ? (frames.LastOrDefault(f => f.Time < gameplayClock.CurrentTime) ?? frames.First()).Time + : (frames.FirstOrDefault(f => f.Time > gameplayClock.CurrentTime) ?? frames.Last()).Time + ); + } + private void seek(int direction, double amount) { double target = Math.Clamp(gameplayClock.CurrentTime + (direction * amount), 0, gameplayState.Beatmap.GetLastObjectTime()); From 8c4af58109b202e4505f62634c8e9001397c5d3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 15:08:24 +0900 Subject: [PATCH 466/539] Keep replay controls expanded by default --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Screens/Play/ReplayPlayer.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c05831d043..6b2cb4ee74 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -142,6 +142,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.ReplaySettingsOverlay, true); + SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true); SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); @@ -421,6 +422,7 @@ namespace osu.Game.Configuration ProfileCoverExpanded, EditorLimitedDistanceSnap, ReplaySettingsOverlay, + ReplayPlaybackControlsExpanded, AutomaticallyDownloadMissingBeatmaps, EditorShowSpeedChanges, TouchDisableGameplayTaps, diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 805f907466..d4d6e7ecd0 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -52,7 +53,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { if (!LoadedBeatmapSuccessfully) return; @@ -60,7 +61,7 @@ namespace osu.Game.Screens.Play var playbackSettings = new PlaybackSettings { Depth = float.MaxValue, - Expanded = { Value = false } + Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) } }; if (GameplayClockContainer is MasterGameplayClockContainer master) From 22cfc8605094aebb6809333582543d6443756899 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jan 2024 14:24:48 +0300 Subject: [PATCH 467/539] Add failing test case --- osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 2926b11067..ee0c64aa3f 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -40,8 +40,15 @@ namespace osu.Game.Tests.Visual.Settings AddStep("change value from default", () => textBox.Current.Value = "non-default"); AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0); + AddStep("disable setting", () => textBox.Current.Disabled = true); + AddUntilStep("restore button still shown", () => revertToDefaultButton.Alpha > 0); + + AddStep("enable setting", () => textBox.Current.Disabled = false); AddStep("restore default", () => textBox.Current.SetDefault()); AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0); + + AddStep("disable setting", () => textBox.Current.Disabled = true); + AddUntilStep("restore button still hidden", () => revertToDefaultButton.Alpha == 0); } [Test] From b97c3ac9ee1c3a0e34afb296dc0e99543d002011 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jan 2024 14:20:07 +0300 Subject: [PATCH 468/539] Fix revert-to-default button appearing on disabled settings regardless of value --- osu.Game/Overlays/RevertToDefaultButton.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 582138b0b4..6fa5209f64 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -115,7 +115,12 @@ namespace osu.Game.Overlays Enabled.Value = !current.Disabled; - this.FadeTo(current.Disabled ? 0.2f : (current.IsDefault ? 0 : 1), fade_duration, Easing.OutQuint); + if (current.IsDefault) + this.FadeTo(0, fade_duration, Easing.OutQuint); + else if (current.Disabled) + this.FadeTo(0.2f, fade_duration, Easing.OutQuint); + else + this.FadeTo(1, fade_duration, Easing.OutQuint); if (IsHovered && Enabled.Value) { From c50534c8191e3fe408fe9e04dcbaf41c2b9c4577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 20:38:15 +0900 Subject: [PATCH 469/539] Move seek/step logic into `ReplayPlayer` --- .../Play/PlayerSettings/PlaybackSettings.cs | 51 +++++-------------- osu.Game/Screens/Play/ReplayPlayer.cs | 42 +++++++++++---- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index b946805be8..b3d07421ed 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,14 +1,11 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -36,10 +33,10 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly IBindable isPaused = new BindableBool(); [Resolved] - private GameplayClockContainer gameplayClock { get; set; } = null!; + private ReplayPlayer replayPlayer { get; set; } = null!; [Resolved] - private GameplayState gameplayState { get; set; } = null!; + private GameplayClockContainer gameplayClock { get; set; } = null!; private IconButton pausePlay = null!; @@ -51,9 +48,6 @@ namespace osu.Game.Screens.Play.PlayerSettings [BackgroundDependencyLoader] private void load() { - const double seek_amount = 5000; - const double seek_fast_amount = 10000; - Children = new Drawable[] { new FillFlowContainer @@ -77,23 +71,23 @@ namespace osu.Game.Screens.Play.PlayerSettings Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastBackward, - Action = () => seek(-1, seek_fast_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000), + Action = () => replayPlayer.SeekInDirection(-10), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Backward, - Action = () => seek(-1, seek_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), + Action = () => replayPlayer.SeekInDirection(-1), + TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.StepBackward, - Action = () => seekFrame(-1), + Action = () => replayPlayer.StepFrame(-1), TooltipText = PlayerSettingsOverlayStrings.StepBackward, }, pausePlay = new IconButton @@ -115,7 +109,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.StepForward, - Action = () => seekFrame(1), + Action = () => replayPlayer.StepFrame(1), TooltipText = PlayerSettingsOverlayStrings.StepForward, }, new SeekButton @@ -123,16 +117,16 @@ namespace osu.Game.Screens.Play.PlayerSettings Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Forward, - Action = () => seek(1, seek_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000), + Action = () => replayPlayer.SeekInDirection(1), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, new SeekButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.FastForward, - Action = () => seek(1, seek_fast_amount), - TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000), + Action = () => replayPlayer.SeekInDirection(10), + TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000), }, }, }, @@ -182,27 +176,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }, true); } - private void seekFrame(int direction) - { - gameplayClock.Stop(); - - var frames = gameplayState.Score.Replay.Frames; - - if (frames.Count == 0) - return; - - gameplayClock.Seek(direction < 0 - ? (frames.LastOrDefault(f => f.Time < gameplayClock.CurrentTime) ?? frames.First()).Time - : (frames.FirstOrDefault(f => f.Time > gameplayClock.CurrentTime) ?? frames.Last()).Time - ); - } - - private void seek(int direction, double amount) - { - double target = Math.Clamp(gameplayClock.CurrentTime + (direction * amount), 0, gameplayState.Beatmap.GetLastObjectTime()); - gameplayClock.Seek(target); - } - private partial class SeekButton : IconButton { public SeekButton() diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index d4d6e7ecd0..a26a2b9904 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -23,8 +23,11 @@ using osu.Game.Users; namespace osu.Game.Screens.Play { + [Cached] public partial class ReplayPlayer : Player, IKeyBindingHandler { + public const double BASE_SEEK_AMOUNT = 1000; + private readonly Func, Score> createScore; private readonly bool replayIsFailedScore; @@ -93,16 +96,22 @@ namespace osu.Game.Screens.Play public bool OnPressed(KeyBindingPressEvent e) { - const double keyboard_seek_amount = 5000; - switch (e.Action) { + case GlobalAction.StepReplayBackward: + StepFrame(-1); + return true; + + case GlobalAction.StepReplayForward: + StepFrame(1); + return true; + case GlobalAction.SeekReplayBackward: - keyboardSeek(-1); + SeekInDirection(-1); return true; case GlobalAction.SeekReplayForward: - keyboardSeek(1); + SeekInDirection(1); return true; case GlobalAction.TogglePauseReplay: @@ -114,13 +123,28 @@ namespace osu.Game.Screens.Play } return false; + } - void keyboardSeek(int direction) - { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime()); + public void StepFrame(int direction) + { + GameplayClockContainer.Stop(); - Seek(target); - } + var frames = GameplayState.Score.Replay.Frames; + + if (frames.Count == 0) + return; + + GameplayClockContainer.Seek(direction < 0 + ? (frames.LastOrDefault(f => f.Time < GameplayClockContainer.CurrentTime) ?? frames.First()).Time + : (frames.FirstOrDefault(f => f.Time > GameplayClockContainer.CurrentTime) ?? frames.Last()).Time + ); + } + + public void SeekInDirection(float amount) + { + double target = Math.Clamp(GameplayClockContainer.CurrentTime + amount * BASE_SEEK_AMOUNT, 0, GameplayState.Beatmap.GetLastObjectTime()); + + Seek(target); } public void OnReleased(KeyBindingReleaseEvent e) From 0383bdf6a15d57a0a499b6b88ac53d4eb57b4f2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jan 2024 20:38:25 +0900 Subject: [PATCH 470/539] Add bindings for stepping backward/forward --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 10 +++++++++- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5a39c02185..436334cfe1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), + new KeyBinding(InputKey.Comma, GlobalAction.StepReplayBackward), + new KeyBinding(InputKey.Period, GlobalAction.StepReplayForward), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; @@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings IncreaseOffset, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))] - DecreaseOffset + DecreaseOffset, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayForward))] + StepReplayForward, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] + StepReplayBackward, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index ca27d0ff95..703e0ff1ca 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -324,6 +324,16 @@ namespace osu.Game.Localisation /// public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward"); + /// + /// "Seek replay forward one frame" + /// + public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame"); + + /// + /// "Step replay backward one frame" + /// + public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame"); + /// /// "Toggle chat focus" /// From 3eeefd5b7e8708894c26474cf3868fc54e662f61 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 18 Jan 2024 16:13:48 -0800 Subject: [PATCH 471/539] Fix changelog stream user count only accounting for latest build --- osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs | 3 +++ osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs index 76d1941d9d..dac72f2488 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -28,6 +28,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("latest_build")] public APIChangelogBuild LatestBuild { get; set; } + [JsonProperty("user_count")] + public int UserCount { get; set; } + public bool Equals(APIUpdateStream other) => Id == other?.Id; internal static readonly Dictionary KNOWN_STREAMS = new Dictionary diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 08ea373fb1..30273d2405 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Changelog protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion; - protected override LocalisableString InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override LocalisableString InfoText => Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } From 45effdb6dfaddf3e6fc5ed0aa882629e8ff99470 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 20 Jan 2024 08:41:34 +0300 Subject: [PATCH 472/539] Remove local vertex batching from triangles backgrounds --- osu.Game/Graphics/Backgrounds/Triangles.cs | 11 +---------- osu.Game/Graphics/Backgrounds/TrianglesV2.cs | 12 +----------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 1a78c1ec5e..8db7f3a1c3 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Lists; using osu.Framework.Bindables; @@ -264,7 +263,6 @@ namespace osu.Game.Graphics.Backgrounds private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; private Vector2 size; - private IVertexBatch vertexBatch; public TrianglesDrawNode(Triangles source) : base(source) @@ -290,12 +288,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Draw(renderer); - if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -333,7 +325,7 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); @@ -356,7 +348,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index a20fd78569..4f611d9def 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; -using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -196,8 +195,6 @@ namespace osu.Game.Graphics.Backgrounds private float texelSize; private bool masking; - private IVertexBatch? vertexBatch; - public TrianglesDrawNode(TrianglesV2 source) : base(source) { @@ -235,12 +232,6 @@ namespace osu.Game.Graphics.Backgrounds if (Source.AimCount == 0 || thickness == 0) return; - if (vertexBatch == null || vertexBatch.Size != Source.AimCount) - { - vertexBatch?.Dispose(); - vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); - } - borderDataBuffer ??= renderer.CreateUniformBuffer(); borderDataBuffer.Data = borderDataBuffer.Data with { @@ -273,7 +264,7 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), textureCoords: textureCoords); } shader.Unbind(); @@ -296,7 +287,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Dispose(isDisposing); - vertexBatch?.Dispose(); borderDataBuffer?.Dispose(); } } From 3ad3b052dffe4a1434868ad3cd2fe37c67784d24 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:10:36 +0300 Subject: [PATCH 473/539] Enable masking is osu logo --- osu.Game/Screens/Menu/OsuLogo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 75ef8be02e..243612eee1 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -192,6 +192,7 @@ namespace osu.Game.Screens.Menu ColourLight = Color4Extensions.FromHex(@"ff7db7"), ColourDark = Color4Extensions.FromHex(@"de5b95"), RelativeSizeAxes = Axes.Both, + Masking = true }, } }, From a3703d657ab5ba2a93dbd89cad0adb6a21dfbd17 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:12:49 +0300 Subject: [PATCH 474/539] Enable masking in popup dialog --- osu.Game/Overlays/Dialog/PopupDialog.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 4ac37a63e2..1c1196a1c5 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -150,6 +150,7 @@ namespace osu.Game.Overlays.Dialog ColourLight = Color4Extensions.FromHex(@"271e26"), ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, + Masking = true }, flashLayer = new Box { From 087d0f03a4595d5eac66bd2f8ffb2ca6ea85264a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:15:48 +0300 Subject: [PATCH 475/539] Enable masking in toolbar --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 1da2e1b744..bdd16b347d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -255,6 +255,7 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Both, ColourLight = OsuColour.Gray(40), ColourDark = OsuColour.Gray(20), + Masking = true }, }; } From 421ae73715ad9f1d3f5dcc331c8ac81304830187 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:21:09 +0300 Subject: [PATCH 476/539] Enable masking in DrawableCarouselBeatmap --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index baf0a14062..ecf65fe55c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -109,7 +109,8 @@ namespace osu.Game.Screens.Select.Carousel TriangleScale = 2, RelativeSizeAxes = Axes.Both, ColourLight = Color4Extensions.FromHex(@"3a7285"), - ColourDark = Color4Extensions.FromHex(@"123744") + ColourDark = Color4Extensions.FromHex(@"123744"), + Masking = true }, new FillFlowContainer { From 60f7b4ea2f582b8fa2f1f15dd70c0febe0ea711d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:25:29 +0300 Subject: [PATCH 477/539] Enable masking in DrawableRank --- osu.Game/Online/Leaderboards/DrawableRank.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 5177f35478..bbf981af09 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -50,6 +50,7 @@ namespace osu.Game.Online.Leaderboards ColourDark = rankColour.Darken(0.1f), ColourLight = rankColour.Lighten(0.1f), Velocity = 0.25f, + Masking = true }, new OsuSpriteText { From 6ba3546be5c2ef8f514e6652f2965bfb70d1b59c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 02:28:14 +0300 Subject: [PATCH 478/539] Enable masking in RoundedButton --- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 6aded3fe32..65bd5f49c2 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -68,6 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 SpawnRatio = 0.6f, RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, + Masking = true }); updateColours(); From 1999e772f6df41e35dafb2e185f6ff49a31089bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Jan 2024 10:31:27 +0900 Subject: [PATCH 479/539] Fix `FollowPointConnection` pool filling up when follow points are hidden Closes https://github.com/ppy/osu/issues/26642. I think this applied to all pooling cases here. --- .../Objects/Pooling/PooledDrawableWithLifetimeContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 3b45acc7bb..1b0176cae5 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -153,6 +153,9 @@ namespace osu.Game.Rulesets.Objects.Pooling protected override bool CheckChildrenLife() { + if (!IsPresent) + return false; + bool aliveChanged = base.CheckChildrenLife(); aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); return aliveChanged; From 2dedead1d12c39fdb2f26385664b9f37105877fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Jan 2024 10:55:52 +0900 Subject: [PATCH 480/539] Allow debug instances to coexist alongside release instances --- osu.Game/OsuGame.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7138bf2175..c244708385 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -80,7 +80,12 @@ namespace osu.Game [Cached(typeof(OsuGame))] public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { +#if DEBUG + // Different port allows runnning release and debug builds alongside each other. + public const int IPC_PORT = 44824; +#else public const int IPC_PORT = 44823; +#endif /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). From e003ecb5937f5d1239b64250e8968908bf41c165 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 04:57:48 +0300 Subject: [PATCH 481/539] Change default masking value to true --- osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs | 1 + osu.Game/Graphics/Backgrounds/Triangles.cs | 4 ++-- osu.Game/Graphics/Backgrounds/TrianglesV2.cs | 4 ++-- osu.Game/Graphics/UserInterface/DialogButton.cs | 1 + osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 1 - osu.Game/Online/Leaderboards/DrawableRank.cs | 1 - osu.Game/Overlays/Dialog/PopupDialog.cs | 1 - osu.Game/Overlays/Mods/ModSelectColumn.cs | 1 + osu.Game/Overlays/Toolbar/ToolbarButton.cs | 1 - osu.Game/Screens/Menu/OsuLogo.cs | 1 - osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 1 - 11 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index f1143cf14d..16cd302b88 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs @@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; + Masking = false; } protected override void Update() diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 8db7f3a1c3..cf74d6ef62 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -78,9 +78,9 @@ namespace osu.Game.Graphics.Backgrounds /// /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// shape is drawn to the screen. Default is true. /// - public bool Masking { get; set; } + public bool Masking { get; set; } = true; /// /// Whether we should drop-off alpha values of triangles more quickly to improve diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 4f611d9def..40b076ebc8 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -34,9 +34,9 @@ namespace osu.Game.Graphics.Backgrounds /// /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. + /// shape is drawn to the screen. Default is true. /// - public bool Masking { get; set; } + public bool Masking { get; set; } = true; private readonly BindableFloat spawnRatio = new BindableFloat(1f); diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c920597a95..59acbecbbf 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -150,6 +150,7 @@ namespace osu.Game.Graphics.UserInterface TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), Shear = new Vector2(-0.2f, 0), + Masking = false }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 65bd5f49c2..6aded3fe32 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -68,7 +68,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 SpawnRatio = 0.6f, RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, - Masking = true }); updateColours(); diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index bbf981af09..5177f35478 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -50,7 +50,6 @@ namespace osu.Game.Online.Leaderboards ColourDark = rankColour.Darken(0.1f), ColourLight = rankColour.Lighten(0.1f), Velocity = 0.25f, - Masking = true }, new OsuSpriteText { diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 1c1196a1c5..4ac37a63e2 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -150,7 +150,6 @@ namespace osu.Game.Overlays.Dialog ColourLight = Color4Extensions.FromHex(@"271e26"), ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, - Masking = true }, flashLayer = new Box { diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 1c56763bd9..631bb72ced 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -95,6 +95,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Velocity = 0.7f, + Masking = false }, headerText = new OsuTextFlowContainer(t => { diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index bdd16b347d..1da2e1b744 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -255,7 +255,6 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Both, ColourLight = OsuColour.Gray(40), ColourDark = OsuColour.Gray(20), - Masking = true }, }; } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 243612eee1..75ef8be02e 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -192,7 +192,6 @@ namespace osu.Game.Screens.Menu ColourLight = Color4Extensions.FromHex(@"ff7db7"), ColourDark = Color4Extensions.FromHex(@"de5b95"), RelativeSizeAxes = Axes.Both, - Masking = true }, } }, diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index ecf65fe55c..4a28b3212e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -110,7 +110,6 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.Both, ColourLight = Color4Extensions.FromHex(@"3a7285"), ColourDark = Color4Extensions.FromHex(@"123744"), - Masking = true }, new FillFlowContainer { From a69fd8198db3901fffa43a323ddb15a6df029713 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Jan 2024 10:57:23 +0900 Subject: [PATCH 482/539] Update framework and other nuget packages - Moq held back because dicks - NUnit held back because large API changes (trivial but effort) - SignalR held back due to api deprecations --- ...u.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- ....Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Desktop/osu.Desktop.csproj | 4 ++-- .../osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 20 +++++++++---------- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 2baa7ee0e0..5babdb47ff 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index a2308e6dfc..5d64ca832a 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index e839d2657c..6796a8962b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index a2308e6dfc..5d64ca832a 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index f37cfdc5f1..d6a11fa924 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,9 +23,9 @@ - + - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 5de21a68d0..47c93fbd02 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -8,7 +8,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index c45c85833c..0a77845343 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index b991db408c..877b9c3ea4 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index ea033cda45..9c248abd66 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 48465bb119..4eb6b0ab3d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index ef6c16f2c4..7b08524240 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 2cc07dd9ed..3b00f103c4 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f33ddac66f..1b1abe3971 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,12 +21,12 @@ - + - - - - + + + + @@ -36,13 +36,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - - + + + From 1393f52b2bf537bacf2248db501068f5f2b165b8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Jan 2024 05:20:42 +0300 Subject: [PATCH 483/539] Rename Masking to ClampToDrawable --- osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs | 2 +- .../Visual/Background/TestSceneTrianglesBackground.cs | 2 +- .../Visual/Background/TestSceneTrianglesV2Background.cs | 2 +- osu.Game/Graphics/Backgrounds/Triangles.cs | 8 ++++---- osu.Game/Graphics/Backgrounds/TrianglesV2.cs | 8 ++++---- osu.Game/Graphics/UserInterface/DialogButton.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index 16cd302b88..566176505d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; - Masking = false; + ClampToDrawable = false; } protected override void Update() diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs index 378dd99664..4733b7f92f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Background AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s)); - AddToggleStep("Masking", m => triangles.Masking = m); + AddToggleStep("ClampToDrawable", c => triangles.ClampToDrawable = c); } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs index 01a2464b8e..71d1baddc8 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White); AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); - AddToggleStep("Masking", m => maskedTriangles.Masking = m); + AddToggleStep("ClampToDrawable", c => maskedTriangles.ClampToDrawable = c); } } } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index cf74d6ef62..d7bfeb7731 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -80,7 +80,7 @@ namespace osu.Game.Graphics.Backgrounds /// If enabled, only the portion of triangles that falls within this 's /// shape is drawn to the screen. Default is true. /// - public bool Masking { get; set; } = true; + public bool ClampToDrawable { get; set; } = true; /// /// Whether we should drop-off alpha values of triangles more quickly to improve @@ -257,7 +257,7 @@ namespace osu.Game.Graphics.Backgrounds private IShader shader; private Texture texture; - private bool masking; + private bool clamp; private readonly List parts = new List(); private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; @@ -276,7 +276,7 @@ namespace osu.Game.Graphics.Backgrounds shader = Source.shader; texture = Source.texture; size = Source.DrawSize; - masking = Source.Masking; + clamp = Source.ClampToDrawable; parts.Clear(); parts.AddRange(Source.parts); @@ -306,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 40b076ebc8..f723b1b358 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -36,7 +36,7 @@ namespace osu.Game.Graphics.Backgrounds /// If enabled, only the portion of triangles that falls within this 's /// shape is drawn to the screen. Default is true. /// - public bool Masking { get; set; } = true; + public bool ClampToDrawable { get; set; } = true; private readonly BindableFloat spawnRatio = new BindableFloat(1f); @@ -193,7 +193,7 @@ namespace osu.Game.Graphics.Backgrounds private Vector2 size; private float thickness; private float texelSize; - private bool masking; + private bool clamp; public TrianglesDrawNode(TrianglesV2 source) : base(source) @@ -208,7 +208,7 @@ namespace osu.Game.Graphics.Backgrounds texture = Source.texture; size = Source.DrawSize; thickness = Source.Thickness; - masking = Source.Masking; + clamp = Source.ClampToDrawable; Quad triangleQuad = new Quad( Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix), @@ -248,7 +248,7 @@ namespace osu.Game.Graphics.Backgrounds { Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 59acbecbbf..10aca017f1 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -150,7 +150,7 @@ namespace osu.Game.Graphics.UserInterface TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), Shear = new Vector2(-0.2f, 0), - Masking = false + ClampToDrawable = false }, }, }, diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 631bb72ced..05454159c7 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Velocity = 0.7f, - Masking = false + ClampToDrawable = false }, headerText = new OsuTextFlowContainer(t => { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 4a28b3212e..baf0a14062 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Select.Carousel TriangleScale = 2, RelativeSizeAxes = Axes.Both, ColourLight = Color4Extensions.FromHex(@"3a7285"), - ColourDark = Color4Extensions.FromHex(@"123744"), + ColourDark = Color4Extensions.FromHex(@"123744") }, new FillFlowContainer { From 18d16018d340b38729b2ddbcb46e3bcb5b3fe3b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Jan 2024 12:17:16 +0900 Subject: [PATCH 484/539] Fix failing tests due to pooling safety changes --- .../Skinning/TestSceneBarLine.cs | 40 +++++++--------- .../TestSceneDrawableJudgement.cs | 47 ++++++++++--------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs index ab9f57ecc3..a5c18babe2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -1,10 +1,8 @@ // 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.Collections.Generic; using NUnit.Framework; -using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -16,37 +14,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Test] public void TestMinor() { - AddStep("Create barlines", () => recreate()); + AddStep("Create barlines", recreate); } - private void recreate(Func>? createBarLines = null) + private void recreate() { var stageDefinitions = new List { new StageDefinition(4), }; - SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s => + SetContents(_ => { - if (createBarLines != null) + var maniaPlayfield = new ManiaPlayfield(stageDefinitions); + + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => { - var barLines = createBarLines(); - - foreach (var b in barLines) - s.Add(b); - - return; - } - - for (int i = 0; i < 64; i++) - { - s.Add(new BarLine + for (int i = 0; i < 64; i++) { - StartTime = Time.Current + i * 500, - Major = i % 4 == 0, - }); - } - })); + maniaPlayfield.Add(new BarLine + { + StartTime = Time.Current + i * 500, + Major = i % 4 == 0, + }); + } + }); + + return maniaPlayfield; + }); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 874130233a..5f5596cbb3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } = null!; - private readonly List> pools; + private readonly List> pools = new List>(); - public TestSceneDrawableJudgement() + [TestCaseSource(nameof(validResults))] + public void Test(HitResult result) { - pools = new List>(); - - foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) - showResult(result); + showResult(result); } + private static IEnumerable validResults => Enum.GetValues().Skip(1); + [Test] public void TestHitLightingDisabled() { @@ -72,32 +72,33 @@ namespace osu.Game.Rulesets.Osu.Tests pools.Add(pool = new DrawablePool(1)); else { - pool = pools[poolIndex]; - // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. + pool = pools[poolIndex]; ((Container)pool.Parent!).Clear(false); } var container = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject - { - StartTime = Time.Current - }, new Judgement()) - { - Type = result, - }, null)).With(j => - { - j.Anchor = Anchor.Centre; - j.Origin = Anchor.Centre; - }) - } + Child = pool, }; + // Must be scheduled so the pool is loaded before we try and retrieve from it. + Schedule(() => + { + container.Add(pool.Get(j => j.Apply(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) + { + Type = result, + }, null)).With(j => + { + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + })); + }); + poolIndex++; return container; }); From 02369139b3054213e6598337a66ea868a5ff56c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 21 Jan 2024 13:11:41 +0900 Subject: [PATCH 485/539] Remove `FillFlow` overhead of argon counters --- .../Play/HUD/ArgonCounterTextComponent.cs | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index f16669f865..f8c82feddd 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -42,35 +42,30 @@ namespace osu.Game.Screens.Play.HUD Origin = anchor; AutoSizeAxes = Axes.Both; - InternalChild = new FillFlowContainer + InternalChildren = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + labelText = new OsuSpriteText { - labelText = new OsuSpriteText + Alpha = 0, + Text = label.GetValueOrDefault(), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), + Margin = new MarginPadding { Left = 2.5f }, + }, + NumberContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new[] { - Alpha = 0, - Text = label.GetValueOrDefault(), - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), - Margin = new MarginPadding { Left = 2.5f }, - }, - NumberContainer = new Container - { - AutoSizeAxes = Axes.Both, - Children = new[] + wireframesPart = new ArgonCounterSpriteText(wireframesLookup) { - wireframesPart = new ArgonCounterSpriteText(wireframesLookup) - { - Anchor = anchor, - Origin = anchor, - }, - textPart = new ArgonCounterSpriteText(textLookup) - { - Anchor = anchor, - Origin = anchor, - }, - } + Anchor = anchor, + Origin = anchor, + }, + textPart = new ArgonCounterSpriteText(textLookup) + { + Anchor = anchor, + Origin = anchor, + }, } } }; @@ -110,7 +105,11 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); - ShowLabel.BindValueChanged(s => labelText.Alpha = s.NewValue ? 1 : 0, true); + ShowLabel.BindValueChanged(s => + { + labelText.Alpha = s.NewValue ? 1 : 0; + NumberContainer.Y = s.NewValue ? 12 : 0; + }, true); } private partial class ArgonCounterSpriteText : OsuSpriteText From bab14dce31dcc408a600ebe4d0a94648b2cbaae6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 22 Jan 2024 02:07:09 +0300 Subject: [PATCH 486/539] Add failing test case --- .../TestSceneSliderApplication.cs | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index f41dd913ab..380a2087ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests { DrawableSlider dho = null; - AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider + AddStep("create slider", () => Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddWaitStep("wait for progression", 1); - AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider + AddStep("apply new slider", () => dho.Apply(applyDefaults(new Slider { Position = new Vector2(256, 192), ComboIndex = 1, @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests Child = new SkinProvidingContainer(provider) { RelativeSizeAxes = Axes.Both, - Child = dho = new DrawableSlider(prepareObject(new Slider + Child = dho = new DrawableSlider(applyDefaults(new Slider { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, @@ -97,7 +97,38 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red); } - private Slider prepareObject(Slider slider) + [Test] + public void TestIncreaseRepeatCount() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + Child = dho = new DrawableSlider(applyDefaults(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.LINEAR, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })); + }); + + AddStep("increase repeat count", () => + { + dho.HitObject.RepeatCount++; + applyDefaults(dho.HitObject); + }); + + AddAssert("repeat got custom anchor", () => + dho.ChildrenOfType().Single().RelativeAnchorPosition == Vector2.Divide(dho.SliderBody!.PathOffset, dho.DrawSize)); + } + + private Slider applyDefaults(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return slider; From 57b2d018a99a7307d09984687299b03c027040cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 22 Jan 2024 02:07:29 +0300 Subject: [PATCH 487/539] Fix slider sometimes not updating relative anchor position --- .../Objects/Drawables/DrawableSlider.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index ed4c02a4a9..baec200107 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container repeatContainer; private PausableSkinnableSound slidingSample; - private readonly LayoutValue drawSizeLayout; + private readonly LayoutValue relativeAnchorPositionLayout; public DrawableSlider() : this(null) @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AlwaysPresent = true, Alpha = 0 }; - AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); + AddLayout(relativeAnchorPositionLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry)); } [BackgroundDependencyLoader] @@ -190,6 +190,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables repeatContainer.Add(repeat); break; } + + relativeAnchorPositionLayout.Invalidate(); } protected override void ClearNestedHitObjects() @@ -265,14 +267,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Size = SliderBody?.Size ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero; - if (!drawSizeLayout.IsValid) + if (!relativeAnchorPositionLayout.IsValid) { Vector2 pos = Vector2.Divide(OriginPosition, DrawSize); foreach (var obj in NestedHitObjects) obj.RelativeAnchorPosition = pos; Ball.RelativeAnchorPosition = pos; - drawSizeLayout.Validate(); + relativeAnchorPositionLayout.Validate(); } } From cec4f670d1f84e39b4fa918b13e57fbed087f65b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 22 Jan 2024 03:12:23 +0300 Subject: [PATCH 488/539] Reduce allocation overhead in BeatmapCarousel --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4408634787..1f2103ff61 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -899,7 +899,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in Scroll.Children) + foreach (DrawableCarouselItem item in Scroll.ScrollContent) { updateItem(item); From 2bd9cd5d3410a281cff18f285a01c355a9c9b8e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 22 Jan 2024 04:39:08 +0300 Subject: [PATCH 489/539] Fix blueprint container not handling right clicks correctly while moveing an element --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 110beb0fa6..2d6e234e57 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { bool selectionPerformed = performMouseDownActions(e); - bool movementPossible = prepareSelectionMovement(); + bool movementPossible = prepareSelectionMovement(e); // check if selection has occurred if (selectionPerformed) @@ -536,9 +536,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Attempts to begin the movement of any selected blueprints. /// + /// The defining the beginning of a movement. /// Whether a movement is possible. - private bool prepareSelectionMovement() + private bool prepareSelectionMovement(MouseDownEvent e) { + if (e.Button == MouseButton.Right) + return false; + if (!SelectionHandler.SelectedBlueprints.Any()) return false; From 74f05a5c4b28d1493e7556588f0589cb20821650 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 15:54:24 +0900 Subject: [PATCH 490/539] Use container itself rather than `ScrollContent` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 1f2103ff61..35d534cf68 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -899,7 +899,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in Scroll.ScrollContent) + foreach (DrawableCarouselItem item in Scroll) { updateItem(item); From 1f0ad5cff24f4c445d4dd8d6b5c4be1e3de98472 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 15:56:16 +0900 Subject: [PATCH 491/539] Apply same fix in more places --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 35d534cf68..70ecde3858 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -868,7 +868,7 @@ namespace osu.Game.Screens.Select { var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - foreach (var panel in Scroll.Children) + foreach (var panel in Scroll) { Debug.Assert(panel.Item != null); @@ -1094,7 +1094,7 @@ namespace osu.Game.Screens.Select // to enter clamp-special-case mode where it animates completely differently to normal. float scrollChange = scrollTarget.Value - Scroll.Current; Scroll.ScrollTo(scrollTarget.Value, false); - foreach (var i in Scroll.Children) + foreach (var i in Scroll) i.Y += scrollChange; break; } From 3e5fe66e58b3bf3e36761511883b8c0170f7c0e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 16:37:56 +0900 Subject: [PATCH 492/539] Fix potential null reference in player screen transition handling See https://github.com/ppy/osu/actions/runs/7607677219/job/20715418536?pr=26660. --- osu.Game/Screens/Play/Player.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index db39d06c54..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1240,9 +1240,14 @@ namespace osu.Game.Screens.Play { b.IgnoreUserSettings.Value = true; - b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); - b.IsBreakTime.Value = false; + // May be null if the load never completed. + if (breakTracker != null) + { + b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime); + b.IsBreakTime.Value = false; + } }); + storyboardReplacesBackground.Value = false; } } From 3dd18c777a578acbc8ded48cb3ea92683391fc5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 17:00:35 +0900 Subject: [PATCH 493/539] Remove a couple more overrides --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 2 -- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 +------ osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6f5ff9c99c..3792a67896 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -40,8 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { public override string Title => "Lounge"; - protected override bool PlayExitSound => false; - protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { SelectedRoom = { BindTarget = SelectedRoom } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 8a85a46a2f..aab7ac3280 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -44,8 +44,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public override string ShortTitle => "room"; - protected override bool PlayExitSound => !exitConfirmed; - [Resolved] private MultiplayerClient client { get; set; } @@ -249,15 +247,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } - private bool exitConfirmed; - public override bool OnExiting(ScreenExitEvent e) { // room has not been created yet or we're offline; exit immediately. if (client.Room == null || !IsConnected) return base.OnExiting(e); - if (!exitConfirmed && dialogOverlay != null) + if (dialogOverlay != null) { if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) confirmDialog.PerformOkAction(); @@ -265,7 +261,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { - exitConfirmed = true; if (this.IsCurrentScreen()) this.Exit(); })); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index ea855c0474..fa1ee004c9 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay public virtual string ShortTitle => Title; - protected override bool PlayExitSound => false; + protected sealed override bool PlayExitSound => false; [Resolved] protected IRoomManager? RoomManager { get; private set; } From cd551b1abd4912b7d93fedeabd0b21fcffa9f1bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 18:01:21 +0900 Subject: [PATCH 494/539] Fix star fountains sometimes resetting visually Addresses https://github.com/ppy/osu/discussions/26622. --- osu.Game/Screens/Menu/StarFountain.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index fd59ec3573..dd5171c6be 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Skinning; @@ -43,8 +44,6 @@ namespace osu.Game.Screens.Menu private const double shoot_duration = 800; - protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration; - [Resolved] private ISkinSource skin { get; set; } = null!; @@ -57,7 +56,6 @@ namespace osu.Game.Screens.Menu private void load(TextureStore textures) { Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star"); - Active.Value = true; } protected override FallingParticle CreateParticle() @@ -81,8 +79,15 @@ namespace osu.Game.Screens.Menu return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); } + private ScheduledDelegate? deactivateDelegate; + public void Shoot(int direction) { + Active.Value = true; + + deactivateDelegate?.Cancel(); + deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, shoot_duration); + lastShootTime = Clock.CurrentTime; lastShootDirection = direction; } From 47e9846315244e9f4824a8fedcb8e6c4dee3cd44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 18:48:48 +0900 Subject: [PATCH 495/539] Adjust slider tick / end miss animations to be less busy --- .../Skinning/Argon/ArgonJudgementPiece.cs | 11 ++++++----- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 9 ++++----- osu.Game/Skinning/LegacyJudgementPieceOld.cs | 10 +++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs index bb61bd37c1..9a5abba4fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs @@ -65,14 +65,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { this.RotateTo(-45); - this.ScaleTo(1.8f); + this.ScaleTo(1.6f); this.ScaleTo(1.2f, 100, Easing.In); - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + this.FadeOutFromOne(400); } else if (Result.IsMiss()) { + this.FadeOutFromOne(800); + this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); @@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon } else { + this.FadeOutFromOne(800); + JudgementText .FadeInFromZero(300, Easing.OutQuint) .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); } - this.FadeOutFromOne(800); - ringExplosion?.PlayAnimation(); } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index ada651b60e..c7103cd9fd 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -45,11 +45,10 @@ namespace osu.Game.Rulesets.Judgements if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss) { this.RotateTo(-45); - this.ScaleTo(1.8f); + this.ScaleTo(1.6f); this.ScaleTo(1.2f, 100, Easing.In); - this.MoveTo(Vector2.Zero); - this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint); + this.FadeOutFromOne(400); } else if (Result.IsMiss()) { @@ -61,9 +60,9 @@ namespace osu.Game.Rulesets.Judgements this.RotateTo(0); this.RotateTo(40, 800, Easing.InQuint); - } - this.FadeOutFromOne(800); + this.FadeOutFromOne(800); + } } public Drawable? GetAboveHitObjectsProxiedContent() => null; diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 1834a17279..a9f68bd378 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -63,14 +63,10 @@ namespace osu.Game.Skinning // missed ticks / slider end don't get the normal animation. if (isMissedTick()) { - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); + this.ScaleTo(1.2f); + this.ScaleTo(1f, 100, Easing.In); - if (legacyVersion > 1.0m) - { - this.MoveTo(new Vector2(0, -2f)); - this.MoveToOffset(new Vector2(0, 10), fade_out_delay + fade_out_length, Easing.In); - } + this.FadeOutFromOne(400); } else { From 9b2740f8a4032dfa568f2bbcc1ee0609a56c2bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 11:11:15 +0100 Subject: [PATCH 496/539] Fix score submission test failures due to checking audio playback validity (again) As seen in https://github.com/ppy/osu/actions/runs/7607899979/job/20716013982?pr=26662#step:5:75 In https://github.com/ppy/osu/pull/26484, I went "well if I'm moving the enabling of validation of playback rate to `SubmittingPlayer` context, then surely I can remove the local override in the test scene, right?" Reader: Apparently I did not notice that `FakeImportingPlayer : TestPlayer : SoloPlayer : SubmittingPlayer`. So no, I could not remove the local override in the test scene. You could probably attempt to conjure up some excuse about deep inheritance hierarchies here but nah. Really just a failure to read on my behalf as usual. --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index e2ce3a014c..5e22e47572 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -383,6 +384,11 @@ namespace osu.Game.Tests.Visual.Gameplay AllowImportCompletion = new SemaphoreSlim(1); } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart) + { + ShouldValidatePlaybackRate = false, + }; + protected override async Task ImportScore(Score score) { ScoreImportStarted = true; From c8521b49cd6bcfa3aee3c437da357d49a196844d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jan 2024 18:43:15 +0900 Subject: [PATCH 497/539] Change S rank to require no miss --- .../Scoring/CatchScoreProcessor.cs | 2 +- .../Scoring/OsuScoreProcessor.cs | 18 ++++++++++++++++++ .../Scoring/TaikoScoreProcessor.cs | 18 ++++++++++++++++++ .../Visual/Ranking/TestSceneAccuracyCircle.cs | 19 +++++++++++-------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 161a59c5fd..a12fe7f3e1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); } - public override ScoreRank RankFromAccuracy(double accuracy) + public override ScoreRank RankFromScore(double accuracy, Dictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 97980c6d18..0d62a6e718 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -14,6 +16,22 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + public override ScoreRank RankFromScore(double accuracy, Dictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + protected override HitEvent CreateHitEvent(JudgementResult result) => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 2fd9f070ec..023cd1f902 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { @@ -33,6 +35,22 @@ namespace osu.Game.Rulesets.Taiko.Scoring * strongScaleValue(result); } + public override ScoreRank RankFromScore(double accuracy, Dictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) == 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + public override int GetBaseScoreForResult(HitResult result) { switch (result) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 435dd77120..7aa36429a7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -96,6 +97,14 @@ namespace osu.Game.Tests.Visual.Ranking { var scoreProcessor = ruleset.CreateScoreProcessor(); + var statistics = new Dictionary + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + }; + return new ScoreInfo { User = new APIUser @@ -109,15 +118,9 @@ namespace osu.Game.Tests.Visual.Ranking TotalScore = 2845370, Accuracy = accuracy, MaxCombo = 999, - Rank = scoreProcessor.RankFromAccuracy(accuracy), + Rank = scoreProcessor.RankFromScore(accuracy, statistics), Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } + Statistics = statistics, }; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 80e751422e..95f672f6e6 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -370,7 +370,7 @@ namespace osu.Game.Rulesets.Scoring if (rank.Value == ScoreRank.F) return; - rank.Value = RankFromAccuracy(Accuracy.Value); + rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); foreach (var mod in Mods.Value.OfType()) rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); } @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public virtual ScoreRank RankFromAccuracy(double accuracy) + public virtual ScoreRank RankFromScore(double accuracy, Dictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b30fc7aee1..427b1cb7cf 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -280,7 +280,7 @@ namespace osu.Game.Scoring.Legacy { scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); - var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy); + var rank = currentRuleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); foreach (var mod in scoreInfo.Mods.OfType()) rank = mod.AdjustRank(rank, scoreInfo.Accuracy); From bb1eab58440ab36c0c5197335a20b72a23961219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:25:36 +0900 Subject: [PATCH 498/539] Update test in line with new rank definitions always being applied --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 6e7c8c3631..e56dff74c3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -293,7 +293,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void AccuracyAndRankOfLazerScorePreserved() + public void AccuracyOfLazerScorePreserved() { var ruleset = new OsuRuleset().RulesetInfo; @@ -322,7 +322,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); - Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); } From 4eba3b5d7093f89f186a343c8f316054edd5552c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:48:29 +0900 Subject: [PATCH 499/539] Move `BackgroundDataStoreProcessor` to better namespace --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 1 + osu.Game/{ => Database}/BackgroundDataStoreProcessor.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/{ => Database}/BackgroundDataStoreProcessor.cs (99%) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 8b066f860f..e22cf2398b 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs similarity index 99% rename from osu.Game/BackgroundDataStoreProcessor.cs rename to osu.Game/Database/BackgroundDataStoreProcessor.cs index fc7db13d41..e462e0ccf8 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; @@ -22,7 +21,7 @@ using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; -namespace osu.Game +namespace osu.Game.Database { /// /// Performs background updating of data stores at startup. From 644e7d6fe6f845ccf0e3d919229d2e130dd76d1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:55:17 +0900 Subject: [PATCH 500/539] Add migration --- .../Database/BackgroundDataStoreProcessor.cs | 58 +++++++++++++++++++ osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index e462e0ccf8..324d699e7c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -73,6 +73,7 @@ namespace osu.Game.Database processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); + upgradeScoreRanks(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -354,6 +355,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; + // TODO: ensure that this is also updating rank (as it will set TotalScoreVersion to latest). StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); @@ -375,6 +377,62 @@ namespace osu.Game.Database completeNotification(notification, processedCount, scoreIds.Count, failedCount); } + private void upgradeScoreRanks() + { + Logger.Log("Querying for scores that need rank upgrades..."); + + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .Select(s => s.ID))); + + Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); + + if (scoreIds.Count == 0) + return; + + var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in scoreIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, scoreIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id)!; + // TODO: uncomment when ready + // s.Rank = StandardisedScoreMigrationTools.ComputeRank(s, beatmapManager); + }); + + ++processedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update rank score {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; + } + } + + completeNotification(notification, processedCount, scoreIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 389b20b5c8..c74980abb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -43,9 +43,10 @@ namespace osu.Game.Scoring.Legacy /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct /// method. Reconvert all scores. /// + /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// /// - public const int LATEST_VERSION = 30000012; + public const int LATEST_VERSION = 30000013; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 83f9118b22d64e1b52f213d895cb6e959d88b842 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 21:31:17 +0900 Subject: [PATCH 501/539] Adjust results screen to handle S->A rank adjustment when misses are present --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++----- .../Expanded/Accuracy/AccuracyCircle.cs | 39 +++++++++++++++++++ .../Ranking/Expanded/Accuracy/RankBadge.cs | 8 ++-- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ab2e867255..866e20d063 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -71,15 +72,16 @@ namespace osu.Game.Tests.Visual.Ranking private int onlineScoreID = 1; - [TestCase(1, ScoreRank.X)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.2, ScoreRank.D)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + [TestCase(1, ScoreRank.X, 0)] + [TestCase(0.9999, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.A, 1)] + [TestCase(0.925, ScoreRank.A, 5)] + [TestCase(0.85, ScoreRank.B, 9)] + [TestCase(0.75, ScoreRank.C, 11)] + [TestCase(0.5, ScoreRank.D, 21)] + [TestCase(0.2, ScoreRank.D, 51)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount) { TestResultsScreen screen = null; @@ -91,6 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); score.Accuracy = accuracy; score.Rank = rank; + score.Statistics[HitResult.Miss] = missCount; return screen = createResultsScreen(score); }); diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 8cbca74466..d2632c70c9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -111,6 +112,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly double accuracyD; private readonly bool withFlair; + private readonly bool isFailedSDueToMisses; + private RankText failedSRankText; + public AccuracyCircle(ScoreInfo score, bool withFlair = false) { this.score = score; @@ -119,10 +123,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); + + // Some rulesets require no misses to get an S rank. + // if (score.Accuracy >= accuracyS && score.Rank == ScoreRank.A) + // accuracyS = score.Accuracy + 0.0001; + accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); + + isFailedSDueToMisses = score.Accuracy >= accuracyS && score.Rank == ScoreRank.A; } [BackgroundDependencyLoader] @@ -249,6 +260,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { + if (isFailedSDueToMisses) + AddInternal(failedSRankText = new RankText(ScoreRank.S)); + AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), @@ -387,6 +401,31 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); } } + + if (isFailedSDueToMisses) + { + const double adjust_duration = 200; + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY - adjust_duration)) + { + failedSRankText.FadeIn(adjust_duration); + + using (BeginDelayedSequence(adjust_duration)) + { + failedSRankText + .FadeColour(Color4.Red, 800, Easing.Out) + .RotateTo(10, 1000, Easing.Out) + .MoveToY(100, 1000, Easing.In) + .FadeOut(800, Easing.Out); + + accuracyCircle + .FillTo(accuracyS - 0.0001, 70, Easing.OutQuint); + + badges.Single(b => b.Rank == ScoreRank.S) + .FadeOut(70, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 7af327828e..8aea6045eb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// private readonly double displayPosition; - private readonly ScoreRank rank; + public readonly ScoreRank Rank; private Drawable rankContainer; private Drawable overlay; @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { Accuracy = accuracy; displayPosition = position; - this.rank = rank; + Rank = rank; RelativeSizeAxes = Axes.Both; Alpha = 0; @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Size = new Vector2(28, 14), Children = new[] { - new DrawableRank(rank), + new DrawableRank(Rank), overlay = new CircularContainer { RelativeSizeAxes = Axes.Both, @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Colour = OsuColour.ForRank(Rank).Opacity(0.2f), Radius = 10, }, Child = new Box From 99d3fcc3261a5f1ec9f8270d734e5e5f93318ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 13:45:55 +0100 Subject: [PATCH 502/539] Fix crash if ruleset components container is not present --- osu.Game/Screens/Play/HUDOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a9fe393395..b5482f2a5b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -172,7 +172,10 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, rulesetComponents, playfieldComponents, topRightElements }; + hideTargets = new List { mainComponents, playfieldComponents, topRightElements }; + + if (rulesetComponents != null) + hideTargets.Add(rulesetComponents); if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); From 15df6b1da16cd745e45e60dc150bed29be2f2775 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 21:47:38 +0900 Subject: [PATCH 503/539] Revert keyboard seek speed change --- osu.Game/Screens/Play/ReplayPlayer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a26a2b9904..3c5b85662a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -107,11 +107,11 @@ namespace osu.Game.Screens.Play return true; case GlobalAction.SeekReplayBackward: - SeekInDirection(-1); + SeekInDirection(-5); return true; case GlobalAction.SeekReplayForward: - SeekInDirection(1); + SeekInDirection(5); return true; case GlobalAction.TogglePauseReplay: From 17d05b0fdfd392423c708bd368cd02a77116d0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 14:23:46 +0100 Subject: [PATCH 504/539] Fix non-miss drawable judgements fading out instantly on triangles skin `else if` proves to be insidious once again. --- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index c7103cd9fd..7330f138ce 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -49,8 +49,10 @@ namespace osu.Game.Rulesets.Judgements this.ScaleTo(1.2f, 100, Easing.In); this.FadeOutFromOne(400); + return; } - else if (Result.IsMiss()) + + if (Result.IsMiss()) { this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); @@ -60,9 +62,9 @@ namespace osu.Game.Rulesets.Judgements this.RotateTo(0); this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); } + + this.FadeOutFromOne(800); } public Drawable? GetAboveHitObjectsProxiedContent() => null; From 9aa7c7f59197b9754e98e1ba5ce5e4730f4d5f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 15:35:03 +0100 Subject: [PATCH 505/539] Revert incorrect removal of `exitConfirmed` flag Removing it meant that it was _literally impossible_ to exit multiplayer. --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index aab7ac3280..a37314de0e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -247,13 +247,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + private bool exitConfirmed; + public override bool OnExiting(ScreenExitEvent e) { // room has not been created yet or we're offline; exit immediately. if (client.Room == null || !IsConnected) return base.OnExiting(e); - if (dialogOverlay != null) + if (!exitConfirmed && dialogOverlay != null) { if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) confirmDialog.PerformOkAction(); @@ -261,6 +263,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { + exitConfirmed = true; if (this.IsCurrentScreen()) this.Exit(); })); From cb8ec48717b49110bb784ae69028612d546a7a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:56:30 +0100 Subject: [PATCH 506/539] Make `RankFromScore()`'s dictionary param readonly Just to make sure nobody tries any "funny" business. --- osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs | 2 +- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 2 +- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index a12fe7f3e1..12a4182bf1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 0d62a6e718..4d8381cf42 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Scoring { } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { ScoreRank rank = base.RankFromScore(accuracy, results); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 023cd1f902..441a53c05a 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring * strongScaleValue(result); } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { ScoreRank rank = base.RankFromScore(accuracy, results); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 95f672f6e6..a092829317 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public virtual ScoreRank RankFromScore(double accuracy, Dictionary results) + public virtual ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; From 20ed7e13e31c4da412e1cffab581e4b7054492cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:57:12 +0100 Subject: [PATCH 507/539] Fix back-to-front conditional in taiko processor Would be weird to degrade a score due to *no* misses wouldn't it? --- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 441a53c05a..7e40d575bc 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring { case ScoreRank.S: case ScoreRank.X: - if (results.GetValueOrDefault(HitResult.Miss) == 0) + if (results.GetValueOrDefault(HitResult.Miss) > 0) rank = ScoreRank.A; break; } From 45dc9de1e0d22f83ea2851bf8116c180724193da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:58:11 +0100 Subject: [PATCH 508/539] Remove remnant of old implementation of showing "A due to misses" --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index d2632c70c9..be9a3a8a78 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -124,10 +124,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); - // Some rulesets require no misses to get an S rank. - // if (score.Accuracy >= accuracyS && score.Rank == ScoreRank.A) - // accuracyS = score.Accuracy + 0.0001; - accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); From 81a02665b67a4f68dda204be6207acd6d9488cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 18:24:39 +0100 Subject: [PATCH 509/539] Adjust existing test to fail --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index e56dff74c3..234067ccda 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void AccuracyAndRankOfStableScorePreserved() + public void AccuracyOfStableScoreRecomputed() { var memoryStream = new MemoryStream(); @@ -261,15 +261,16 @@ namespace osu.Game.Tests.Beatmaps.Formats // and we want to emulate a stable score here using (var sw = new SerializationWriter(memoryStream, true)) { - sw.Write((byte)0); // ruleset id (osu!) + sw.Write((byte)3); // ruleset id (mania). + // mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test sw.Write("username"); // irrelevant to this test sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test - sw.Write((ushort)198); // count300 - sw.Write((ushort)1); // count100 + sw.Write((ushort)1); // count300 + sw.Write((ushort)0); // count100 sw.Write((ushort)0); // count50 - sw.Write((ushort)0); // countGeki + sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania) sw.Write((ushort)0); // countKatu sw.Write((ushort)1); // countMiss sw.Write(12345678); // total score, irrelevant to this test @@ -287,8 +288,8 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { - Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300))); - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); }); } From 3f31593a196a5bb7ce93ca4fafb91f45309043be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:01:18 +0100 Subject: [PATCH 510/539] Add another failing tests covering recomputation of ranks --- .../Formats/LegacyScoreDecoderTest.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 234067ccda..fd99b51a2f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -293,6 +293,47 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + [Test] + public void RankOfStableScoreUsesLazerDefinitions() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)195); // count300 + sw.Write((ushort)1); // count100 + sw.Write((ushort)4); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)0); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + }); + } + [Test] public void AccuracyOfLazerScorePreserved() { From aa8eee0796ffcc6e6a244f623f0a98ab69fb2c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 19:00:21 +0100 Subject: [PATCH 511/539] Move maximum statistics population to `LegacyScoreDecoder` --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 68 +++++++++++++++++++ osu.Game/Scoring/ScoreImporter.cs | 67 ------------------ osu.Game/Scoring/ScoreManager.cs | 9 ++- 3 files changed, 76 insertions(+), 68 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 427b1cb7cf..3d671d80c3 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -19,6 +20,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -130,6 +132,8 @@ namespace osu.Game.Scoring.Legacy } } + PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) PopulateLegacyAccuracyAndRank(score.ScoreInfo); else @@ -170,6 +174,70 @@ namespace osu.Game.Scoring.Legacy } } + /// + /// Populates the for a given . + /// + /// The score to populate the statistics of. + /// The corresponding . + internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) + { + Debug.Assert(score.BeatmapInfo != null); + + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) + return; + + var ruleset = score.Ruleset.Detach(); + var rulesetInstance = ruleset.CreateInstance(); + var scoreProcessor = rulesetInstance.CreateScoreProcessor(); + + Debug.Assert(rulesetInstance != null); + + // Populate the maximum statistics. + HitResult maxBasicResult = rulesetInstance.GetHitResults() + .Select(h => h.result) + .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); + + foreach ((HitResult result, int count) in score.Statistics) + { + switch (result) + { + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; + break; + + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; + break; + + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + case HitResult.SmallBonus: + case HitResult.LargeBonus: + break; + + default: + score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; + break; + } + } + + if (!score.IsLegacyScore) + return; + +#pragma warning disable CS0618 + // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. + // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. + var calculator = rulesetInstance.CreateDifficultyCalculator(workingBeatmap); + var attributes = calculator.Calculate(score.Mods); + + int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); + if (attributes.MaxCombo > maxComboFromStatistics) + score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; +#pragma warning restore CS0618 + } + /// /// Populates the accuracy of a given from its contained statistics. /// diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 8e28707107..afa522b253 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,7 +17,6 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using Realms; namespace osu.Game.Scoring @@ -91,8 +90,6 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); - PopulateMaximumStatistics(model); - if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); @@ -110,70 +107,6 @@ namespace osu.Game.Scoring } } - /// - /// Populates the for a given . - /// - /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) - { - Debug.Assert(score.BeatmapInfo != null); - - if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) - return; - - var beatmap = score.BeatmapInfo!.Detach(); - var ruleset = score.Ruleset.Detach(); - var rulesetInstance = ruleset.CreateInstance(); - var scoreProcessor = rulesetInstance.CreateScoreProcessor(); - - Debug.Assert(rulesetInstance != null); - - // Populate the maximum statistics. - HitResult maxBasicResult = rulesetInstance.GetHitResults() - .Select(h => h.result) - .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); - - foreach ((HitResult result, int count) in score.Statistics) - { - switch (result) - { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; - break; - - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; - break; - - case HitResult.IgnoreHit: - case HitResult.IgnoreMiss: - case HitResult.SmallBonus: - case HitResult.LargeBonus: - break; - - default: - score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; - break; - } - } - - if (!score.IsLegacyScore) - return; - -#pragma warning disable CS0618 - // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. - // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. - var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap)); - var attributes = calculator.Calculate(score.Mods); - - int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); - if (attributes.MaxCombo > maxComboFromStatistics) - score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; -#pragma warning restore CS0618 - } - // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 02d9e0a280..1ee99e9e93 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -26,6 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { + private readonly Func beatmaps; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; @@ -44,6 +46,7 @@ namespace osu.Game.Scoring OsuConfigManager configManager = null) : base(storage, realm) { + this.beatmaps = beatmaps; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -171,7 +174,11 @@ namespace osu.Game.Scoring /// Populates the for a given . /// /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public void PopulateMaximumStatistics(ScoreInfo score) + { + Debug.Assert(score.BeatmapInfo != null); + LegacyScoreDecoder.PopulateMaximumStatistics(score, beatmaps().GetWorkingBeatmap(score.BeatmapInfo.Detach())); + } #region Implementation of IPresentImports From 2958631c5d69fdd3af915d04f26825077213069e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 19:14:53 +0100 Subject: [PATCH 512/539] Use lazer accuracy & rank implementations across the board --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 11 +++++++++++ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 10 ++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 8c73806cb5..4221ab93f1 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -597,6 +597,17 @@ namespace osu.Game.Database return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) + { + Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); + var rank = ruleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + + foreach (var mod in scoreInfo.Mods.OfType()) + rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + + return rank; + } + /// /// Used to populate the model using data parsed from its corresponding replay file. /// diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 3d671d80c3..be704ca18b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -40,7 +40,6 @@ namespace osu.Game.Scoring.Legacy }; WorkingBeatmap workingBeatmap; - byte[] compressedScoreInfo = null; using (SerializationReader sr = new SerializationReader(stream)) { @@ -109,6 +108,8 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.LegacyOnlineID = sr.ReadInt32(); + byte[] compressedScoreInfo = null; + if (version >= 30000001) compressedScoreInfo = sr.ReadByteArray(); @@ -133,11 +134,8 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); - - if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) - PopulateLegacyAccuracyAndRank(score.ScoreInfo); - else - populateLazerAccuracyAndRank(score.ScoreInfo); + score.ScoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(score.ScoreInfo); + score.ScoreInfo.Rank = StandardisedScoreMigrationTools.ComputeRank(score.ScoreInfo); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. From db4849442ec7b93312afc39bbc009d4b1da62d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:23:37 +0100 Subject: [PATCH 513/539] Unify legacy total score / accuracy / rank recomputation flows --- .../Database/BackgroundDataStoreProcessor.cs | 6 +- .../StandardisedScoreMigrationTools.cs | 60 +++++++++++-------- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 15 +---- osu.Game/Scoring/ScoreImporter.cs | 5 -- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 324d699e7c..1e44141819 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -355,8 +355,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; - // TODO: ensure that this is also updating rank (as it will set TotalScoreVersion to latest). - StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager); + StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo)); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); @@ -412,8 +411,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; - // TODO: uncomment when ready - // s.Rank = StandardisedScoreMigrationTools.ComputeRank(s, beatmapManager); + s.Rank = StandardisedScoreMigrationTools.ComputeRank(s); }); ++processedCount; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 4221ab93f1..d62d4a905a 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -235,39 +234,49 @@ namespace osu.Game.Database /// Updates a legacy to standardised scoring. /// /// The score to update. - /// A used for lookups. - public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps) + /// The applicable for this score. + public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap) { - score.TotalScore = convertFromLegacyTotalScore(score, beatmaps); - score.Accuracy = ComputeAccuracy(score); + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); } /// /// Updates a legacy to standardised scoring. /// + /// + /// This overload is intended for server-side flows. + /// See: https://github.com/ppy/osu-queue-score-statistics/blob/3681e92ac91c6c61922094bdbc7e92e6217dd0fc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs + /// /// The score to update. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. - public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + public static void UpdateFromLegacy(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { - score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes); - score.Accuracy = ComputeAccuracy(score); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. - /// A used for lookups. + /// The in which the score was set. + /// The applicable for this score. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { if (!score.IsLegacyScore) return score.TotalScore; - WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); - Ruleset ruleset = score.Ruleset.CreateInstance(); - if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; @@ -283,27 +292,28 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); - return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return score.TotalScore; - Debug.Assert(score.LegacyTotalScore != null); - - Ruleset ruleset = score.Ruleset.CreateInstance(); if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + // ensure legacy total score is saved for later. + score.LegacyTotalScore = score.TotalScore; + double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier); @@ -584,11 +594,10 @@ namespace osu.Game.Database } } - public static double ComputeAccuracy(ScoreInfo scoreInfo) - { - Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + public static double ComputeAccuracy(ScoreInfo scoreInfo) => computeAccuracy(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + { int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) @@ -597,10 +606,11 @@ namespace osu.Game.Database return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } - public static ScoreRank ComputeRank(ScoreInfo scoreInfo) + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + + private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) { - Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); - var rank = ruleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); foreach (var mod in scoreInfo.Mods.OfType()) rank = mod.AdjustRank(rank, scoreInfo.Accuracy); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index be704ca18b..c12e8b8474 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -134,8 +134,7 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); - score.ScoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(score.ScoreInfo); - score.ScoreInfo.Rank = StandardisedScoreMigrationTools.ComputeRank(score.ScoreInfo); + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. @@ -342,18 +341,6 @@ namespace osu.Game.Scoring.Legacy } } - private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo) - { - scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); - - var rank = currentRuleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); - - foreach (var mod in scoreInfo.Mods.OfType()) - rank = mod.AdjustRank(rank, scoreInfo.Accuracy); - - scoreInfo.Rank = rank; - } - private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index afa522b253..768c28cc38 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -100,11 +100,6 @@ namespace osu.Game.Scoring // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); - else if (model.IsLegacyScore) - { - model.LegacyTotalScore = model.TotalScore; - StandardisedScoreMigrationTools.UpdateFromLegacy(model, beatmaps()); - } } // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). From 217cbf684b249c220b1c867bca92f0fd4e4a0361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:25:29 +0100 Subject: [PATCH 514/539] Remove superfluous recomputation of accuracy --- .../Database/StandardisedScoreMigrationTools.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index d62d4a905a..5fcf35b690 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -240,9 +240,10 @@ namespace osu.Game.Database var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); } /// @@ -260,9 +261,10 @@ namespace osu.Game.Database { var scoreProcessor = ruleset.CreateScoreProcessor(); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// @@ -484,14 +486,9 @@ namespace osu.Game.Database break; case 3: - // in the mania case accuracy actually changes between score V1 and score V2 / standardised - // (PERFECT weighting changes from 300 to 305), - // so for better accuracy recompute accuracy locally based on hit statistics and use that instead, - double scoreV2Accuracy = ComputeAccuracy(score); - convertedTotalScore = (long)Math.Round(( 850000 * comboProportion - + 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy) + + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + bonusProportion) * modMultiplier); break; @@ -594,8 +591,6 @@ namespace osu.Game.Database } } - public static double ComputeAccuracy(ScoreInfo scoreInfo) => computeAccuracy(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); - private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) { int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) From cdd6e71d0129bce737995ae888d015589f7e43f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:26:30 +0100 Subject: [PATCH 515/539] Remove legacy computation of accuracy & ranks --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c12e8b8474..a7374d4d4b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -235,112 +235,6 @@ namespace osu.Game.Scoring.Legacy #pragma warning restore CS0618 } - /// - /// Populates the accuracy of a given from its contained statistics. - /// - /// - /// Legacy use only. - /// - /// The to populate. - public static void PopulateLegacyAccuracyAndRank(ScoreInfo score) - { - int countMiss = score.GetCountMiss() ?? 0; - int count50 = score.GetCount50() ?? 0; - int count100 = score.GetCount100() ?? 0; - int count300 = score.GetCount300() ?? 0; - int countGeki = score.GetCountGeki() ?? 0; - int countKatu = score.GetCountKatu() ?? 0; - - switch (score.Ruleset.OnlineID) - { - case 0: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 1: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 2: - { - int totalHits = count50 + count100 + count300 + countMiss + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.98) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.94) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.85) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 3: - { - int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.95) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.8) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.7) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - } - } - private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; From d53fb5a1652a3ae50cb89a2e8609da752a319df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:52:45 +0100 Subject: [PATCH 516/539] Update assertions to match expected behaviour --- .../Formats/LegacyScoreDecoderTest.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index fd99b51a2f..7e3967dc95 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -10,7 +10,6 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; @@ -23,6 +22,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -59,14 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); - Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic)); Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL")); Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL")); - Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001)); Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Is.Not.Empty); @@ -289,7 +289,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); }); } @@ -330,12 +330,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); }); } [Test] - public void AccuracyOfLazerScorePreserved() + public void AccuracyRankAndTotalScoreOfLazerScorePreserved() { var ruleset = new OsuRuleset().RulesetInfo; @@ -363,6 +363,8 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537)); + Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null); Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); @@ -457,6 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Ruleset = new OsuRuleset().RulesetInfo, Difficulty = new BeatmapDifficulty(), BeatmapVersion = beatmapVersion, + }, + // needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die + // when trying to recompute total score. + HitObjects = + { + new HitCircle() } }); } From 24be6d92ceebf8950bb7bddd44710eeaac92f10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 21:13:06 +0100 Subject: [PATCH 517/539] Expand xmldoc --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 5fcf35b690..bf92dbc8f5 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -231,7 +231,10 @@ namespace osu.Game.Database } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompite the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// /// The score to update. /// The applicable for this score. @@ -247,7 +250,10 @@ namespace osu.Game.Database } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompute the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// /// /// This overload is intended for server-side flows. From 069af13aaf1d1c716ce207f9c9d2eab48f013eaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jan 2024 05:26:16 +0900 Subject: [PATCH 518/539] Reduce enumerator overhead in `GameplayLeaderboard` --- osu.Game/Online/Leaderboards/Leaderboard.cs | 4 ++-- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 67f2590ad8..0fd9597ac0 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -287,7 +287,7 @@ namespace osu.Game.Online.Leaderboards double delay = 0; - foreach (var s in scoreFlowContainer.Children) + foreach (var s in scoreFlowContainer) { using (s.BeginDelayedSequence(delay)) s.Show(); @@ -384,7 +384,7 @@ namespace osu.Game.Online.Leaderboards if (scoreFlowContainer == null) return; - foreach (var c in scoreFlowContainer.Children) + foreach (var c in scoreFlowContainer) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoreFlowContainer).Y; float bottomY = topY + LeaderboardScore.HEIGHT; diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d990af32e7..d2b6b834f8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; // logic is mostly shared with Leaderboard, copied here for simplicity. - foreach (var c in Flow.Children) + foreach (var c in Flow) { float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y; float bottomY = topY + panel_height; From 02bb506cce02df332ff4f884462bc5e24dfac949 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jan 2024 05:32:11 +0900 Subject: [PATCH 519/539] Avoid using `.Children` for enumeration in other locations --- osu.Desktop.slnf | 13 ++++++------- osu.Game/Graphics/Containers/WaveContainer.cs | 4 ++-- .../Graphics/UserInterface/BreadcrumbControl.cs | 2 +- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 4 ++-- osu.Game/Graphics/UserInterface/StarCounter.cs | 2 +- osu.Game/Overlays/Chat/Listing/ChannelListing.cs | 2 +- osu.Game/Overlays/OverlayStreamControl.cs | 4 ++-- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- .../HUD/JudgementCounter/JudgementCounterDisplay.cs | 2 +- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 10 files changed, 18 insertions(+), 19 deletions(-) diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf index 503e5935f5..606988ccdf 100644 --- a/osu.Desktop.slnf +++ b/osu.Desktop.slnf @@ -16,15 +16,14 @@ "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament\\osu.Game.Tournament.csproj", "osu.Game\\osu.Game.csproj", - - "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", - "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", + "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", - "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj" + "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", + "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj" ] } -} +} \ No newline at end of file diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 5abc66d2ac..2ae4dc5a76 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.Containers protected override void PopIn() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Show(); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); @@ -132,7 +132,7 @@ namespace osu.Game.Graphics.Containers protected override void PopOut() { - foreach (var w in wavesContainer.Children) + foreach (var w in wavesContainer) w.Hide(); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index fc0770d896..af4b3849af 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -33,7 +33,7 @@ namespace osu.Game.Graphics.UserInterface Current.ValueChanged += index => { - foreach (var t in TabContainer.Children.OfType()) + foreach (var t in TabContainer.OfType()) { int tIndex = TabContainer.IndexOf(t); int tabIndex = TabContainer.IndexOf(TabMap[index.NewValue]); diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 05309760e7..c260c92b43 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface if (Dropdown is IHasAccentColour dropdown) dropdown.AccentColour = value; - foreach (var i in TabContainer.Children.OfType()) + foreach (var i in TabContainer.OfType()) i.AccentColour = value; } } @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - protected virtual float StripWidth => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; + protected virtual float StripWidth => TabContainer.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; /// /// Whether entries should be automatically populated if is an type. diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index fe986b275e..720f479216 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -101,7 +101,7 @@ namespace osu.Game.Graphics.UserInterface public void StopAnimation() { animate(current); - foreach (var star in stars.Children) + foreach (var star in stars) star.FinishTransforms(true); } diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs index 809ea2f11d..1699dcceb0 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Chat.Listing flow.ChildrenEnumerable = newChannels.Where(c => c.Type == ChannelType.Public) .Select(c => new ChannelListingItem(c)); - foreach (var item in flow.Children) + foreach (var item in flow) { item.OnRequestJoin += channel => OnRequestJoin?.Invoke(channel); item.OnRequestLeave += channel => OnRequestLeave?.Invoke(channel); diff --git a/osu.Game/Overlays/OverlayStreamControl.cs b/osu.Game/Overlays/OverlayStreamControl.cs index 84de384fb5..bc37a57cab 100644 --- a/osu.Game/Overlays/OverlayStreamControl.cs +++ b/osu.Game/Overlays/OverlayStreamControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -49,7 +49,7 @@ namespace osu.Game.Overlays protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.OfType>()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index b2b3fbd626..d742d2377f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -343,7 +343,7 @@ namespace osu.Game.Screens.Menu { buttonArea.ButtonSystemState = state; - foreach (var b in buttonArea.Children.OfType()) + foreach (var b in buttonArea.OfType()) b.ButtonSystemState = state; } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 326be55222..25e5464205 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter CounterFlow.Direction = convertedDirection; - foreach (var counter in CounterFlow.Children) + foreach (var counter in CounterFlow) counter.Direction.Value = convertedDirection; }, true); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 369db37e63..bd659d7423 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -242,7 +242,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in beatmapContainer.Children) + foreach (var panel in beatmapContainer) { Debug.Assert(panel.Item != null); From fdd499a4859bcd682dc09156b3dcef1a8a32bcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 21:34:28 +0100 Subject: [PATCH 520/539] Fix rank background processing not working (and not bumping score version) --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 1e44141819..27d19f86c8 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -383,6 +383,7 @@ namespace osu.Game.Database HashSet scoreIds = realmAccess.Run(r => new HashSet( r.All() .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() // need to materialise here as realm cannot support `.Select()`. .Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); @@ -412,6 +413,7 @@ namespace osu.Game.Database { ScoreInfo s = r.Find(id)!; s.Rank = StandardisedScoreMigrationTools.ComputeRank(s); + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); ++processedCount; From 4d253ebf3c98cc8b923695c7a94b4360143d7d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 22:01:03 +0100 Subject: [PATCH 521/539] Remove redundant assertion It does look pretty dumb to be checking if something is `null` *after calling an instance method on it*... --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a7374d4d4b..63bd822205 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -187,8 +187,6 @@ namespace osu.Game.Scoring.Legacy var rulesetInstance = ruleset.CreateInstance(); var scoreProcessor = rulesetInstance.CreateScoreProcessor(); - Debug.Assert(rulesetInstance != null); - // Populate the maximum statistics. HitResult maxBasicResult = rulesetInstance.GetHitResults() .Select(h => h.result) From da992ccc55e6712b18b3b91b8864e7c44238f9ca Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 23 Jan 2024 04:54:27 +0300 Subject: [PATCH 522/539] Implement per-axis triangles clamping --- .../Skinning/Default/TrianglesPiece.cs | 3 +- .../TestSceneTrianglesBackground.cs | 8 +++-- .../TestSceneTrianglesV2Background.cs | 8 +++-- osu.Game/Graphics/Backgrounds/Triangles.cs | 36 +++++++++++-------- osu.Game/Graphics/Backgrounds/TrianglesV2.cs | 36 +++++++++++-------- .../Graphics/UserInterface/DialogButton.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 4 +-- 7 files changed, 59 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index 566176505d..512ac8ee3e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; using osu.Game.Graphics.Backgrounds; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; - ClampToDrawable = false; + ClampAxes = Axes.None; } protected override void Update() diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs index 4733b7f92f..dd4c372193 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -29,7 +29,8 @@ namespace osu.Game.Tests.Visual.Background ColourDark = Color4.Gray, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(0.9f) + Size = new Vector2(0.9f), + ClampAxes = Axes.None } }; } @@ -40,7 +41,10 @@ namespace osu.Game.Tests.Visual.Background AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s)); - AddToggleStep("ClampToDrawable", c => triangles.ClampToDrawable = c); + AddStep("ClampAxes X", () => triangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => triangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => triangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => triangles.ClampAxes = Axes.None); } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs index 71d1baddc8..4713852c0b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs @@ -86,7 +86,8 @@ namespace osu.Game.Tests.Visual.Background { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + ClampAxes = Axes.None } } }, @@ -128,7 +129,10 @@ namespace osu.Game.Tests.Visual.Background AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White); AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); - AddToggleStep("ClampToDrawable", c => maskedTriangles.ClampToDrawable = c); + AddStep("ClampAxes X", () => maskedTriangles.ClampAxes = Axes.X); + AddStep("ClampAxes Y", () => maskedTriangles.ClampAxes = Axes.Y); + AddStep("ClampAxes Both", () => maskedTriangles.ClampAxes = Axes.Both); + AddStep("ClampAxes None", () => maskedTriangles.ClampAxes = Axes.None); } } } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index d7bfeb7731..e877915fac 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -77,10 +77,10 @@ namespace osu.Game.Graphics.Backgrounds } /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. Default is true. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool ClampToDrawable { get; set; } = true; + public Axes ClampAxes { get; set; } = Axes.Both; /// /// Whether we should drop-off alpha values of triangles more quickly to improve @@ -257,7 +257,7 @@ namespace osu.Game.Graphics.Backgrounds private IShader shader; private Texture texture; - private bool clamp; + private Axes clampAxes; private readonly List parts = new List(); private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; @@ -276,7 +276,7 @@ namespace osu.Game.Graphics.Backgrounds shader = Source.shader; texture = Source.texture; size = Source.DrawSize; - clamp = Source.ClampToDrawable; + clampAxes = Source.ClampAxes; parts.Clear(); parts.AddRange(Source.parts); @@ -306,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -331,17 +331,23 @@ namespace osu.Game.Graphics.Backgrounds shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index f723b1b358..706b05f5ad 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -33,10 +33,10 @@ namespace osu.Game.Graphics.Backgrounds protected virtual bool CreateNewTriangles => true; /// - /// If enabled, only the portion of triangles that falls within this 's - /// shape is drawn to the screen. Default is true. + /// Controls on which the portion of triangles that falls within this 's + /// shape is drawn to the screen. Default is Axes.Both. /// - public bool ClampToDrawable { get; set; } = true; + public Axes ClampAxes { get; set; } = Axes.Both; private readonly BindableFloat spawnRatio = new BindableFloat(1f); @@ -193,7 +193,7 @@ namespace osu.Game.Graphics.Backgrounds private Vector2 size; private float thickness; private float texelSize; - private bool clamp; + private Axes clampAxes; public TrianglesDrawNode(TrianglesV2 source) : base(source) @@ -208,7 +208,7 @@ namespace osu.Game.Graphics.Backgrounds texture = Source.texture; size = Source.DrawSize; thickness = Source.Thickness; - clamp = Source.ClampToDrawable; + clampAxes = Source.ClampAxes; Quad triangleQuad = new Quad( Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix), @@ -248,7 +248,7 @@ namespace osu.Game.Graphics.Backgrounds { Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); - Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + Quad triangleQuad = getClampedQuad(clampAxes, topLeft, relativeSize); var drawQuad = new Quad( Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), @@ -270,17 +270,23 @@ namespace osu.Game.Graphics.Backgrounds shader.Unbind(); } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + private static Quad getClampedQuad(Axes clampAxes, Vector2 topLeft, Vector2 size) { - float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); - float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + Vector2 clampedTopLeft = topLeft; - return new Quad( - leftClamped, - topClamped, - Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, - Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped - ); + if (clampAxes == Axes.X || clampAxes == Axes.Both) + { + clampedTopLeft.X = Math.Clamp(topLeft.X, 0f, 1f); + size.X = Math.Clamp(topLeft.X + size.X, 0f, 1f) - clampedTopLeft.X; + } + + if (clampAxes == Axes.Y || clampAxes == Axes.Both) + { + clampedTopLeft.Y = Math.Clamp(topLeft.Y, 0f, 1f); + size.Y = Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - clampedTopLeft.Y; + } + + return new Quad(clampedTopLeft.X, clampedTopLeft.Y, size.X, size.Y); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 10aca017f1..c39f41bf72 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -150,7 +150,7 @@ namespace osu.Game.Graphics.UserInterface TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), Shear = new Vector2(-0.2f, 0), - ClampToDrawable = false + ClampAxes = Axes.Y }, }, }, diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 05454159c7..b2c5a054e1 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Mods var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV(); var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f); - triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f)); + triangles.Colour = ColourInfo.GradientVertical(trianglesColour, value); } } @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Velocity = 0.7f, - ClampToDrawable = false + ClampAxes = Axes.Y }, headerText = new OsuTextFlowContainer(t => { From c8cd7ebe6c66b57cbcfef2d841deacb60fdbd755 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jan 2024 17:29:31 +0900 Subject: [PATCH 523/539] Fix now playing beatmap backgrounds not being correctly centred https://github.com/ppy/osu/discussions/26679 --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 7ec52364d4..ab99370603 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -405,6 +405,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(150), FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, new Box { From 0cf90677e616793ca3ea7e7da1c1a74b7b80e7b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jan 2024 19:16:55 +0900 Subject: [PATCH 524/539] Apply more correct visual offset adjustment Co-authored-by: Walavouchey <36758269+Walavouchey@users.noreply.github.com> --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index be9a3a8a78..e7e54d0fae 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -415,7 +415,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy .FadeOut(800, Easing.Out); accuracyCircle - .FillTo(accuracyS - 0.0001, 70, Easing.OutQuint); + .FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); badges.Single(b => b.Rank == ScoreRank.S) .FadeOut(70, Easing.OutQuint); From cb87d6ce500bd8070b1a0ad69537a6f643e1aa67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 11:05:29 +0100 Subject: [PATCH 525/539] Move transferal of `LegacyTotalScore` back to original spot This subtle detail was messing with server-side score import flows. Server-side, legacy total score will *already* be in `LegacyTotalScore` from the start, and `TotalScore` will be zero until recomputed via `StandardisedScoreMigrationTools.UpdateFromLegacy()` - so in that context, attempting to move it across is incorrect. --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 6 +++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index bf92dbc8f5..403e73ab77 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -316,12 +317,11 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; + Debug.Assert(score.LegacyTotalScore != null); + if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - // ensure legacy total score is saved for later. - score.LegacyTotalScore = score.TotalScore; - double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 63bd822205..e51a95798b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -134,6 +134,10 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. From 0b5be3c40cd0ccb2c1ab190c719d44d6d4521268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 12:53:16 +0100 Subject: [PATCH 526/539] Remove outdated test Non-legacy scores *are* subject to upgrades again - albeit it is *rank* upgrades that they are subject to. --- .../BackgroundDataStoreProcessorTests.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index e22cf2398b..e960995c45 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -211,31 +211,6 @@ namespace osu.Game.Tests.Database AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); } - [Test] - public void TestNonLegacyScoreNotSubjectToUpgrades() - { - ScoreInfo scoreInfo = null!; - TestBackgroundDataStoreProcessor processor = null!; - - AddStep("Add score which requires upgrade (and has beatmap)", () => - { - Realm.Write(r => - { - r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) - { - TotalScoreVersion = 30000005, - LegacyTotalScore = 123456, - }); - }); - }); - - AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); - AddUntilStep("Wait for completion", () => processor.Completed); - - AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); - AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005)); - } - public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; From 6c169e3156af665059df1a526495eee5cec6aa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 12:59:35 +0100 Subject: [PATCH 527/539] Do not reprocess ranks for custom rulesets Chances are that because we've broken rank API, it would utterly fail for all custom rulesets anyhow. --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 27d19f86c8..be0c83bdb3 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -383,7 +383,10 @@ namespace osu.Game.Database HashSet scoreIds = realmAccess.Run(r => new HashSet( r.All() .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) - .AsEnumerable() // need to materialise here as realm cannot support `.Select()`. + .AsEnumerable() + // must be done after materialisation, as realm doesn't support + // filtering on nested property predicates or projection via `.Select()` + .Where(s => s.Ruleset.IsLegacyRuleset()) .Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); From 67010fcd03b1546ef0bbf24fdf864db69ad5bb58 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 24 Jan 2024 23:45:07 +0300 Subject: [PATCH 528/539] Reduce allocation overhead in ScoreCounter --- osu.Game/Graphics/UserInterface/ScoreCounter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 255b2149f0..62cdefda43 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -4,7 +4,6 @@ #nullable disable using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; @@ -39,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface protected override double GetProportionalDuration(long currentValue, long newValue) => currentValue > newValue ? currentValue - newValue : newValue - currentValue; - protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(formatString); + protected override LocalisableString FormatCount(long count) => count.ToString(formatString); protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); From 9e1c24271394770313e5b06e6b89ed1e73da5056 Mon Sep 17 00:00:00 2001 From: Mike Will Date: Wed, 24 Jan 2024 08:01:41 -0500 Subject: [PATCH 529/539] Prevent custom divisor ranges from halting preset cycling A custom divisor like 24 or 32 will result in a range containing many divisors that are already in the `Common` and `Triplets` presets. When this happens, it can become impossible to cycle between presets, because the preset can only be changed if the new divisor isn't already contained within the current preset's range. --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 7 +++++++ osu.Game/Screens/Edit/BindableBeatDivisor.cs | 5 +++-- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index f2b3351533..6c36e6729e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -210,6 +210,13 @@ namespace osu.Game.Tests.Visual.Editing switchPresets(-1); assertPreset(BeatDivisorType.Custom, 15); assertBeatSnap(15); + + setDivisorViaInput(24); + assertPreset(BeatDivisorType.Custom, 24); + switchPresets(1); + assertPreset(BeatDivisorType.Common); + switchPresets(-2); + assertPreset(BeatDivisorType.Triplets); } private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index ffa4f01e75..87cb191a82 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -29,10 +29,11 @@ namespace osu.Game.Screens.Edit /// Set a divisor, updating the valid divisor range appropriately. /// /// The intended divisor. - public void SetArbitraryDivisor(int divisor) + /// Ignores the current valid divisor range when true. + public void SetArbitraryDivisor(int divisor, bool force = false) { // If the current valid divisor range doesn't contain the proposed value, attempt to find one which does. - if (!ValidDivisors.Value.Presets.Contains(divisor)) + if (force || !ValidDivisors.Value.Presets.Contains(divisor)) { if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor)) ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index b33edb9edb..da1a37d57f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -208,11 +208,11 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (currentType) { case BeatDivisorType.Common: - beatDivisor.SetArbitraryDivisor(4); + beatDivisor.SetArbitraryDivisor(4, true); break; case BeatDivisorType.Triplets: - beatDivisor.SetArbitraryDivisor(6); + beatDivisor.SetArbitraryDivisor(6, true); break; case BeatDivisorType.Custom: From 781196858209dcc6804182b9d8dfac2896e6fd1d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 25 Jan 2024 03:28:02 +0300 Subject: [PATCH 530/539] Fix ArgonScoreCounter is still using localisation --- osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 348327d710..44b9fb3123 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } - protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); + protected override LocalisableString FormatCount(long count) => count.ToString(); protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) { From 30e335233da019c7e72203a2589fbe8fd9484476 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Thu, 25 Jan 2024 09:27:10 +0800 Subject: [PATCH 531/539] Crop screenshot image to scaling container. --- osu.Game/Graphics/ScreenshotManager.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 26e499ae9a..47ce0d31eb 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; namespace osu.Game.Graphics { @@ -52,6 +53,9 @@ namespace osu.Game.Graphics private INotificationOverlay notificationOverlay { get; set; } private Sample shutter; + private Bindable sizeX; + private Bindable sizeY; + private Bindable scalingMode; [BackgroundDependencyLoader] private void load(OsuConfigManager config, Storage storage, AudioManager audio) @@ -62,6 +66,10 @@ namespace osu.Game.Graphics captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); shutter = audio.Samples.Get("UI/shutter"); + + sizeX = config.GetBindable(OsuSetting.ScalingSizeX); + sizeY = config.GetBindable(OsuSetting.ScalingSizeY); + scalingMode = config.GetBindable(OsuSetting.Scaling); } public bool OnPressed(KeyBindingPressEvent e) @@ -119,6 +127,19 @@ namespace osu.Game.Graphics using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { + if (scalingMode.Value == ScalingMode.Everything) + { + image.Mutate(m => + { + var size = m.GetCurrentSize(); + var rect = new Rectangle(Point.Empty, size); + int sx = (size.Width - (int)(size.Width * sizeX.Value)) / 2; + int sy = (size.Height - (int)(size.Height * sizeY.Value)) / 2; + rect.Inflate(-sx, -sy); + m.Crop(rect); + }); + } + clipboard.SetImage(image); (string filename, var stream) = getWritableStream(); From b20051fd55b9977152b22e1d3a86dba86d9b0839 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Thu, 25 Jan 2024 10:35:41 +0800 Subject: [PATCH 532/539] offset crop rectangle to scaling container --- osu.Game/Graphics/ScreenshotManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 47ce0d31eb..a60d55cbef 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -53,6 +53,8 @@ namespace osu.Game.Graphics private INotificationOverlay notificationOverlay { get; set; } private Sample shutter; + private Bindable posX; + private Bindable posY; private Bindable sizeX; private Bindable sizeY; private Bindable scalingMode; @@ -67,6 +69,8 @@ namespace osu.Game.Graphics shutter = audio.Samples.Get("UI/shutter"); + posX = config.GetBindable(OsuSetting.ScalingPositionX); + posY = config.GetBindable(OsuSetting.ScalingPositionY); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); sizeY = config.GetBindable(OsuSetting.ScalingSizeY); scalingMode = config.GetBindable(OsuSetting.Scaling); @@ -136,6 +140,8 @@ namespace osu.Game.Graphics int sx = (size.Width - (int)(size.Width * sizeX.Value)) / 2; int sy = (size.Height - (int)(size.Height * sizeY.Value)) / 2; rect.Inflate(-sx, -sy); + rect.X = (int)(rect.X * posX.Value) * 2; + rect.Y = (int)(rect.Y * posY.Value) * 2; m.Crop(rect); }); } From d2990170d021f2caee2c6e66368f07792c7ac53c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 13:53:31 +0900 Subject: [PATCH 533/539] Add retry loop to avoid log export failing occasionally on windows Closes https://github.com/ppy/osu/issues/26693. --- osu.Game/IO/MigratableStorage.cs | 40 ++--------- .../Sections/General/UpdateSettings.cs | 4 +- osu.Game/Utils/FileUtils.cs | 72 +++++++++++++++++++ 3 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 osu.Game/Utils/FileUtils.cs diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 14a3c5a43c..d03d259f71 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -6,8 +6,8 @@ using System; using System.IO; using System.Linq; -using System.Threading; using osu.Framework.Platform; +using osu.Game.Utils; namespace osu.Game.IO { @@ -81,7 +81,7 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => fi.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -92,11 +92,11 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + allFilesDeleted &= FileUtils.AttemptOperation(target.Delete, throwOnFailure: false); return allFilesDeleted; } @@ -115,7 +115,7 @@ namespace osu.Game.IO if (IgnoreSuffixes.Any(suffix => fileInfo.Name.EndsWith(suffix, StringComparison.Ordinal))) continue; - AttemptOperation(() => + FileUtils.AttemptOperation(() => { fileInfo.Refresh(); @@ -139,35 +139,5 @@ namespace osu.Game.IO CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); } } - - /// - /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. - /// - /// The action to perform. - /// The number of attempts (250ms wait between each). - /// Whether to throw an exception on failure. If false, will silently fail. - protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) - { - while (true) - { - try - { - action(); - return true; - } - catch (Exception) - { - if (attempts-- == 0) - { - if (throwOnFailure) - throw; - - return false; - } - } - - Thread.Sleep(250); - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 3ff5556f4d..fe88413e6a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -15,6 +15,7 @@ using osu.Game.Localisation; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; +using osu.Game.Utils; using SharpCompress.Archives.Zip; namespace osu.Game.Overlays.Settings.Sections.General @@ -111,7 +112,8 @@ namespace osu.Game.Overlays.Settings.Sections.General using (var outStream = storage.CreateFileSafely(archive_filename)) using (var zip = ZipArchive.Create()) { - foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) zip.AddEntry(f, logStorage.GetStream(f), true); + foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) + FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip); zip.SaveTo(outStream); } diff --git a/osu.Game/Utils/FileUtils.cs b/osu.Game/Utils/FileUtils.cs new file mode 100644 index 0000000000..063ab178f7 --- /dev/null +++ b/osu.Game/Utils/FileUtils.cs @@ -0,0 +1,72 @@ +// 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.Threading; + +namespace osu.Game.Utils +{ + public static class FileUtils + { + /// + /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. + /// + /// The action to perform. + /// The provided state. + /// The number of attempts (250ms wait between each). + /// Whether to throw an exception on failure. If false, will silently fail. + public static bool AttemptOperation(Action action, T state, int attempts = 10, bool throwOnFailure = true) + { + while (true) + { + try + { + action(state); + return true; + } + catch (Exception) + { + if (attempts-- == 0) + { + if (throwOnFailure) + throw; + + return false; + } + } + + Thread.Sleep(250); + } + } + + /// + /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. + /// + /// The action to perform. + /// The number of attempts (250ms wait between each). + /// Whether to throw an exception on failure. If false, will silently fail. + public static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) + { + while (true) + { + try + { + action(); + return true; + } + catch (Exception) + { + if (attempts-- == 0) + { + if (throwOnFailure) + throw; + + return false; + } + } + + Thread.Sleep(250); + } + } + } +} From 0cbba7e011bd91d4d9883adc4d5c772c6789670a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 14:18:20 +0900 Subject: [PATCH 534/539] Apply NRT to `ScreenshotManager` --- osu.Game/Graphics/ScreenshotManager.cs | 39 ++++++++++++-------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index a60d55cbef..d952460c47 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Threading; @@ -38,37 +36,36 @@ namespace osu.Game.Graphics /// public IBindable CursorVisibility => cursorVisibility; - private Bindable screenshotFormat; - private Bindable captureMenuCursor; + [Resolved] + private GameHost host { get; set; } = null!; [Resolved] - private GameHost host { get; set; } + private Clipboard clipboard { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } + private INotificationOverlay notificationOverlay { get; set; } = null!; - private Storage storage; + private Storage storage = null!; - [Resolved] - private INotificationOverlay notificationOverlay { get; set; } + private Sample? shutter; - private Sample shutter; - private Bindable posX; - private Bindable posY; - private Bindable sizeX; - private Bindable sizeY; - private Bindable scalingMode; + private Bindable screenshotFormat = null!; + private Bindable captureMenuCursor = null!; + private Bindable posX = null!; + private Bindable posY = null!; + private Bindable sizeX = null!; + private Bindable sizeY = null!; + private Bindable scalingMode = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config, Storage storage, AudioManager audio) { this.storage = storage.GetStorageForDirectory(@"screenshots"); - screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); - captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); - shutter = audio.Samples.Get("UI/shutter"); + screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); + captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); posX = config.GetBindable(OsuSetting.ScalingPositionX); posY = config.GetBindable(OsuSetting.ScalingPositionY); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); @@ -84,7 +81,7 @@ namespace osu.Game.Graphics switch (e.Action) { case GlobalAction.TakeScreenshot: - shutter.Play(); + shutter?.Play(); TakeScreenshotAsync().FireAndForget(); return true; } @@ -148,7 +145,7 @@ namespace osu.Game.Graphics clipboard.SetImage(image); - (string filename, var stream) = getWritableStream(); + (string? filename, Stream? stream) = getWritableStream(); if (filename == null) return; @@ -191,7 +188,7 @@ namespace osu.Game.Graphics private static readonly object filename_reservation_lock = new object(); - private (string filename, Stream stream) getWritableStream() + private (string? filename, Stream? stream) getWritableStream() { lock (filename_reservation_lock) { From 85927e06827eaa3660f00191eac6b4251e8db7b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 14:22:27 +0900 Subject: [PATCH 535/539] Move configuration retrieval to non-bindable inline for readability --- osu.Game/Graphics/ScreenshotManager.cs | 65 ++++++++++++-------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index d952460c47..ca6a3fe84f 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace osu.Game.Graphics @@ -45,32 +46,18 @@ namespace osu.Game.Graphics [Resolved] private INotificationOverlay notificationOverlay { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + private Storage storage = null!; private Sample? shutter; - private Bindable screenshotFormat = null!; - private Bindable captureMenuCursor = null!; - private Bindable posX = null!; - private Bindable posY = null!; - private Bindable sizeX = null!; - private Bindable sizeY = null!; - private Bindable scalingMode = null!; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, Storage storage, AudioManager audio) + private void load(Storage storage, AudioManager audio) { this.storage = storage.GetStorageForDirectory(@"screenshots"); - shutter = audio.Samples.Get("UI/shutter"); - - screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); - captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); - posX = config.GetBindable(OsuSetting.ScalingPositionX); - posY = config.GetBindable(OsuSetting.ScalingPositionY); - sizeX = config.GetBindable(OsuSetting.ScalingSizeX); - sizeY = config.GetBindable(OsuSetting.ScalingSizeY); - scalingMode = config.GetBindable(OsuSetting.Scaling); } public bool OnPressed(KeyBindingPressEvent e) @@ -99,9 +86,19 @@ namespace osu.Game.Graphics { Interlocked.Increment(ref screenShotTasks); + ScreenshotFormat screenshotFormat = config.Get(OsuSetting.ScreenshotFormat); + bool captureMenuCursor = config.Get(OsuSetting.ScreenshotCaptureMenuCursor); + + float posX = config.Get(OsuSetting.ScalingPositionX); + float posY = config.Get(OsuSetting.ScalingPositionY); + float sizeX = config.Get(OsuSetting.ScalingSizeX); + float sizeY = config.Get(OsuSetting.ScalingSizeY); + + ScalingMode scalingMode = config.Get(OsuSetting.Scaling); + try { - if (!captureMenuCursor.Value) + if (!captureMenuCursor) { cursorVisibility.Value = false; @@ -110,7 +107,7 @@ namespace osu.Game.Graphics int framesWaited = 0; - using (var framesWaitedEvent = new ManualResetEventSlim(false)) + using (ManualResetEventSlim framesWaitedEvent = new ManualResetEventSlim(false)) { ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() => { @@ -126,32 +123,32 @@ namespace osu.Game.Graphics } } - using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) + using (Image? image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { - if (scalingMode.Value == ScalingMode.Everything) + if (scalingMode == ScalingMode.Everything) { image.Mutate(m => { - var size = m.GetCurrentSize(); - var rect = new Rectangle(Point.Empty, size); - int sx = (size.Width - (int)(size.Width * sizeX.Value)) / 2; - int sy = (size.Height - (int)(size.Height * sizeY.Value)) / 2; + Size size = m.GetCurrentSize(); + Rectangle rect = new Rectangle(Point.Empty, size); + int sx = (size.Width - (int)(size.Width * sizeX)) / 2; + int sy = (size.Height - (int)(size.Height * sizeY)) / 2; rect.Inflate(-sx, -sy); - rect.X = (int)(rect.X * posX.Value) * 2; - rect.Y = (int)(rect.Y * posY.Value) * 2; + rect.X = (int)(rect.X * posX) * 2; + rect.Y = (int)(rect.Y * posY) * 2; m.Crop(rect); }); } clipboard.SetImage(image); - (string? filename, Stream? stream) = getWritableStream(); + (string? filename, Stream? stream) = getWritableStream(screenshotFormat); if (filename == null) return; using (stream) { - switch (screenshotFormat.Value) + switch (screenshotFormat) { case ScreenshotFormat.Png: await image.SaveAsPngAsync(stream).ConfigureAwait(false); @@ -164,7 +161,7 @@ namespace osu.Game.Graphics break; default: - throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}."); + throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat}."); } } @@ -188,12 +185,12 @@ namespace osu.Game.Graphics private static readonly object filename_reservation_lock = new object(); - private (string? filename, Stream? stream) getWritableStream() + private (string? filename, Stream? stream) getWritableStream(ScreenshotFormat format) { lock (filename_reservation_lock) { - var dt = DateTime.Now; - string fileExt = screenshotFormat.ToString().ToLowerInvariant(); + DateTime dt = DateTime.Now; + string fileExt = format.ToString().ToLowerInvariant(); string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}"; if (!storage.Exists(withoutIndex)) From 69e822f3c53186c61299c6b80bcb17a834f547eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 14:30:26 +0900 Subject: [PATCH 536/539] Refactor crop logic slightly --- osu.Game/Graphics/ScreenshotManager.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index ca6a3fe84f..a085558b3a 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -89,13 +89,6 @@ namespace osu.Game.Graphics ScreenshotFormat screenshotFormat = config.Get(OsuSetting.ScreenshotFormat); bool captureMenuCursor = config.Get(OsuSetting.ScreenshotCaptureMenuCursor); - float posX = config.Get(OsuSetting.ScalingPositionX); - float posY = config.Get(OsuSetting.ScalingPositionY); - float sizeX = config.Get(OsuSetting.ScalingSizeX); - float sizeY = config.Get(OsuSetting.ScalingSizeY); - - ScalingMode scalingMode = config.Get(OsuSetting.Scaling); - try { if (!captureMenuCursor) @@ -125,17 +118,26 @@ namespace osu.Game.Graphics using (Image? image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { - if (scalingMode == ScalingMode.Everything) + if (config.Get(OsuSetting.Scaling) == ScalingMode.Everything) { + float posX = config.Get(OsuSetting.ScalingPositionX); + float posY = config.Get(OsuSetting.ScalingPositionY); + float sizeX = config.Get(OsuSetting.ScalingSizeX); + float sizeY = config.Get(OsuSetting.ScalingSizeY); + image.Mutate(m => { - Size size = m.GetCurrentSize(); - Rectangle rect = new Rectangle(Point.Empty, size); - int sx = (size.Width - (int)(size.Width * sizeX)) / 2; - int sy = (size.Height - (int)(size.Height * sizeY)) / 2; + Rectangle rect = new Rectangle(Point.Empty, m.GetCurrentSize()); + + // Reduce size by user scale settings... + int sx = (rect.Width - (int)(rect.Width * sizeX)) / 2; + int sy = (rect.Height - (int)(rect.Height * sizeY)) / 2; rect.Inflate(-sx, -sy); + + // ...then adjust the region based on their positional offset. rect.X = (int)(rect.X * posX) * 2; rect.Y = (int)(rect.Y * posY) * 2; + m.Crop(rect); }); } From f22bfa350a2b95ff3fa16a5528d7ef95c0e0d3fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 15:02:43 +0900 Subject: [PATCH 537/539] Add test coverage of hidden scores on accuracy circle --- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 7aa36429a7..41a5603060 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -46,6 +46,16 @@ namespace osu.Game.Tests.Visual.Ranking addCircleStep(createScore(1, new OsuRuleset())); } + [Test] + public void TestOsuRankHidden() + { + addCircleStep(createScore(0, new OsuRuleset(), 20, true)); + addCircleStep(createScore(0.8, new OsuRuleset(), 5, true)); + addCircleStep(createScore(0.95, new OsuRuleset(), 0, true)); + addCircleStep(createScore(0.97, new OsuRuleset(), 1, true)); + addCircleStep(createScore(1, new OsuRuleset(), 0, true)); + } + [Test] public void TestCatchRank() { @@ -66,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking addCircleStep(createScore(1, new CatchRuleset())); } - private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () => + private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy}, {score.Statistics.GetValueOrDefault(HitResult.Miss)} miss)", () => { Children = new Drawable[] { @@ -93,18 +103,22 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private ScoreInfo createScore(double accuracy, Ruleset ruleset) + private ScoreInfo createScore(double accuracy, Ruleset ruleset, int missCount = 0, bool hidden = false) { var scoreProcessor = ruleset.CreateScoreProcessor(); var statistics = new Dictionary { - { HitResult.Miss, 1 }, + { HitResult.Miss, missCount }, { HitResult.Meh, 50 }, { HitResult.Good, 100 }, { HitResult.Great, 300 }, }; + var mods = hidden + ? new[] { new OsuModHidden() } + : new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }; + return new ScoreInfo { User = new APIUser @@ -114,7 +128,7 @@ namespace osu.Game.Tests.Visual.Ranking }, BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Ruleset = ruleset.RulesetInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + Mods = mods, TotalScore = 2845370, Accuracy = accuracy, MaxCombo = 999, From 37e370e654d743ca11b78074cd9a60f66d5590c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 14:57:42 +0900 Subject: [PATCH 538/539] Fix crash at results screen when hidden is enabled and S rank becomes A due to miss Closes https://github.com/ppy/osu/issues/26692. --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index e7e54d0fae..0aff98df2b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -417,7 +417,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy accuracyCircle .FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); - badges.Single(b => b.Rank == ScoreRank.S) + badges.Single(b => b.Rank == getRank(ScoreRank.S)) .FadeOut(70, Easing.OutQuint); } } From a264ac9f381dc71d8b8d79af70c61d168a9ed91f Mon Sep 17 00:00:00 2001 From: Mike Will Date: Thu, 25 Jan 2024 05:12:54 -0500 Subject: [PATCH 539/539] Change name and description of `force` parameter in `SetArbitraryDivisor` --- osu.Game/Screens/Edit/BindableBeatDivisor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index 87cb191a82..4b0726658f 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -29,11 +29,11 @@ namespace osu.Game.Screens.Edit /// Set a divisor, updating the valid divisor range appropriately. /// /// The intended divisor. - /// Ignores the current valid divisor range when true. - public void SetArbitraryDivisor(int divisor, bool force = false) + /// Forces changing the valid divisors to a known preset. + public void SetArbitraryDivisor(int divisor, bool preferKnownPresets = false) { // If the current valid divisor range doesn't contain the proposed value, attempt to find one which does. - if (force || !ValidDivisors.Value.Presets.Contains(divisor)) + if (preferKnownPresets || !ValidDivisors.Value.Presets.Contains(divisor)) { if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor)) ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;