From d773eb2c22c804e97977298fb57bc3cd265a7530 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 5 Feb 2020 14:05:12 +0800 Subject: [PATCH 001/126] refactor rotation logic to use explicit delta value --- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e3dd2b1b4f..91e49e0264 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -98,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; + if (validAndTracking) { if (!rotationTransferred) @@ -106,13 +108,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces rotationTransferred = true; } - if (thisAngle - lastAngle > 180) + if (delta > 180) + { lastAngle += 360; - else if (lastAngle - thisAngle > 180) + delta -= 360; + } + else if (-delta > 180) + { lastAngle -= 360; + delta += 360; + } - currentRotation += thisAngle - lastAngle; - RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime); + currentRotation += delta; + RotationAbsolute += Math.Abs(delta) * Math.Sign(Clock.ElapsedFrameTime); } lastAngle = thisAngle; From 9f79713fb3a7b14e4f502d96b9b5bf9e417342cc Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 5 Feb 2020 14:23:59 +0800 Subject: [PATCH 002/126] move rotation logic to its own method --- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 91e49e0264..58132635ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -96,32 +96,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - var delta = thisAngle - lastAngle; + bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + if (validAndTracking) - { - if (!rotationTransferred) - { - currentRotation = Rotation * 2; - rotationTransferred = true; - } - - if (delta > 180) - { - lastAngle += 360; - delta -= 360; - } - else if (-delta > 180) - { - lastAngle -= 360; - delta += 360; - } - - currentRotation += delta; - RotationAbsolute += Math.Abs(delta) * Math.Sign(Clock.ElapsedFrameTime); - } + Rotate(delta); lastAngle = thisAngle; @@ -136,5 +116,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); } + + public void Rotate(float angle) + { + if (!rotationTransferred) + { + currentRotation = Rotation * 2; + rotationTransferred = true; + } + + if (angle > 180) + { + lastAngle += 360; + angle -= 360; + } + else if (-angle > 180) + { + lastAngle -= 360; + angle += 360; + } + + currentRotation += angle; + RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime); + } } } From 25a930c43877007279a2503e34b4fa7702860f21 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 08:59:35 +0800 Subject: [PATCH 003/126] Implement OsuModSpunOut --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 17 ++++++++++++++- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 21 ++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 1cdcddbd33..1ef53542a8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -2,13 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -18,5 +22,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 0.9; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var hitObject in drawables) + { + if (hitObject is DrawableSpinner spinner) + { + spinner.Disc.AutoSpin = true; + } + } + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..b5265babd9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) + if (!SpmCounter.IsPresent && (Disc.Tracking || Disc.AutoSpin)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 58132635ca..e042a3791d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -73,6 +73,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } + public bool AutoSpin { get; set; } = false; + protected override bool OnMouseMove(MouseMoveEvent e) { mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition); @@ -94,16 +96,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); - var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); + bool valid = spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - var delta = thisAngle - lastAngle; + if (valid && AutoSpin) + Rotate(6f); + else + { + var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; - if (validAndTracking) - Rotate(delta); + if (valid && tracking) + Rotate(delta); - lastAngle = thisAngle; + lastAngle = thisAngle; + } if (Complete && updateCompleteTick()) { @@ -114,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); + this.RotateTo(currentRotation / 2, 500, Easing.OutExpo); } public void Rotate(float angle) From 0dee6ceab74a3826c2705d55e50921f58d38dc52 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:06:29 +0800 Subject: [PATCH 004/126] Remove unnecessary using --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 1ef53542a8..f1a1e47118 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; From 4d9232a895ad4dd12e43cc0c9fc0b519a62091a2 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:51:32 +0800 Subject: [PATCH 005/126] Move autospin logic to mods --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 24 +++++++++++++++++-- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 21 ++++++---------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index f1a1e47118..07c10966d3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,15 +3,18 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -22,15 +25,32 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; + private double lastFrameTime; + private double frameDelay; + public void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var hitObject in drawables) { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.AutoSpin = true; + spinner.Disc.Trackable = false; + spinner.Disc.OnUpdate += d => + { + if (d is SpinnerDisc s) + { + if (s.Valid) + s.Rotate((float)frameDelay); + } + }; } } } + + public void Update(Playfield playfield) + { + frameDelay = playfield.Time.Current - lastFrameTime; + lastFrameTime = playfield.Time.Current; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b5265babd9..2930134d4f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || Disc.AutoSpin)) + if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Trackable)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e042a3791d..9a9d915cfe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public bool AutoSpin { get; set; } = false; + public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + public bool Trackable { get; set; } protected override bool OnMouseMove(MouseMoveEvent e) { @@ -95,22 +96,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void Update() { base.Update(); + var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool valid = spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; - if (valid && AutoSpin) - Rotate(6f); - else - { - var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); + if (Valid && tracking && Trackable) + Rotate(delta); - var delta = thisAngle - lastAngle; - - if (valid && tracking) - Rotate(delta); - - lastAngle = thisAngle; - } + lastAngle = thisAngle; if (Complete && updateCompleteTick()) { From ca09ae6849b94cdc2ef15c4b770212d1f8a92138 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:53:20 +0800 Subject: [PATCH 006/126] fix formatting --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 07c10966d3..c74e4e3e70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate((float)frameDelay); + s.Rotate((float)frameDelay); } }; } From 204c2f0bde7cbd36563842d0f4831a65d2308fcc Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:16:04 +0800 Subject: [PATCH 007/126] add tests --- osu.Game.Rulesets.Osu.Tests/Class1.cs | 23 +++++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Class1.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Class1.cs b/osu.Game.Rulesets.Osu.Tests/Class1.cs new file mode 100644 index 0000000000..402c14fa64 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Class1.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 System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneSpinnerSpunOut : TestSceneSpinner + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModSpunOut) }).ToList(); + + [SetUp] + public void SetUp() => Schedule(() => + { + SelectedMods.Value = new[] { new OsuModHidden() }; + }); + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index c74e4e3e70..eb49742db6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; From 715608c7985c2366905751be856b3984b6cf94cd Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:49:49 +0800 Subject: [PATCH 008/126] Fix test applying incorrect mod --- .../{Class1.cs => TestSceneSpinnerSpunOut.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{Class1.cs => TestSceneSpinnerSpunOut.cs} (90%) diff --git a/osu.Game.Rulesets.Osu.Tests/Class1.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs similarity index 90% rename from osu.Game.Rulesets.Osu.Tests/Class1.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs index 402c14fa64..a6c09691c7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Class1.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests [SetUp] public void SetUp() => Schedule(() => { - SelectedMods.Value = new[] { new OsuModHidden() }; + SelectedMods.Value = new[] { new OsuModSpunOut() }; }); } } From efa95ecebb66fe587c6c2e9862c384586b29dbf9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:52:59 +0800 Subject: [PATCH 009/126] fix spinner unspinnable --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 9a9d915cfe..4e2758b3d5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - public bool Trackable { get; set; } + public bool Trackable { get; set; } = true; protected override bool OnMouseMove(MouseMoveEvent e) { From fbdf07dc201c87140c00be1121d227d003c3dd1e Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 11:06:37 +0800 Subject: [PATCH 010/126] Correct speed of spun out --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index eb49742db6..16fc7646c2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate((float)frameDelay); + s.Rotate(180 / MathF.PI * ((float)frameDelay) / 40); } }; } From d821b6a15aee2f2d0d7559828a6a04752546ddba Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 11:16:48 +0800 Subject: [PATCH 011/126] make frameDelay a float --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 16fc7646c2..1832910e71 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; private double lastFrameTime; - private double frameDelay; + private float frameDelay; public void ApplyToDrawableHitObjects(IEnumerable drawables) { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate(180 / MathF.PI * ((float)frameDelay) / 40); + s.Rotate(180 / MathF.PI * frameDelay / 40); } }; } @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { - frameDelay = playfield.Time.Current - lastFrameTime; + frameDelay = (float)(playfield.Time.Current - lastFrameTime); lastFrameTime = playfield.Time.Current; } } From 2d672159317783629f3e5334621fc7341942531e Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 12:07:21 +0800 Subject: [PATCH 012/126] make target practice subject of unimplemented mod test --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 12ee4ceb2e..1e18e18631 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); - var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut); + var targetMod = easierMods.FirstOrDefault(m => m is OsuModTarget); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.UserInterface testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour); testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour); - testUnimplementedMod(spunOutMod); + testUnimplementedMod(targetMod); } [Test] From a4637a24a6c6d57a23f18db87ac954577ff9e1c3 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 12:08:44 +0800 Subject: [PATCH 013/126] fix test scene crash --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 1e18e18631..034324aadd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -91,13 +91,14 @@ namespace osu.Game.Tests.Visual.UserInterface var easierMods = osu.GetModsFor(ModType.DifficultyReduction); var harderMods = osu.GetModsFor(ModType.DifficultyIncrease); + var conversionMods = osu.GetModsFor(ModType.Conversion); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); - var targetMod = easierMods.FirstOrDefault(m => m is OsuModTarget); + var targetMod = conversionMods.FirstOrDefault(m => m is OsuModTarget); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); From 8e20e641f440d4a2dff2c49b60816b0021a1cd55 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:33:43 +0800 Subject: [PATCH 014/126] move spun out to automation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b794f5e22e..c4890afe36 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Spun Out"; public override string Acronym => "SO"; public override IconUsage? Icon => OsuIcon.ModSpunout; - public override ModType Type => ModType.DifficultyReduction; + public override ModType Type => ModType.Automation; public override string Description => @"Spinners will be automatically completed."; public override double ScoreMultiplier => 0.9; public override bool Ranked => true; From 83c67dc155518d1a9b2432432e0a4f250c370544 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:34:35 +0800 Subject: [PATCH 015/126] move spun out to automation --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 148869f5e8..ed73a54815 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -113,7 +113,6 @@ namespace osu.Game.Rulesets.Osu new OsuModEasy(), new OsuModNoFail(), new MultiMod(new OsuModHalfTime(), new OsuModDaycore()), - new OsuModSpunOut(), }; case ModType.DifficultyIncrease: @@ -139,6 +138,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModAutoplay(), new OsuModCinema()), new OsuModRelax(), new OsuModAutopilot(), + new OsuModSpunOut(), }; case ModType.Fun: From c9520b299a40cd23d228dd62b38c87a1ee3b9632 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:35:36 +0800 Subject: [PATCH 016/126] replace if check with variable decl --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index c4890afe36..6a2610ae05 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -36,11 +36,10 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Disc.Trackable = false; spinner.Disc.OnUpdate += d => { - if (d is SpinnerDisc s) - { - if (s.Valid) - s.Rotate(180 / MathF.PI * frameDelay / 40); - } + var s = d as SpinnerDisc; + + if (s.Valid) + s.Rotate(180 / MathF.PI * frameDelay / 40); }; } } From d314b38699d1211d034cc896a478b809d8aa15e5 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:46:06 +0800 Subject: [PATCH 017/126] rename trackable to enabled and cleanup code --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 6a2610ae05..e02ded979f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.Trackable = false; + spinner.Disc.Enabled = false; spinner.Disc.OnUpdate += d => { var s = d as SpinnerDisc; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2930134d4f..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Trackable)) + if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 4e2758b3d5..b062fc5afa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces get => tracking; set { - if (value == tracking) return; + if ((Enabled && value) == tracking) return; - tracking = value; + tracking = Enabled && value; background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } @@ -74,7 +74,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - public bool Trackable { get; set; } = true; + + public bool Enabled { get; set; } = true; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var delta = thisAngle - lastAngle; - if (Valid && tracking && Trackable) + if (Valid && tracking) Rotate(delta); lastAngle = thisAngle; From 68873830aadfde6495812766820c3b9b78dc94d3 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:49:08 +0800 Subject: [PATCH 018/126] make spm counter show up automatically with spun out --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..752bd7be85 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) + if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Enabled)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); From 596f4f7d2e6c62c7a4c3fa43ce161c5b71c388d8 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 14:54:46 +0800 Subject: [PATCH 019/126] use spinner's clock to drive spinner --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index e02ded979f..084be40672 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; - private double lastFrameTime; - private float frameDelay; - public void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var hitObject in drawables) @@ -39,16 +36,10 @@ namespace osu.Game.Rulesets.Osu.Mods var s = d as SpinnerDisc; if (s.Valid) - s.Rotate(180 / MathF.PI * frameDelay / 40); + s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); }; } } } - - public void Update(Playfield playfield) - { - frameDelay = (float)(playfield.Time.Current - lastFrameTime); - lastFrameTime = playfield.Time.Current; - } } } From e78d94d4692d02ed5843e5f23882c660e23099d1 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 14:56:47 +0800 Subject: [PATCH 020/126] return to use if for casting --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 084be40672..2c1d1362a6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -33,9 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Disc.Enabled = false; spinner.Disc.OnUpdate += d => { - var s = d as SpinnerDisc; - - if (s.Valid) + if (d is SpinnerDisc s && s.Valid) s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); }; } From 10a1948720b15e8de61149ff7abfcad89d5eca8d Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 16:19:21 +0800 Subject: [PATCH 021/126] remove using directive --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 2c1d1362a6..d56e39b588 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { From 9aa5db88d4baed09dcda72005845cf2dad27e9f0 Mon Sep 17 00:00:00 2001 From: mcendu Date: Mon, 10 Feb 2020 14:14:04 +0800 Subject: [PATCH 022/126] move auto fade in to mod --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 13 +++++++++---- .../Objects/Drawables/DrawableSpinner.cs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index d56e39b588..670f4a2cd8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Mods { @@ -30,10 +30,15 @@ namespace osu.Game.Rulesets.Osu.Mods if (hitObject is DrawableSpinner spinner) { spinner.Disc.Enabled = false; - spinner.Disc.OnUpdate += d => + spinner.OnUpdate += d => { - if (d is SpinnerDisc s && s.Valid) - s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); + if (d is DrawableSpinner s) + { + if (s.Disc.Valid) + s.Disc.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); + if (!s.SpmCounter.IsPresent) + s.SpmCounter.FadeIn(s.HitObject.TimeFadeIn); + } }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 752bd7be85..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Enabled)) + if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); From 403c03841d1d3497dfda7ffc3af8f2fb72d2daba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2020 21:39:45 +0100 Subject: [PATCH 023/126] Decouple test scene & add assertions --- .../TestSceneSpinnerSpunOut.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs index a6c09691c7..e406f9ddff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs @@ -5,19 +5,66 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSpinnerSpunOut : TestSceneSpinner + public class TestSceneSpinnerSpunOut : OsuTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModSpunOut) }).ToList(); + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SpinnerDisc), + typeof(DrawableSpinner), + typeof(DrawableOsuHitObject), + typeof(OsuModSpunOut) + }; [SetUp] public void SetUp() => Schedule(() => { SelectedMods.Value = new[] { new OsuModSpunOut() }; }); + + [Test] + public void TestSpunOut() + { + DrawableSpinner spinner = null; + + AddStep("create spinner", () => spinner = createSpinner()); + + AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd); + + AddAssert("spinner is completed", () => spinner.Progress >= 1); + } + + private DrawableSpinner createSpinner() + { + var spinner = new Spinner + { + StartTime = Time.Current + 500, + EndTime = Time.Current + 2500 + }; + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + var drawableSpinner = new DrawableSpinner(spinner) + { + Anchor = Anchor.Centre + }; + + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); + + Add(drawableSpinner); + return drawableSpinner; + } } } From 686040d8ad7926ad515fcb1bfcf586ca8b1d5d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2020 21:42:34 +0100 Subject: [PATCH 024/126] Extract auto-spin logic to method --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 670f4a2cd8..37ef001223 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -30,18 +30,20 @@ namespace osu.Game.Rulesets.Osu.Mods if (hitObject is DrawableSpinner spinner) { spinner.Disc.Enabled = false; - spinner.OnUpdate += d => - { - if (d is DrawableSpinner s) - { - if (s.Disc.Valid) - s.Disc.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); - if (!s.SpmCounter.IsPresent) - s.SpmCounter.FadeIn(s.HitObject.TimeFadeIn); - } - }; + spinner.OnUpdate += autoSpin; } } } + + private void autoSpin(Drawable drawable) + { + if (drawable is DrawableSpinner spinner) + { + if (spinner.Disc.Valid) + spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime / 40); + if (!spinner.SpmCounter.IsPresent) + spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); + } + } } } From b6378c7ae22ef88365a604a34289f574d6decae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9CNate?= Date: Sun, 16 Feb 2020 17:50:52 +0800 Subject: [PATCH 025/126] fix speed * Original /40 is due to documentation error. Co-Authored-By: clayton --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 37ef001223..30b9c84538 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (drawable is DrawableSpinner spinner) { if (spinner.Disc.Valid) - spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime / 40); + spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime * 0.03f); if (!spinner.SpmCounter.IsPresent) spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); } From e2ea92e21f7d3a6fad4459a8cfdaadbbcfead410 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 13:51:12 +0800 Subject: [PATCH 026/126] Use framework method to convert rad to deg --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 30b9c84538..49c4e7fa45 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (drawable is DrawableSpinner spinner) { if (spinner.Disc.Valid) - spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime * 0.03f); + spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); if (!spinner.SpmCounter.IsPresent) spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); } From e59d7fee26728e6c281d03bf67ee004c127973de Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 Mar 2020 23:42:14 -0400 Subject: [PATCH 027/126] fix comment grammar --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 2083671072..c3d1e4c857 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods /// /// Transfer a setting from to a configuration bindable. - /// Only performs the transfer if the user it not currently overriding.. + /// Only performs the transfer if the user is not currently overriding. /// protected void TransferSetting(BindableNumber bindable, T beatmapDefault) where T : struct, IComparable, IConvertible, IEquatable From 18bf7c913b1b73f444f09ca16d2f43334d696404 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 Mar 2020 23:43:26 -0400 Subject: [PATCH 028/126] show mod settings in ModIcon tooltip --- .../Mods/CatchModDifficultyAdjust.cs | 5 +++++ osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 5 +++++ osu.Game/Rulesets/Mods/Mod.cs | 11 +++++++++++ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 +++ osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 ++ osu.Game/Rulesets/Mods/ModEasy.cs | 2 ++ osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 ++ osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 ++ osu.Game/Rulesets/UI/ModIcon.cs | 2 +- 9 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index e2465d727e..8ea39c8676 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 75de6896a3..c3e1321dac 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 46c0c1da07..b70ddc6d46 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,6 +5,7 @@ using System; using Newtonsoft.Json; using osu.Framework.Graphics.Sprites; using osu.Game.IO.Serialization; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { @@ -42,6 +43,16 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual string Description => string.Empty; + /// + /// The tooltip to display for this mod when used in a . + /// + /// + /// Differs from , as the value of attributes (AR, CS, etc) changeable via the mod + /// are displayed in the tooltip. + /// + [JsonIgnore] + public virtual string IconTooltip => Name; + /// /// The score multiplier of this mod. /// diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index c3d1e4c857..4072e6a6af 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + + $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 152657da33..4f7d82418d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index b56be95dfe..2ec4e9610b 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; + public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 203b88951c..14133bddcd 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 9e63142b42..9cb97dfc35 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } + public override string IconTooltip => $"{Name} ({InitialRate.Value}x to {FinalRate.Value}x)"; + private double finalRateTime; private double beginRampTime; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 3edab0745d..3cd1b0820d 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI type = mod.Type; - TooltipText = mod.Name; + TooltipText = mod.IconTooltip; Size = new Vector2(size); From 7a0a633ef9320fbd2c519f7630999306fcce6ce5 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 Mar 2020 00:06:55 -0400 Subject: [PATCH 029/126] don't use ToString, proper indent level --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 8 ++++---- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 8ea39c8676..661c59332f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index c3e1321dac..477028dbbe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; protected override void TransferSettings(BeatmapDifficulty difficulty) { From 5a6d8f1932715d9fc7479d8cb5614e0b2efe4025 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 12:47:17 -0400 Subject: [PATCH 030/126] use SettingSource to define IconTooltip format --- .../Mods/CatchModDifficultyAdjust.cs | 9 ++--- .../Mods/OsuModDifficultyAdjust.cs | 9 ++--- .../Configuration/SettingSourceAttribute.cs | 11 +++++- osu.Game/Rulesets/Mods/Mod.cs | 34 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 7 ++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 4 +-- osu.Game/Rulesets/Mods/ModEasy.cs | 4 +-- osu.Game/Rulesets/Mods/ModHalfTime.cs | 4 +-- 8 files changed, 52 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 661c59332f..e4298dc008 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,11 +30,6 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; - protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 477028dbbe..91707ea328 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,11 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; - protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fe487cb1d0..1a79dc7335 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -30,17 +30,26 @@ namespace osu.Game.Configuration public int? OrderPosition { get; } + public string TooltipText { get; } + public SettingSourceAttribute(string label, string description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } - public SettingSourceAttribute(string label, string description, int orderPosition) + public SettingSourceAttribute(string label, string description, string tooltipText, int orderPosition) : this(label, description) { OrderPosition = orderPosition; + TooltipText = tooltipText; } + + public SettingSourceAttribute(string label, string description, string tooltipText) : this(label, description) + { + TooltipText = tooltipText; + } + } public static class SettingSourceExtensions diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b70ddc6d46..860e768350 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,8 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -51,7 +55,35 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip => Name; + public virtual string IconTooltip + { + get + { + List attributes = new List(); + foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) + { + // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided + string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; + object bindableObj = property.GetValue(this); + if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableInt.Value)); + continue; + } + if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableFloat.Value)); + continue; + } + if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableDouble.Value)); + continue; + } + } + return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; + } + } /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4072e6a6af..8188e36b64 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods protected const int LAST_SETTING_ORDER = 2; - [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] + [SettingSource("HP Drain", "Override a beatmap's set HP.", "HP {0}", FIRST_SETTING_ORDER)] public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] + [SettingSource("Accuracy", "Override a beatmap's set OD.", "OD {0}", LAST_SETTING_ORDER)] public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, @@ -52,9 +52,6 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + - $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; - private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 4f7d82418d..fe027b9da0 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); - [SettingSource("Speed increase", "The actual increase to apply")] + [SettingSource("Speed increase", "The actual increase to apply", "{0}x")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 1.01, @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; - - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 2ec4e9610b..c92c7297c3 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -21,15 +21,13 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - [SettingSource("Extra Lives", "Number of extra lives")] + [SettingSource("Extra Lives", "Number of extra lives", "{0} lives")] public Bindable Retries { get; } = new BindableInt(2) { MinValue = 0, MaxValue = 10 }; - public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; - private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 14133bddcd..7c1f4b8e12 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); - [SettingSource("Speed decrease", "The actual decrease to apply")] + [SettingSource("Speed decrease", "The actual decrease to apply", "{0}x")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 0.5, @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; - - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } From 9dc814681195f864192436ba0a9f8197a726108b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 13:21:44 -0400 Subject: [PATCH 031/126] fix style issues --- osu.Game/Configuration/SettingSourceAttribute.cs | 4 ++-- osu.Game/Rulesets/Mods/Mod.cs | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1a79dc7335..fb0daf9217 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -45,11 +45,11 @@ namespace osu.Game.Configuration TooltipText = tooltipText; } - public SettingSourceAttribute(string label, string description, string tooltipText) : this(label, description) + public SettingSourceAttribute(string label, string description, string tooltipText) + : this(label, description) { TooltipText = tooltipText; } - } public static class SettingSourceExtensions diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 860e768350..69fd45767b 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -60,25 +60,28 @@ namespace osu.Game.Rulesets.Mods get { List attributes = new List(); + foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; object bindableObj = property.GetValue(this); + if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) { attributes.Add(string.Format(tooltipText, bindableInt.Value)); continue; } + if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) { attributes.Add(string.Format(tooltipText, bindableFloat.Value)); continue; } + if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) { attributes.Add(string.Format(tooltipText, bindableDouble.Value)); - continue; } } return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; From 9e3bff3b97503c6e4f2a89fa5ea1c8a01f3767bf Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 13:36:16 -0400 Subject: [PATCH 032/126] oops, missed a newline --- osu.Game/Rulesets/Mods/Mod.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 69fd45767b..f3b7fed96a 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -84,6 +84,7 @@ namespace osu.Game.Rulesets.Mods attributes.Add(string.Format(tooltipText, bindableDouble.Value)); } } + return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; } } From 3d955921302cbe8e449640bdb0488b528654fd7c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 14:37:31 -0400 Subject: [PATCH 033/126] use var for list declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- 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 f3b7fed96a..b4faf55734 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods { get { - List attributes = new List(); + var attributes = new List(); foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { From 7bdbdd25f8b3955b9c0a8f2dcb897a722073f958 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:05:12 -0400 Subject: [PATCH 034/126] Revert "use SettingSource to define IconTooltip format" This reverts commit 5a6d8f1932715d9fc7479d8cb5614e0b2efe4025. --- .../Mods/CatchModDifficultyAdjust.cs | 9 ++++- .../Mods/OsuModDifficultyAdjust.cs | 9 ++++- .../Configuration/SettingSourceAttribute.cs | 11 +----- osu.Game/Rulesets/Mods/Mod.cs | 38 +------------------ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 7 +++- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 4 +- osu.Game/Rulesets/Mods/ModEasy.cs | 4 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 4 +- 8 files changed, 30 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index e4298dc008..661c59332f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 91707ea328..477028dbbe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fb0daf9217..fe487cb1d0 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -30,25 +30,16 @@ namespace osu.Game.Configuration public int? OrderPosition { get; } - public string TooltipText { get; } - public SettingSourceAttribute(string label, string description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } - public SettingSourceAttribute(string label, string description, string tooltipText, int orderPosition) + public SettingSourceAttribute(string label, string description, int orderPosition) : this(label, description) { OrderPosition = orderPosition; - TooltipText = tooltipText; - } - - public SettingSourceAttribute(string label, string description, string tooltipText) - : this(label, description) - { - TooltipText = tooltipText; } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b4faf55734..b70ddc6d46 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,12 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -55,39 +51,7 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip - { - get - { - var attributes = new List(); - - foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) - { - // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided - string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; - object bindableObj = property.GetValue(this); - - if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableInt.Value)); - continue; - } - - if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableFloat.Value)); - continue; - } - - if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableDouble.Value)); - } - } - - return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; - } - } + public virtual string IconTooltip => Name; /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 8188e36b64..4072e6a6af 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods protected const int LAST_SETTING_ORDER = 2; - [SettingSource("HP Drain", "Override a beatmap's set HP.", "HP {0}", FIRST_SETTING_ORDER)] + [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - [SettingSource("Accuracy", "Override a beatmap's set OD.", "OD {0}", LAST_SETTING_ORDER)] + [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + + $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index fe027b9da0..4f7d82418d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); - [SettingSource("Speed increase", "The actual increase to apply", "{0}x")] + [SettingSource("Speed increase", "The actual increase to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 1.01, @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index c92c7297c3..2ec4e9610b 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -21,13 +21,15 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - [SettingSource("Extra Lives", "Number of extra lives", "{0} lives")] + [SettingSource("Extra Lives", "Number of extra lives")] public Bindable Retries { get; } = new BindableInt(2) { MinValue = 0, MaxValue = 10 }; + public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 7c1f4b8e12..14133bddcd 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); - [SettingSource("Speed decrease", "The actual decrease to apply", "{0}x")] + [SettingSource("Speed decrease", "The actual decrease to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 0.5, @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } From cda1efef0bde4d9ee692cb8d7747aea63b502e58 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:34:36 -0400 Subject: [PATCH 035/126] move overridability to SettingDescription method --- .../Mods/CatchModDifficultyAdjust.cs | 20 +++++++++++++++---- .../Mods/OsuModDifficultyAdjust.cs | 20 +++++++++++++++---- osu.Game/Rulesets/Mods/Mod.cs | 18 ++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 16 +++++++++++++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 8 files changed, 67 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 661c59332f..ee05dd1560 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.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.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +31,21 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string SettingDescription + { + get + { + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + + string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 477028dbbe..520e5a6726 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.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.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +31,21 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string SettingDescription + { + get + { + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + + string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b70ddc6d46..231b95f974 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -51,7 +51,23 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip => Name; + public string IconTooltip + { + get + { + string settingDescription = string.IsNullOrEmpty(SettingDescription) ? "" : $" ({SettingDescription})"; + return $"{Name}{settingDescription}"; + } + } + + /// + /// The description of editable settings of a mod to use in the . + /// + /// + /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, + /// the tooltip will not have parentheses. + /// + public virtual string SettingDescription => string.Empty; /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4072e6a6af..a5024d6988 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; +using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -52,8 +53,19 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + - $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + public override string SettingDescription + { + get + { + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + + string[] settings = new string[] { drainRate, overallDifficulty }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } private BeatmapDifficulty difficulty; diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 4f7d82418d..1730c98c7f 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 2ec4e9610b..4433a28a95 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + public override string SettingDescription => Retries.IsDefault ? "" : $" ({Retries.Value} lives)"; private int retries; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 14133bddcd..5f99748a87 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 9cb97dfc35..7b4c1370ac 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string IconTooltip => $"{Name} ({InitialRate.Value}x to {FinalRate.Value}x)"; + public override string SettingDescription => $"{InitialRate.Value} to {FinalRate.Value}"; private double finalRateTime; private double beginRampTime; From 6a63ba1bb826235cdca903ccdc855188d4e3d584 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:42:35 -0400 Subject: [PATCH 036/126] use humanizer for ModEasy lives setting --- osu.Game/Rulesets/Mods/ModEasy.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 4433a28a95..a1dd6c088d 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Humanizer; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? "" : $" ({Retries.Value} lives)"; + public override string SettingDescription => Retries.IsDefault ? "" : $"{"lives".ToQuantity(Retries.Value)}"; private int retries; From 55568ee6a5cae8ce8494eae518f8fc14562f64bf Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:44:38 -0400 Subject: [PATCH 037/126] remove extra parentheses --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 1730c98c7f..05a8dbfa56 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 5f99748a87..5252ce8d89 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; } } From e84b40f8ed86507a16a162d5b1ce6ee1e66821ff Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:53:40 -0400 Subject: [PATCH 038/126] remove unnecessary ToString calls --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 8 ++++---- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index ee05dd1560..3ebe8c6f6f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -35,10 +35,10 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 520e5a6726..d63239755b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -35,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas From ac202ba7ea963d43b2e5a7054dbfd285edf2a48b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:57:37 -0400 Subject: [PATCH 039/126] remove unused using directive --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index a5024d6988..f50c2cf001 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; -using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -60,7 +59,7 @@ namespace osu.Game.Rulesets.Mods string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string[] settings = new string[] { drainRate, overallDifficulty }; + string[] settings = { drainRate, overallDifficulty }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); From a440d156205ea6503cfed552bb782dad9e6cba41 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:58:02 -0400 Subject: [PATCH 040/126] simplify array initializationstatement --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 3ebe8c6f6f..6d0025b236 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Mods string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; - string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index d63239755b..307fe3da4a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; - string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); From eab705a9b690e9bdac340edae616895aef3465f1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 17:00:36 -0400 Subject: [PATCH 041/126] remove another ToString statement --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f50c2cf001..4a8313b1f1 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods get { string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string[] settings = { drainRate, overallDifficulty }; // filter out empty strings so we don't have orphaned commas From 4907fb8fd1e966c1940936b74712c9bf672b3184 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 17:04:22 -0400 Subject: [PATCH 042/126] remove another ToString statement --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4a8313b1f1..f8341e6cdb 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string[] settings = { drainRate, overallDifficulty }; From 63e9b2a299cf6496ec535315a4787ad9327cb044 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 18:49:45 -0400 Subject: [PATCH 043/126] use string.Empty, use base SettingDescription for [Osu/Catch]ModDifficultyAdjust --- .../Mods/CatchModDifficultyAdjust.cs | 17 ++++++------ .../Mods/OsuModDifficultyAdjust.cs | 17 ++++++------ osu.Game/Rulesets/Mods/Mod.cs | 26 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 14 +++++----- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- 7 files changed, 54 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 6d0025b236..a60b35739e 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.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.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -35,15 +36,15 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; - string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + circleSize, + base.SettingDescription, + approachRate + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 307fe3da4a..18492828f0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.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.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -35,15 +36,15 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; - string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + circleSize, + base.SettingDescription, + approachRate + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 231b95f974..95e8ff86eb 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,8 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -67,7 +72,26 @@ namespace osu.Game.Rulesets.Mods /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, /// the tooltip will not have parentheses. /// - public virtual string SettingDescription => string.Empty; + public virtual string SettingDescription + { + get + { + var tooltipTexts = new List(); + + foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) + { + object bindableObj = property.GetValue(this); + bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; + string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj.ToString(); + tooltipTexts.Add(tooltipText); + } + + // filter out empty strings so we don't have orphaned commas + //tooltipTexts = tooltipTexts.Where(s => !string.IsNullOrEmpty(s)).ToList(); + string joinedTooltipText = string.Join(", ", tooltipTexts); + return $"{Name}{joinedTooltipText}"; + } + } /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f8341e6cdb..1baf9f7057 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; +using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -56,13 +57,14 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value}"; - string[] settings = { drainRate, overallDifficulty }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + drainRate, + overallDifficulty + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 05a8dbfa56..7d86190134 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index a1dd6c088d..c1c4124b98 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? "" : $"{"lives".ToQuantity(Retries.Value)}"; + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; private int retries; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 5252ce8d89..ec215369a3 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; } } From 67667b3d22c0f0e389cd0717c1ed717704dfb11c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 21:22:46 -0400 Subject: [PATCH 044/126] enforce precision for ModDifficultyAdjust and derived classes --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index a60b35739e..6288d498bd 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; return string.Join(", ", new[] { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 18492828f0..4830b29c4e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 1baf9f7057..06616c7b24 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:0.#}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:0.#}"; return string.Join(", ", new[] { From cb6e6025567179280b52d2e58aa099af6b2ef5d9 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:06:35 -0400 Subject: [PATCH 045/126] enforce single signficiant digit precision for other mods --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 7d86190134..105c19dec7 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index ec215369a3..32e16e0914 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7b4c1370ac..01b49faa75 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value} to {FinalRate.Value}"; + public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:0.#}"; private double finalRateTime; private double beginRampTime; From ea87afd5775c8f4220d642d093f0fe664b1a0d3e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:06:54 -0400 Subject: [PATCH 046/126] use string.Empty in IconTooltip --- osu.Game/Rulesets/Mods/Mod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 95e8ff86eb..23ad48ac5a 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.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; @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mods { get { - string settingDescription = string.IsNullOrEmpty(SettingDescription) ? "" : $" ({SettingDescription})"; + string settingDescription = string.IsNullOrEmpty(SettingDescription) ? string.Empty : $" ({SettingDescription})"; return $"{Name}{settingDescription}"; } } From 98b8f828103c037ae54ecbeb07c9a398289ae335 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:07:09 -0400 Subject: [PATCH 047/126] simplify SettingDescription default definition --- osu.Game/Rulesets/Mods/Mod.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 23ad48ac5a..5944717c13 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.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; @@ -86,10 +86,8 @@ namespace osu.Game.Rulesets.Mods tooltipTexts.Add(tooltipText); } - // filter out empty strings so we don't have orphaned commas - //tooltipTexts = tooltipTexts.Where(s => !string.IsNullOrEmpty(s)).ToList(); - string joinedTooltipText = string.Join(", ", tooltipTexts); - return $"{Name}{joinedTooltipText}"; + string joinedTooltipText = string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); + return $"{joinedTooltipText}"; } } From afe7397d891237271902caa4b3f31f8b189b770b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:50:52 -0400 Subject: [PATCH 048/126] remove unnecessary using statements --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 1 - osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 6288d498bd..c465048da3 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.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.Bindables; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 4830b29c4e..fab4638fb7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.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.Bindables; using osu.Game.Beatmaps; From 889608a408e854e30ffd2177aa9d3f079cc71717 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:51:29 -0400 Subject: [PATCH 049/126] remove redundant ToString call --- 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 5944717c13..f712fdc3be 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mods { object bindableObj = property.GetValue(this); bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; - string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj.ToString(); + string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj; tooltipTexts.Add(tooltipText); } From 1da590c63f6b77b68471e564e1210b2109f304aa Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:54:21 -0400 Subject: [PATCH 050/126] use N1 format instead of 0.# --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index c465048da3..acdd0a420c 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index fab4638fb7..8228161008 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 06616c7b24..c3a8efdd66 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:0.#}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:0.#}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 105c19dec7..3f01bfb11e 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 32e16e0914..c555692ed9 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 01b49faa75..f21ba684b4 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:0.#}"; + public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:N1}"; private double finalRateTime; private double beginRampTime; From 5cc626d37b746c69d5979e9c465933c8fe6c1b8a Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:57:46 -0400 Subject: [PATCH 051/126] move SettingDescription override to ModRateAdjust --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 -- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 -- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 3f01bfb11e..152657da33 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; - - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index c555692ed9..203b88951c 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; - - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 1739524bcd..9059d54035 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Mods { track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } + + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } From 64fc116d673500e6b4e55639e7e7daf7d7fad640 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 23:08:00 -0400 Subject: [PATCH 052/126] use two decimal points for ModRateAdjust format --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 9059d54035..cb2ff149f1 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } } From 997ce397efa6c64e6218fdd342cc0ea738a47b40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 12:48:05 +0900 Subject: [PATCH 053/126] Disable raw input toggle on all but windows --- .../Settings/Sections/Input/MouseSettings.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 59d39a1c3c..e7f2f21465 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; @@ -56,24 +57,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, }; - rawInputToggle.ValueChanged += enabled => + if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { - // this is temporary until we support per-handler settings. - const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string standard_mouse_handler = @"OsuTKMouseHandler"; - - ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; - }; - - ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); - ignoredInputHandler.ValueChanged += handler => + rawInputToggle.Disabled = true; + sensitivity.Bindable.Disabled = true; + } + else { - bool raw = !handler.NewValue.Contains("Raw"); - rawInputToggle.Value = raw; - sensitivity.Bindable.Disabled = !raw; - }; + rawInputToggle.ValueChanged += enabled => + { + // this is temporary until we support per-handler settings. + const string raw_mouse_handler = @"OsuTKRawMouseHandler"; + const string standard_mouse_handler = @"OsuTKMouseHandler"; - ignoredInputHandler.TriggerChange(); + ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; + }; + + ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); + ignoredInputHandler.ValueChanged += handler => + { + bool raw = !handler.NewValue.Contains("Raw"); + rawInputToggle.Value = raw; + sensitivity.Bindable.Disabled = !raw; + }; + + ignoredInputHandler.TriggerChange(); + } } private class SensitivitySetting : SettingsSlider From a6b153673e2fa61dc5c7c674a5fb6b964008063d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 14:58:02 +0900 Subject: [PATCH 054/126] Fix icons not updating tooltip text correctly --- osu.Game/Rulesets/UI/ModIcon.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 3cd1b0820d..8ea6c74349 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI private readonly ModType type; - public virtual string TooltipText { get; } + public virtual string TooltipText => mod.IconTooltip; private Mod mod; @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.UI type = mod.Type; - TooltipText = mod.IconTooltip; - Size = new Vector2(size); Children = new Drawable[] From 205f4dcb54f854fc36fcf8e322e20329e4e3144f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 15:20:56 +0900 Subject: [PATCH 055/126] Simplify string construction logic --- osu.Game/Rulesets/Mods/Mod.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f712fdc3be..0e5fe3fc9c 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mods { get { - string settingDescription = string.IsNullOrEmpty(SettingDescription) ? string.Empty : $" ({SettingDescription})"; - return $"{Name}{settingDescription}"; + string description = SettingDescription; + + return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})"; } } @@ -81,13 +82,14 @@ namespace osu.Game.Rulesets.Mods foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { object bindableObj = property.GetValue(this); - bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; - string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj; - tooltipTexts.Add(tooltipText); + + if ((bindableObj as IHasDefaultValue)?.IsDefault == true) + continue; + + tooltipTexts.Add($"{attr.Label} {bindableObj}"); } - string joinedTooltipText = string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); - return $"{joinedTooltipText}"; + return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); } } From 2fa42ed644d4dede960ec3e1bb4c424551b22ec6 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 23 Mar 2020 12:54:08 -0400 Subject: [PATCH 056/126] use N2 for ModTimeRamp, add x text --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index f21ba684b4..c1f3e357a1 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:N1}"; + public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; private double finalRateTime; private double beginRampTime; From 2b1245f63a279e2eee4521f689ed692ed839d376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 12:50:00 +0900 Subject: [PATCH 057/126] Improve xmldoc in a couple of places --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 27993ff173..ff6ed5bf17 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -406,7 +406,7 @@ namespace osu.Game.Rulesets.UI public abstract Playfield Playfield { get; } /// - /// Place to put drawables above hit objects but below UI. + /// Content to be placed above hitobjects. Will be affected by frame stability. /// public abstract Container Overlays { get; } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e569bb8459..3ba28aad45 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.UI { /// /// A container which consumes a parent gameplay clock and standardises frame counts for children. - /// Will ensure a minimum of 40 frames per clock second is maintained, regardless of any system lag or seeks. + /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// public class FrameStabilityContainer : Container, IHasReplayHandler { From d372ddaadd65696972f778ddfd2cb3c3df750ac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 12:50:18 +0900 Subject: [PATCH 058/126] Move break overlay to a location it is not affected by gameplay scale --- osu.Game/Screens/Play/Player.cs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc5bac9fd1..3ff47b868c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -293,19 +293,26 @@ namespace osu.Game.Screens.Play performImmediateExit(); }, }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + new Container + { + Name = "Frame-stable elements", + Clock = DrawableRuleset.FrameStableClock, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + ScoreProcessor, + HealthProcessor, + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }, + } + }, }); - DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }); - - DrawableRuleset.Overlays.Add(ScoreProcessor); - DrawableRuleset.Overlays.Add(HealthProcessor); - HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } From 07462120e47d46e0a8639f2868538eb822800d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 15:28:56 +0900 Subject: [PATCH 059/126] Split break tracking into its own component --- .../Visual/Gameplay/TestSceneAutoplay.cs | 3 +- ...eakOverlay.cs => TestSceneBreakTracker.cs} | 53 ++++++++---- .../Graphics/Containers/UserDimContainer.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 12 ++- osu.Game/Screens/Play/BreakOverlay.cs | 80 ++---------------- osu.Game/Screens/Play/BreakTracker.cs | 82 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 43 +++++----- 7 files changed, 160 insertions(+), 115 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneBreakOverlay.cs => TestSceneBreakTracker.cs} (80%) create mode 100644 osu.Game/Screens/Play/BreakTracker.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index afeda5fb7c..8108ce0864 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; +using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Screens.Play; @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.BreakOverlay.Breaks.First().StartTime)); + AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs similarity index 80% rename from osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 19dce303ea..d46b4ea289 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; using osu.Game.Screens.Play; @@ -12,14 +13,16 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneBreakOverlay : OsuTestScene + public class TestSceneBreakTracker : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BreakOverlay), }; - private readonly TestBreakOverlay breakOverlay; + private readonly BreakOverlay breakOverlay; + + private readonly TestBreakTracker breakTracker; private readonly IReadOnlyList testBreaks = new List { @@ -35,9 +38,23 @@ namespace osu.Game.Tests.Visual.Gameplay }, }; - public TestSceneBreakOverlay() + public TestSceneBreakTracker() { - Add(breakOverlay = new TestBreakOverlay(true)); + AddRange(new Drawable[] + { + breakTracker = new TestBreakTracker(), + breakOverlay = new BreakOverlay(true) + { + ProcessCustomClock = false, + } + }); + } + + protected override void Update() + { + base.Update(); + + breakOverlay.Clock = breakTracker.Clock; } [Test] @@ -88,7 +105,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadBreaksStep("multiple breaks", testBreaks); seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true); - AddAssert("is skipped to break #2", () => breakOverlay.CurrentBreakIndex == 1); + AddAssert("is skipped to break #2", () => breakTracker.CurrentBreakIndex == 1); seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true); seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false); @@ -110,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void addShowBreakStep(double seconds) { - AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List + AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = breakTracker.Breaks = new List { new BreakPeriod { @@ -122,12 +139,12 @@ namespace osu.Game.Tests.Visual.Gameplay private void setClock(bool useManual) { - AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakOverlay.SwitchClock(useManual)); + AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakTracker.SwitchClock(useManual)); } private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breaks); + AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); seekAndAssertBreak("seek back to 0", 0, false); } @@ -151,17 +168,18 @@ namespace osu.Game.Tests.Visual.Gameplay private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak) { - AddStep(seekStepDescription, () => breakOverlay.ManualClockTime = time); + AddStep(seekStepDescription, () => breakTracker.ManualClockTime = time); AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () => { - breakOverlay.ProgressTime(); - return breakOverlay.IsBreakTime.Value == shouldBeBreak; + breakTracker.ProgressTime(); + return breakTracker.IsBreakTime.Value == shouldBeBreak; }); } - private class TestBreakOverlay : BreakOverlay + private class TestBreakTracker : BreakTracker { - private readonly FramedClock framedManualClock; + public readonly FramedClock FramedManualClock; + private readonly ManualClock manualClock; private IFrameBasedClock originalClock; @@ -173,20 +191,19 @@ namespace osu.Game.Tests.Visual.Gameplay set => manualClock.CurrentTime = value; } - public TestBreakOverlay(bool letterboxing) - : base(letterboxing) + public TestBreakTracker() { - framedManualClock = new FramedClock(manualClock = new ManualClock()); + FramedManualClock = new FramedClock(manualClock = new ManualClock()); ProcessCustomClock = false; } public void ProgressTime() { - framedManualClock.ProcessFrame(); + FramedManualClock.ProcessFrame(); Update(); } - public void SwitchClock(bool setManual) => Clock = setManual ? framedManualClock : originalClock; + public void SwitchClock(bool setManual) => Clock = setManual ? FramedManualClock : originalClock; protected override void LoadComplete() { diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 4485ce3447..39c1fdad52 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Containers /// /// Whether player is in break time. - /// Must be bound to to allow for dim adjustments in gameplay. + /// Must be bound to to allow for dim adjustments in gameplay. /// public readonly IBindable IsBreakTime = new Bindable(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ff6ed5bf17..5062c92afe 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.UI /// public override Playfield Playfield => playfield.Value; - private Container overlays; + public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override Container Overlays => overlays; + public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; @@ -187,11 +187,12 @@ namespace osu.Game.Rulesets.UI FrameStablePlayback = FrameStablePlayback, Children = new Drawable[] { + FrameStableComponents, KeyBindingInputManager .WithChild(CreatePlayfieldAdjustmentContainer() .WithChild(Playfield) ), - overlays = new Container { RelativeSizeAxes = Axes.Both } + Overlays, } }, }; @@ -410,6 +411,11 @@ namespace osu.Game.Rulesets.UI /// public abstract Container Overlays { get; } + /// + /// Components to be run potentially multiple times in line with frame-stable gameplay. + /// + public abstract Container FrameStableComponents { get; } + /// /// The frame-stable clock which is being used for playfield display. /// diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index ee8be87352..89f51315f2 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,8 +14,6 @@ namespace osu.Game.Screens.Play { public class BreakOverlay : Container { - private readonly ScoreProcessor scoreProcessor; - /// /// The duration of the break overlay fading. /// @@ -37,10 +33,6 @@ namespace osu.Game.Screens.Play { breaks = value; - // reset index in case the new breaks list is smaller than last one - isBreakTime.Value = false; - CurrentBreakIndex = 0; - if (IsLoaded) initializeBreaks(); } @@ -48,27 +40,17 @@ namespace osu.Game.Screens.Play public override bool RemoveCompletedTransforms => false; - /// - /// Whether the gameplay is currently in a break. - /// - public IBindable IsBreakTime => isBreakTime; - - protected int CurrentBreakIndex; - - private readonly BindableBool isBreakTime = new BindableBool(); - private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; - private readonly BreakInfo info; private readonly BreakArrows breakArrows; - private readonly double gameplayStartTime; - public BreakOverlay(bool letterboxing, double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) { - this.gameplayStartTime = gameplayStartTime; - this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; + + BreakInfo info; + Child = fadeContainer = new Container { Alpha = 0, @@ -119,13 +101,11 @@ namespace osu.Game.Screens.Play } }; - if (scoreProcessor != null) bindProcessor(scoreProcessor); - } - - [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) - { - if (clock != null) Clock = clock; + if (scoreProcessor != null) + { + info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); + info.GradeDisplay.Current.BindTo(scoreProcessor.Rank); + } } protected override void LoadComplete() @@ -134,42 +114,6 @@ namespace osu.Game.Screens.Play initializeBreaks(); } - protected override void Update() - { - base.Update(); - updateBreakTimeBindable(); - } - - private void updateBreakTimeBindable() => - isBreakTime.Value = getCurrentBreak()?.HasEffect == true - || Clock.CurrentTime < gameplayStartTime - || scoreProcessor?.HasCompleted == true; - - private BreakPeriod getCurrentBreak() - { - if (breaks?.Count > 0) - { - var time = Clock.CurrentTime; - - if (time > breaks[CurrentBreakIndex].EndTime) - { - while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) - CurrentBreakIndex++; - } - else - { - while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) - CurrentBreakIndex--; - } - - var closest = breaks[CurrentBreakIndex]; - - return closest.Contains(time) ? closest : null; - } - - return null; - } - private void initializeBreaks() { FinishTransforms(true); @@ -207,11 +151,5 @@ namespace osu.Game.Screens.Play } } } - - private void bindProcessor(ScoreProcessor processor) - { - info.AccuracyDisplay.Current.BindTo(processor.Accuracy); - info.GradeDisplay.Current.BindTo(processor.Rank); - } } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs new file mode 100644 index 0000000000..64262d52b5 --- /dev/null +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -0,0 +1,82 @@ +// 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.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play +{ + public class BreakTracker : Component + { + private readonly ScoreProcessor scoreProcessor; + + private readonly double gameplayStartTime; + + /// + /// Whether the gameplay is currently in a break. + /// + public IBindable IsBreakTime => isBreakTime; + + protected int CurrentBreakIndex; + + private readonly BindableBool isBreakTime = new BindableBool(); + + private IReadOnlyList breaks; + + public IReadOnlyList Breaks + { + get => breaks; + set + { + breaks = value; + + // reset index in case the new breaks list is smaller than last one + isBreakTime.Value = false; + CurrentBreakIndex = 0; + } + } + + public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + { + this.gameplayStartTime = gameplayStartTime; + this.scoreProcessor = scoreProcessor; + } + + protected override void Update() + { + base.Update(); + + isBreakTime.Value = getCurrentBreak()?.HasEffect == true + || Clock.CurrentTime < gameplayStartTime + || scoreProcessor?.HasCompleted == true; + } + + private BreakPeriod getCurrentBreak() + { + if (breaks?.Count > 0) + { + var time = Clock.CurrentTime; + + if (time > breaks[CurrentBreakIndex].EndTime) + { + while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) + CurrentBreakIndex++; + } + else + { + while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) + CurrentBreakIndex--; + } + + var closest = breaks[CurrentBreakIndex]; + + return closest.Contains(time) ? closest : null; + } + + return null; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3ff47b868c..9ad500039e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -76,6 +76,8 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; + private BreakTracker breakTracker; + protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -204,7 +206,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } private void addUnderlayComponents(Container target) @@ -231,6 +233,18 @@ namespace osu.Game.Screens.Play DrawableRuleset, new ComboEffects(ScoreProcessor) }); + + DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] + { + ScoreProcessor, + HealthProcessor, + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = working.Beatmap.Breaks + } + }); + + HealthProcessor.IsBreakTime.BindTo(breakTracker.IsBreakTime); } private void addOverlayComponents(Container target, WorkingBeatmap working) @@ -294,26 +308,13 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - new Container + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks) { - Name = "Frame-stable elements", Clock = DrawableRuleset.FrameStableClock, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - ScoreProcessor, - HealthProcessor, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }, - } + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks }, }); - - HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } private void onBreakTimeChanged(ValueChangedEvent isBreakTime) @@ -325,7 +326,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value - && !BreakOverlay.IsBreakTime.Value; + && !breakTracker.IsBreakTime.Value; private IBeatmap loadPlayableBeatmap() { @@ -547,7 +548,7 @@ namespace osu.Game.Screens.Play PauseOverlay.Hide(); // breaks and time-based conditions may allow instant resume. - if (BreakOverlay.IsBreakTime.Value) + if (breakTracker.IsBreakTime.Value) completeResume(); else DrawableRuleset.RequestResume(completeResume); @@ -581,8 +582,8 @@ namespace osu.Game.Screens.Play Background.BlurAmount.Value = 0; // bind component bindables. - Background.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); - DimmableStoryboard.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); + Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); + DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 2949e8dc27d66ec99df393b12543fecd8241b471 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 16:58:23 +0900 Subject: [PATCH 060/126] Reduce spread of stacked fruit --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index e361b29a9d..8fa9c61b6f 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -141,14 +141,14 @@ namespace osu.Game.Rulesets.Catch.UI var ourRadius = fruit.DisplayRadius; float theirRadius = 0; - const float allowance = 6; + const float allowance = 10; while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) { var diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; + fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2; fruit.Y -= RNG.NextSingle() * diff; } From 8e4896fbbecce162e327d1c4af60dce652985c6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 17:13:53 +0900 Subject: [PATCH 061/126] Make slider judgements count towards base score / accuracy --- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 2 -- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index a8fd3764c5..ac6c6905e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderRepeatJudgement : OsuJudgement { - public override bool IsBonus => true; - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 212a84c04a..22f3f559db 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTickJudgement : OsuJudgement { - public override bool IsBonus => true; - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; } } From 6555ab6ede959af102069e1c29ae4d29924d8242 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 17:18:27 +0900 Subject: [PATCH 062/126] Only play slider end sounds if tracking --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 2d5b9d874c..35d58b7111 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.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = Ball.Tracking ? r.Judgement.MaxResult : HitResult.Miss); } protected override void UpdateStateTransforms(ArmedState state) From f80efd10c22aefe8678374ce3b14075d619462b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 19:51:02 +0900 Subject: [PATCH 063/126] Avoid using a miss judgement --- .../Objects/Drawables/DrawableSlider.cs | 10 +++++++++- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 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 35d58b7111..5c7f4a42b3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -194,7 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => r.Type = Ball.Tracking ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + + public override void PlaySamples() + { + // rather than doing it this way, we should probably attach the sample to the tail circle. + // this can only be done after we stop using LegacyLastTick. + if (TailCircle.Result.Type != HitResult.Miss) + base.PlaySamples(); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index aa29e42fac..5b5802fa9d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// - public void PlaySamples() => Samples?.Play(); + public virtual void PlaySamples() => Samples?.Play(); protected override void Update() { From 01c9112f82136510ae96dbd918e698ee9623ae81 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 26 Mar 2020 17:09:22 +0100 Subject: [PATCH 064/126] Add a null check to prevent NRE when playing the "no video" version of a beatmap. --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 00df388d09..d4dbdf1ea8 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -55,6 +55,8 @@ namespace osu.Game.Storyboards.Drawables { base.LoadComplete(); + if (videoSprite == null) return; + using (videoSprite.BeginAbsoluteSequence(0)) videoSprite.FadeIn(500); } From 902734b75e8a0b0ceb65e2c5c46f3f2d7bbd2972 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 20:32:43 +0200 Subject: [PATCH 065/126] Add failing test --- .../SongSelect/TestSceneBeatmapCarousel.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 0cc37bbd57..efe79d88ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -83,6 +83,38 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count, 3); } + [Test] + public void TestTraversalHold() + { + var sets = new List(); + + for (int i = 0; i < 20; i++) + { + var set = createTestBeatmapSet(i); + sets.Add(set); + } + + loadBeatmaps(sets); + + void selectNextAndAssert(int amount) + { + setSelected(1, 1); + AddStep($"Next beatmap {amount} times", () => + { + for (int i = 0; i < amount; i++) + { + carousel.SelectNext(); + } + }); + waitForSelection(amount + 1); + } + + for (int i = 1; i < 15; i += i) + { + selectNextAndAssert(i); + } + } + /// /// Test filtering /// From e707adb7738fcf6a72ea4bdb8c4c5a9ecfd361cb Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 21:16:10 +0200 Subject: [PATCH 066/126] Increase amount of test sets --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index efe79d88ab..31114dfd25 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -87,8 +87,9 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestTraversalHold() { var sets = new List(); + const int create_this_many_sets = 200; - for (int i = 0; i < 20; i++) + for (int i = 0; i < create_this_many_sets; i++) { var set = createTestBeatmapSet(i); sets.Add(set); @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(amount + 1); } - for (int i = 1; i < 15; i += i) + for (int i = 1; i < create_this_many_sets; i += i) { selectNextAndAssert(i); } From f75c0826018a72977b3edca3ae469e93f6a28dee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Mar 2020 15:50:11 +0900 Subject: [PATCH 067/126] Fix osu!mania replays recording incorrectly when key mod applied --- .../Replays/ManiaReplayFrame.cs | 21 +++++++------------ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 6 +++--- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index b93e372027..8c73c36e99 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Mania.Beatmaps; @@ -26,13 +27,7 @@ namespace osu.Game.Rulesets.Mania.Replays public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - // We don't need to fully convert, just create the converter - var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); - - // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling - // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. - - var stage = new StageDefinition { Columns = converter.TargetColumns }; + var maniaBeatmap = (ManiaBeatmap)beatmap; var normalAction = ManiaAction.Key1; var specialAction = ManiaAction.Special1; @@ -42,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays while (activeColumns > 0) { - var isSpecial = stage.IsSpecialColumn(counter); + var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter); if ((activeColumns & 1) > 0) Actions.Add(isSpecial ? specialAction : normalAction); @@ -59,17 +54,15 @@ namespace osu.Game.Rulesets.Mania.Replays public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { + var maniaBeatmap = (ManiaBeatmap)beatmap; + int keys = 0; - var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); - - var stage = new StageDefinition { Columns = converter.TargetColumns }; - var specialColumns = new List(); - for (int i = 0; i < converter.TargetColumns; i++) + for (int i = 0; i < maniaBeatmap.TotalColumns; i++) { - if (stage.IsSpecialColumn(i)) + if (maniaBeatmap.Stages.First().IsSpecialColumn(i)) specialColumns.Add(i); } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 58b64e1b8f..c356dd246d 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -45,9 +45,6 @@ namespace osu.Game.Scoring.Legacy if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); - currentBeatmap = workingBeatmap.Beatmap; - scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; - scoreInfo.User = new User { Username = sr.ReadString() }; // MD5Hash @@ -68,6 +65,9 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); + currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); + scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc5bac9fd1..c570f4bf4f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -657,7 +657,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream); + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } } From 6788b7f9cd9222c4dffa9fe46792b4a179e053c4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 27 Mar 2020 09:43:51 +0100 Subject: [PATCH 068/126] Add test for loading storyboards with missing video file. --- .../Resources/storyboard_no_video.osu | 31 ++++++++++++++++ .../Visual/Gameplay/TestSceneStoryboard.cs | 37 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 osu.Game.Tests/Resources/storyboard_no_video.osu diff --git a/osu.Game.Tests/Resources/storyboard_no_video.osu b/osu.Game.Tests/Resources/storyboard_no_video.osu new file mode 100644 index 0000000000..25f1ff6361 --- /dev/null +++ b/osu.Game.Tests/Resources/storyboard_no_video.osu @@ -0,0 +1,31 @@ +osu file format v14 + +[Events] +//Background and Video events +0,0,"BG.jpg",0,0 +Video,0,"video.avi" +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +1674,333.333333333333,4,2,1,70,1,0 +1674,-100,4,2,1,70,0,0 +3340,-100,4,2,1,70,0,0 +3507,-100,4,2,1,70,0,0 +3673,-100,4,2,1,70,0,0 + +[Colours] +Combo1 : 240,80,80 +Combo2 : 171,252,203 +Combo3 : 128,128,255 +Combo4 : 249,254,186 + +[HitObjects] +148,303,1674,5,6,3:2:0:0: +378,252,1840,1,0,0:0:0:0: +389,270,2340,5,2,0:1:0:0: diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index ff8437311e..9f1492a25f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -9,8 +9,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; using osu.Game.Overlays; +using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Resources; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay @@ -54,7 +58,11 @@ namespace osu.Game.Tests.Visual.Gameplay State = { Value = Visibility.Visible }, } }); + } + [Test] + public void TestStoryboard() + { AddStep("Restart", restart); AddToggleStep("Passing", passing => { @@ -62,6 +70,12 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestStoryboardMissingVideo() + { + AddStep("Load storyboard with missing video", loadStoryboardNoVideo); + } + [BackgroundDependencyLoader] private void load() { @@ -94,5 +108,28 @@ namespace osu.Game.Tests.Visual.Gameplay storyboardContainer.Add(storyboard); decoupledClock.ChangeSource(working.Track); } + + private void loadStoryboardNoVideo() + { + if (storyboard != null) + storyboardContainer.Remove(storyboard); + + var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + storyboardContainer.Clock = decoupledClock; + + Storyboard sb; + + using (var str = TestResources.OpenResource("storyboard_no_video.osu")) + using (var bfr = new LineBufferedReader(str)) + { + var decoder = new LegacyStoryboardDecoder(); + sb = decoder.Decode(bfr); + } + + storyboard = sb.CreateDrawable(Beatmap.Value); + + storyboardContainer.Add(storyboard); + decoupledClock.ChangeSource(Beatmap.Value.Track); + } } } From 4106700771f8581ca07846d291aa343c121f0884 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Mar 2020 20:51:44 +0900 Subject: [PATCH 069/126] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7e17f9da16..b147fdd05b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3894c06994..781c566b5f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9cc9792ecf..a2c6106931 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 46af4bce32eb176c459514b04aac08a95cf44e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 19:42:45 +0100 Subject: [PATCH 070/126] Cover regression in autoplay test --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 8108ce0864..5ee17aeea2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.Break; namespace osu.Game.Tests.Visual.Gameplay { @@ -27,7 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); - AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } From adc759771ff73e3b3b023b624348cf9655c9f017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 19:47:42 +0100 Subject: [PATCH 071/126] Hook up score processor in player --- 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 118cea324c..8693035103 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -308,7 +308,7 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks) + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 3a3bfe9a5ea14477da9fdc67d42b9f6fe16598e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 21:19:49 +0100 Subject: [PATCH 072/126] Reorder children to fix pause overlay z-order --- osu.Game/Screens/Play/Player.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8693035103..63ec3b0d2d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -251,6 +251,12 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks + }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), @@ -308,12 +314,6 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) - { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks - }, }); } From 15fb1a099e4d96e725a6c46072fdf6b782bfd529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 28 Mar 2020 00:42:51 +0100 Subject: [PATCH 073/126] Modify assert to avoid false failures In headless tests it was possible for TestInstantLoad() to erroneously fail. There were two scenarios in which LoadingSpinner could be null: 1. If the test runner was quick enough, the assert could end up running even before Loader.OnEntering() had even had a chance to, meaning that the spinner was never even actually assigned to or instantiated at that point in time. 2. Even if Loader.OnEntering() had managed to run, there was also a possibility that the spinner itself wasn't loaded at the point of checking the assertion. As the spinner is accessed through ChildrenOfType(), which only checks InternalChildren and ignores all currently-loading drawables, it would therefore return null. As null != 0, both of these cases would actually fail the test (this is best seen running headless, preferably with a [Repeat] attribute attached). To resolve, allow the spinner to be null at the point of asserting and duplicate the assertion step at the end. This weakens the test, as case (1) should probably be waited for and case (2) could be solved with exposition as protected in the base, but when attempting to wait for the loader itself to be loaded there were also cases where the appropriate until step would take so much time that the spinner would actually become visible in line with the delayed display logic, so this is a best-effort attempt to address both points without radical changes. --- osu.Game.Tests/Visual/Menus/TestSceneLoader.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index b3064ba9be..c44363d9ea 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestInstantLoad() { - // visual only, very impossible to test this using asserts. - AddStep("load immediately", () => { loader = new TestLoader(); @@ -46,12 +44,17 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); - AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0); + spinnerNotPresentOrHidden(); AddUntilStep("loaded", () => loader.ScreenLoaded); AddUntilStep("not current", () => !loader.IsCurrentScreen()); + + spinnerNotPresentOrHidden(); } + private void spinnerNotPresentOrHidden() => + AddAssert("spinner did not display", () => loader.LoadingSpinner == null || loader.LoadingSpinner.Alpha == 0); + [Test] public void TestDelayedLoad() { From a317ef65b8b5d301689e526bec8efd6b453743bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 Mar 2020 12:18:28 +0900 Subject: [PATCH 074/126] Remove default for argument --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index d46b4ea289..ff25e609c1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRange(new Drawable[] { breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true) + breakOverlay = new BreakOverlay(true, null) { ProcessCustomClock = false, } diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 89f51315f2..c978f4e96d 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Play private readonly RemainingTimeCounter remainingTimeCounter; private readonly BreakArrows breakArrows; - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { RelativeSizeAxes = Axes.Both; From fb4b334ce2f9a5e44bf82cd846a7af98f7487388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 Mar 2020 13:39:08 +0900 Subject: [PATCH 075/126] Add support for legacy skin sliderstartcircle / sliderstartcircleoverlay --- .../metrics-skin/sliderstartcircle@2x.png | Bin 0 -> 17245 bytes .../sliderstartcircleoverlay@2x.png | Bin 0 -> 50009 bytes .../Objects/Drawables/DrawableHitCircle.cs | 4 +++- .../Objects/Drawables/DrawableSliderHead.cs | 2 ++ osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 + .../Skinning/LegacyMainCirclePiece.cs | 21 +++++++++++++++--- .../Skinning/OsuLegacySkinTransformer.cs | 6 +++++ 7 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d630443cd7e2222d3a676e8d77219873660ebe0 GIT binary patch literal 17245 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%0A6c)ubJ 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 OsuSkinComponents.HitCircle; + public DrawableHitCircle(HitCircle h) : base(h) { @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return true; }, }, - CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()), + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()), ApproachCircle = new ApproachCircle { Alpha = 0, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c5609b01e0..a360071f26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly IBindable positionBindable = new Bindable(); private readonly IBindable pathVersion = new Bindable(); + protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; + private readonly Slider slider; public DrawableSliderHead(Slider slider, HitCircle h) diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 4ea4220faf..b2cdc8ccbf 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu ApproachCircle, ReverseArrow, HitCircleText, + SliderHeadHitCircle, SliderFollowCircle, SliderBall, SliderBody, diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 93ae0371df..38ba4c5974 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -6,6 +6,7 @@ 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.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; @@ -18,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Skinning { public class LegacyMainCirclePiece : CompositeDrawable { - public LegacyMainCirclePiece() + private readonly string priorityLookup; + + public LegacyMainCirclePiece(string priorityLookup = null) { + this.priorityLookup = priorityLookup; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } @@ -39,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { hitCircleSprite = new Sprite { - Texture = skin.GetTexture("hitcircle"), + Texture = getTextureWithFallback(string.Empty), Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -51,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning }, confineMode: ConfineMode.NoScaling), new Sprite { - Texture = skin.GetTexture("hitcircleoverlay"), + Texture = getTextureWithFallback("overlay"), Anchor = Anchor.Centre, Origin = Anchor.Centre, } @@ -65,6 +70,16 @@ namespace osu.Game.Rulesets.Osu.Skinning indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + + Texture getTextureWithFallback(string name) + { + Texture tex = null; + + if (!string.IsNullOrEmpty(priorityLookup)) + tex = skin.GetTexture($"{priorityLookup}{name}"); + + return tex ?? skin.GetTexture($"hitcircle{name}"); + } } private void updateState(ValueChangedEvent state) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index d6c3f443eb..075c536b4c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -82,6 +82,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderHeadHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderstartcircle"); + + return null; + case OsuSkinComponents.HitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece(); From fc3f9ff6faf06324e32c7964d5af915a0b16ffcb Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 12:54:48 +0200 Subject: [PATCH 076/126] Don't use drawables for select next --- osu.Game/Screens/Select/BeatmapCarousel.cs | 57 +++++++++------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index fa8974f55a..df2c1236f4 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,46 +253,35 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - var visibleItems = Items.Where(s => !s.Item.Filtered.Value).ToList(); - - if (!visibleItems.Any()) + if (!root.Children.Where(s => !s.Filtered.Value).ToList().Any()) return; - DrawableCarouselItem drawable = null; + if (skipDifficulties) + selectNextSet(direction, true); + else + selectNextDifficulty(direction); + } - if (selectedBeatmap != null && (drawable = selectedBeatmap.Drawables.FirstOrDefault()) == null) - // if the selected beatmap isn't present yet, we can't correctly change selection. - // we can fix this by changing this method to not reference drawables / Items in the first place. - return; + private void selectNextSet(int direction, bool skipDifficulties) + { + var visibleSets = root.Children.OfType().Where(s => !s.Filtered.Value).ToList(); - int originalIndex = visibleItems.IndexOf(drawable); - int currentIndex = originalIndex; + var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; - // local function to increment the index in the required direction, wrapping over extremities. - int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; + if (skipDifficulties) + select(item); + else + select(direction > 0 ? item.Beatmaps.First(b => !b.Filtered.Value) : item.Beatmaps.Last(b => !b.Filtered.Value)); + } - while (incrementIndex() != originalIndex) - { - var item = visibleItems[currentIndex].Item; - - if (item.Filtered.Value || item.State.Value == CarouselItemState.Selected) continue; - - switch (item) - { - case CarouselBeatmap beatmap: - if (skipDifficulties) continue; - - select(beatmap); - return; - - case CarouselBeatmapSet set: - if (skipDifficulties) - select(set); - else - select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered.Value) : set.Beatmaps.Last(b => !b.Filtered.Value)); - return; - } - } + private void selectNextDifficulty(int direction) + { + var difficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); + int index = difficulties.IndexOf(selectedBeatmap); + if (index + direction < 0 || index + direction >= difficulties.Count) + selectNextSet(direction, false); + else + select(difficulties[index + direction]); } /// From 6a0c5c87aa52c908af5f632b58b7849e45988c6e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:06:03 +0200 Subject: [PATCH 077/126] Use already existing variable --- 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 df2c1236f4..a6cbf58023 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!root.Children.Where(s => !s.Filtered.Value).ToList().Any()) + if (!beatmapSets.Where(s => !s.Filtered.Value).ToList().Any()) return; if (skipDifficulties) @@ -264,7 +264,7 @@ namespace osu.Game.Screens.Select private void selectNextSet(int direction, bool skipDifficulties) { - var visibleSets = root.Children.OfType().Where(s => !s.Filtered.Value).ToList(); + var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; From 659865b45762ee153f02888e8bbfdc92513b83b3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:08:06 +0200 Subject: [PATCH 078/126] Use understandable set id --- 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 31114dfd25..a811e58694 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < create_this_many_sets; i++) { - var set = createTestBeatmapSet(i); + var set = createTestBeatmapSet(i + 1); sets.Add(set); } From 63f6269eb0ae7e88a8b810c6c7ba5690a9cea1dd Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:10:20 +0200 Subject: [PATCH 079/126] Test both ways --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index a811e58694..b316fcc60b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -83,8 +83,9 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count, 3); } - [Test] - public void TestTraversalHold() + [TestCase(true)] + [TestCase(false)] + public void TestTraversalHold(bool forwards) { var sets = new List(); const int create_this_many_sets = 200; @@ -99,15 +100,16 @@ namespace osu.Game.Tests.Visual.SongSelect void selectNextAndAssert(int amount) { - setSelected(1, 1); - AddStep($"Next beatmap {amount} times", () => + setSelected(forwards ? 1 : create_this_many_sets, 1); + string text = forwards ? "Next" : "Previous"; + AddStep($"{text} beatmap {amount} times", () => { for (int i = 0; i < amount; i++) { - carousel.SelectNext(); + carousel.SelectNext(forwards ? 1 : -1); } }); - waitForSelection(amount + 1); + waitForSelection(forwards ? amount + 1 : create_this_many_sets - amount); } for (int i = 1; i < create_this_many_sets; i += i) From 87854fc4fabea688d60922552f487037e4873d44 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:23:31 +0200 Subject: [PATCH 080/126] Rename variable --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a6cbf58023..91a9b19115 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -266,12 +266,12 @@ namespace osu.Game.Screens.Select { var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); - var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; + var nextSet = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; if (skipDifficulties) - select(item); + select(nextSet); else - select(direction > 0 ? item.Beatmaps.First(b => !b.Filtered.Value) : item.Beatmaps.Last(b => !b.Filtered.Value)); + select(direction > 0 ? nextSet.Beatmaps.First(b => !b.Filtered.Value) : nextSet.Beatmaps.Last(b => !b.Filtered.Value)); } private void selectNextDifficulty(int direction) From 2c27894527f91317e16e660f61a613c700110282 Mon Sep 17 00:00:00 2001 From: Endrik Date: Sat, 28 Mar 2020 19:58:33 +0200 Subject: [PATCH 081/126] Use All instead of ToList Any MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- 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 91a9b19115..cd2deb8abe 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!beatmapSets.Where(s => !s.Filtered.Value).ToList().Any()) + if (beatmapSets.All(s => !s.Filtered.Value)) return; if (skipDifficulties) From b4f05007063dcb46cb6736a817ccb5e18222a511 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 20:21:21 +0200 Subject: [PATCH 082/126] Invert logic --- 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 cd2deb8abe..a2c2cde7c7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (beatmapSets.All(s => !s.Filtered.Value)) + if (beatmapSets.All(s => s.Filtered.Value)) return; if (skipDifficulties) From 8cab303611786eeba91aa2a67bb25633f23e10c6 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 21:02:55 +0200 Subject: [PATCH 083/126] Cover skipDifficulties = false in tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index b316fcc60b..7c3498e034 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -118,6 +118,50 @@ namespace osu.Game.Tests.Visual.SongSelect } } + [Test] + public void TestTraversalHoldDifficulties() + { + var sets = new List(); + + for (int i = 0; i < 20; i++) + { + var set = createTestBeatmapSet(i + 1); + sets.Add(set); + } + + loadBeatmaps(sets); + + void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) + { + // Select very first or very last difficulty + setSelected(forwards ? 1 : 20, forwards ? 1 : 3); + string text = forwards ? "Next" : "Previous"; + AddStep($"{text} difficulty {amount} times", () => + { + for (int i = 0; i < amount; i++) + { + carousel.SelectNext(forwards ? 1 : -1, false); + } + }); + waitForSelection(expectedSet, expectedDiff); + } + + // Selects next set once, difficulty index doesn't change + selectNextAndAssert(3, true, 2, 1); + // Selects next set 16 times (50 // 3 == 16), difficulty index changes twice (50 % 3 == 2) + selectNextAndAssert(50, true, 17, 3); + // Travels around the carousel thrice (200/60 == 3) + // continues to select 20 times (200 % 60 == 20) + // selects next set 6 times (20 // 3 == 6) + // difficulty index changes twice (20 % 3 == 2) + selectNextAndAssert(200, true, 7, 3); + + // All same but in reverse + selectNextAndAssert(3, false, 19, 3); + selectNextAndAssert(50, false, 4, 1); + selectNextAndAssert(200, false, 14, 1); + } + /// /// Test filtering /// From d3114ca858718aa1f69fcd46c1c8faabdc3228f0 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 23:12:13 +0200 Subject: [PATCH 084/126] Don't snake when hit --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index b9cee71ca1..30abce7696 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,6 +87,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { + if (IsHit) return; + bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; From a2b3fe180e096a6f85ab034370821b917ce79345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 14:30:45 +0900 Subject: [PATCH 085/126] Add the ability to disable user input on specific DrawableHitObjects --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5b5802fa9d..9aad125ed1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -38,6 +38,19 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Lazy> nestedHitObjects = new Lazy>(); public IReadOnlyList NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList)Array.Empty(); + /// + /// Whether this object should handle any user input events. + /// + public bool HandleUserInput { get; set; } = true; + + public override bool HandlePositionalInput => HandleUserInput; + + public override bool HandleNonPositionalInput => HandleUserInput; + + public override bool PropagatePositionalInputSubTree => HandleUserInput; + + public override bool PropagateNonPositionalInputSubTree => HandleUserInput; + /// /// Invoked when a has been applied by this or a nested . /// From d1b01095ee292b02ad1af51ff6a74b49df8c8929 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 14:31:03 +0900 Subject: [PATCH 086/126] Rewrite to reduce code changes and complexities in hit object implementation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 17 ++++++-------- .../Objects/Drawables/DrawableSpinner.cs | 9 ++++---- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 23 ++++++++++++++----- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 49c4e7fa45..7b54baa99b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -30,21 +30,18 @@ namespace osu.Game.Rulesets.Osu.Mods { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.Enabled = false; - spinner.OnUpdate += autoSpin; + spinner.HandleUserInput = false; + spinner.OnUpdate += onSpinnerUpdate; } } } - private void autoSpin(Drawable drawable) + private void onSpinnerUpdate(Drawable drawable) { - if (drawable is DrawableSpinner spinner) - { - if (spinner.Disc.Valid) - spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); - if (!spinner.SpmCounter.IsPresent) - spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); - } + var spinner = (DrawableSpinner)drawable; + + spinner.Disc.Tracking = true; + spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 0ec7f2ebfe..3c8ab0f5ab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -176,17 +176,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { - Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) - SpmCounter.FadeIn(HitObject.TimeFadeIn); - base.Update(); + if (HandleUserInput) + Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); + if (!SpmCounter.IsPresent && Disc.Tracking) + SpmCounter.FadeIn(HitObject.TimeFadeIn); + circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.RotationAbsolute); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 0c089c1fed..d4ef039b79 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces get => tracking; set { - if ((Enabled && value) == tracking) return; + if (value == tracking) return; - tracking = Enabled && value; + tracking = value; background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } @@ -73,9 +73,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - - public bool Enabled { get; set; } = true; + /// + /// Whether currently in the correct time range to allow spinning. + /// + private bool isSpinnableTime => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var delta = thisAngle - lastAngle; - if (Valid && tracking) + if (tracking) Rotate(delta); lastAngle = thisAngle; @@ -118,8 +119,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } + /// + /// Rotate the disc by the provided angle (in addition to any existing rotation). + /// + /// + /// Will be a no-op if not a valid time to spin. + /// + /// The delta angle. public void Rotate(float angle) { + if (!isSpinnableTime) + return; + if (!rotationTransferred) { currentRotation = Rotation * 2; From 2ab8267f84090b9fcab62bbd143ec163e1f0c0f3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 29 Mar 2020 10:50:43 +0300 Subject: [PATCH 087/126] Add a comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 30abce7696..2704680d54 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,6 +87,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { + // When the repeat is hit, the arrow should fade out on spot, + // it should no longer follow snaking if (IsHit) return; bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; From 4f5557096c238ac602b59d3dab39605b93cb2ca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 22:51:28 +0900 Subject: [PATCH 088/126] Fix auto mod results not displaying correctly --- osu.Game/Screens/Play/Player.cs | 64 ++++++++++++++------------- osu.Game/Screens/Play/ReplayPlayer.cs | 6 +++ 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 63ec3b0d2d..5da53ad2c9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -637,6 +637,39 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + protected virtual void GotoRanking() + { + if (DrawableRuleset.ReplayScore != null) + { + // if a replay is present, we likely don't want to import into the local database. + this.Push(CreateResults(CreateScore())); + return; + } + + LegacyByteArrayReader replayReader = null; + + var score = new Score { ScoreInfo = CreateScore() }; + + if (recordingReplay?.Frames.Count > 0) + { + score.Replay = recordingReplay; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + } + } + + scoreManager.Import(score.ScoreInfo, replayReader) + .ContinueWith(imported => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(imported.Result)); + })); + } + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; @@ -649,36 +682,7 @@ namespace osu.Game.Screens.Play private void scheduleGotoRanking() { completionProgressDelegate?.Cancel(); - completionProgressDelegate = Schedule(delegate - { - if (DrawableRuleset.ReplayScore != null) - this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); - else - { - var score = new Score { ScoreInfo = CreateScore() }; - - LegacyByteArrayReader replayReader = null; - - if (recordingReplay?.Frames.Count > 0) - { - score.Replay = recordingReplay; - - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); - } - } - - scoreManager.Import(score.ScoreInfo, replayReader) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); - } - }); + completionProgressDelegate = Schedule(GotoRanking); } #endregion diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8708b5f634..74c853340d 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.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.Screens; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -23,6 +24,11 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(score); } + protected override void GotoRanking() + { + this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); + } + protected override ScoreInfo CreateScore() => score.ScoreInfo; } } From 653480b2f855d103405f7e4bdd6c041fd5967eed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:29:46 +0900 Subject: [PATCH 089/126] Add regression test --- .../Visual/Gameplay/TestSceneAllRulesetPlayers.cs | 4 ---- .../Visual/Gameplay/TestSceneAutoplay.cs | 15 +++++++++++++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 8 ++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index 83a7b896d2..b7dcad3825 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -74,9 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = working; SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; - Player?.Exit(); - Player = null; - Player = CreatePlayer(ruleset); LoadScreen(Player); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 5ee17aeea2..43fb848ab8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -5,8 +5,11 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; +using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Gameplay { @@ -17,8 +20,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Player CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new TestPlayer(false, false); + SelectedMods.Value = new[] { ruleset.GetAutoplayMod() }; + return new TestPlayer(false); } protected override void AddCheckSteps() @@ -32,6 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + + AddStep("complete", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + AddUntilStep("results displayed", () => getResultsScreen() != null); + + AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); + AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0); + + ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 803b33a998..5e0c30c4c0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -31,13 +31,13 @@ namespace osu.Game.Screens.Ranking [Resolved(CanBeNull = true)] private Player player { get; set; } - private readonly ScoreInfo score; + public readonly ScoreInfo Score; private Drawable bottomPanel; public ResultsScreen(ScoreInfo score) { - this.score = score; + this.Score = score; } [BackgroundDependencyLoader] @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = new ScorePanel(score) + Child = new ScorePanel(Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ReplayDownloadButton(score) { Width = 300 }, + new ReplayDownloadButton(Score) { Width = 300 }, new RetryButton { Width = 300 }, } } From 07c7233b3d4f68acc11090ab78801bb6d6dbd97b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:46:28 +0900 Subject: [PATCH 090/126] Change int div comments --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 7c3498e034..c76ce628ba 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -148,11 +148,13 @@ namespace osu.Game.Tests.Visual.SongSelect // Selects next set once, difficulty index doesn't change selectNextAndAssert(3, true, 2, 1); - // Selects next set 16 times (50 // 3 == 16), difficulty index changes twice (50 % 3 == 2) + + // Selects next set 16 times (50 \ 3 == 16), difficulty index changes twice (50 % 3 == 2) selectNextAndAssert(50, true, 17, 3); - // Travels around the carousel thrice (200/60 == 3) - // continues to select 20 times (200 % 60 == 20) - // selects next set 6 times (20 // 3 == 6) + + // Travels around the carousel thrice (200 \ 60 == 3) + // continues to select 20 times (200 \ 60 == 20) + // selects next set 6 times (20 \ 3 == 6) // difficulty index changes twice (20 % 3 == 2) selectNextAndAssert(200, true, 7, 3); From 66a990cd5e20ebf0022626900bb95ba972e466ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:50:16 +0900 Subject: [PATCH 091/126] Remove redundant this --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5e0c30c4c0..d063d8749f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking public ResultsScreen(ScoreInfo score) { - this.Score = score; + Score = score; } [BackgroundDependencyLoader] From 6e68b968f8afba0ebd5824fda4a94b9495fbb055 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:52:50 +0900 Subject: [PATCH 092/126] Hide "retry" button on results screen after watching a replay --- osu.Game/Screens/Play/ReplayPlayer.cs | 3 +++ osu.Game/Screens/Ranking/ResultsScreen.cs | 29 +++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 74c853340d..0d2ddb7b01 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Screens; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -29,6 +30,8 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); } + protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); + protected override ScoreInfo CreateScore() => score.ScoreInfo; } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index d063d8749f..1c08b763fe 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -33,16 +33,21 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; + private readonly bool allowRetry; + private Drawable bottomPanel; - public ResultsScreen(ScoreInfo score) + public ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; + this.allowRetry = allowRetry; } [BackgroundDependencyLoader] private void load() { + FillFlowContainer buttons; + InternalChildren = new[] { new ResultsScrollContainer @@ -68,7 +73,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new FillFlowContainer + buttons = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -78,7 +83,6 @@ namespace osu.Game.Screens.Ranking Children = new Drawable[] { new ReplayDownloadButton(Score) { Width = 300 }, - new RetryButton { Width = 300 }, } } } @@ -87,15 +91,20 @@ namespace osu.Game.Screens.Ranking if (player != null) { - AddInternal(new HotkeyRetryOverlay + if (allowRetry) { - Action = () => - { - if (!this.IsCurrentScreen()) return; + buttons.Add(new RetryButton { Width = 300 }); - player?.Restart(); - }, - }); + AddInternal(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + player?.Restart(); + }, + }); + } } } From a72f0f57f6662bec55cdce569e6d071328f7fdcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:05:07 +0900 Subject: [PATCH 093/126] Refactor tests for readability --- .../SongSelect/TestSceneBeatmapCarousel.cs | 67 +++++++++---------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c76ce628ba..f29d532857 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -85,67 +85,48 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(true)] [TestCase(false)] - public void TestTraversalHold(bool forwards) + public void TestTraversalBeyondVisible(bool forwards) { var sets = new List(); - const int create_this_many_sets = 200; - for (int i = 0; i < create_this_many_sets; i++) - { - var set = createTestBeatmapSet(i + 1); - sets.Add(set); - } + const int total_set_count = 200; + + for (int i = 0; i < total_set_count; i++) + sets.Add(createTestBeatmapSet(i + 1)); loadBeatmaps(sets); + for (int i = 1; i < total_set_count; i += i) + selectNextAndAssert(i); + void selectNextAndAssert(int amount) { - setSelected(forwards ? 1 : create_this_many_sets, 1); - string text = forwards ? "Next" : "Previous"; - AddStep($"{text} beatmap {amount} times", () => + setSelected(forwards ? 1 : total_set_count, 1); + + AddStep($"{(forwards ? "Next" : "Previous")} beatmap {amount} times", () => { for (int i = 0; i < amount; i++) { carousel.SelectNext(forwards ? 1 : -1); } }); - waitForSelection(forwards ? amount + 1 : create_this_many_sets - amount); - } - for (int i = 1; i < create_this_many_sets; i += i) - { - selectNextAndAssert(i); + waitForSelection(forwards ? amount + 1 : total_set_count - amount); } } [Test] - public void TestTraversalHoldDifficulties() + public void TestTraversalBeyondVisibleDifficulties() { var sets = new List(); - for (int i = 0; i < 20; i++) - { - var set = createTestBeatmapSet(i + 1); - sets.Add(set); - } + const int total_set_count = 200; + + for (int i = 0; i < total_set_count; i++) + sets.Add(createTestBeatmapSet(i + 1)); loadBeatmaps(sets); - void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) - { - // Select very first or very last difficulty - setSelected(forwards ? 1 : 20, forwards ? 1 : 3); - string text = forwards ? "Next" : "Previous"; - AddStep($"{text} difficulty {amount} times", () => - { - for (int i = 0; i < amount; i++) - { - carousel.SelectNext(forwards ? 1 : -1, false); - } - }); - waitForSelection(expectedSet, expectedDiff); - } - // Selects next set once, difficulty index doesn't change selectNextAndAssert(3, true, 2, 1); @@ -162,6 +143,20 @@ namespace osu.Game.Tests.Visual.SongSelect selectNextAndAssert(3, false, 19, 3); selectNextAndAssert(50, false, 4, 1); selectNextAndAssert(200, false, 14, 1); + + void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) + { + // Select very first or very last difficulty + setSelected(forwards ? 1 : 20, forwards ? 1 : 3); + + AddStep($"{(forwards ? "Next" : "Previous")} difficulty {amount} times", () => + { + for (int i = 0; i < amount; i++) + carousel.SelectNext(forwards ? 1 : -1, false); + }); + + waitForSelection(expectedSet, expectedDiff); + } } /// From b47a532df353f45ec95ef51e9e6d0f383f832502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:07:48 +0900 Subject: [PATCH 094/126] Adjust code formatting slightly --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a2c2cde7c7..59dddc2baa 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -264,9 +264,9 @@ namespace osu.Game.Screens.Select private void selectNextSet(int direction, bool skipDifficulties) { - var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); + var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); - var nextSet = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; + var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count]; if (skipDifficulties) select(nextSet); @@ -276,12 +276,14 @@ namespace osu.Game.Screens.Select private void selectNextDifficulty(int direction) { - var difficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); - int index = difficulties.IndexOf(selectedBeatmap); - if (index + direction < 0 || index + direction >= difficulties.Count) + var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); + + int index = unfilteredDifficulties.IndexOf(selectedBeatmap); + + if (index + direction < 0 || index + direction >= unfilteredDifficulties.Count) selectNextSet(direction, false); else - select(difficulties[index + direction]); + select(unfilteredDifficulties[index + direction]); } /// From f4c8b6d219001b82ee8494ed00ee1a14d27f4a3a Mon Sep 17 00:00:00 2001 From: Endrik Date: Sun, 29 Mar 2020 18:55:47 +0300 Subject: [PATCH 095/126] Fix copy paste oversight --- 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 f29d532857..76a8ee9914 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect { var sets = new List(); - const int total_set_count = 200; + const int total_set_count = 20; for (int i = 0; i < total_set_count; i++) sets.Add(createTestBeatmapSet(i + 1)); From 98a700ef3a70244f4c8bd9e44631593debf1e269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:58:06 +0900 Subject: [PATCH 096/126] Attempt to fix tests by skipping one break at a time --- .../Visual/Gameplay/TestSceneAutoplay.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 43fb848ab8..4b1c2ec256 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -28,15 +29,16 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); - AddUntilStep("wait for seek to complete", () => - Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); + seekToBreak(0); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); - AddStep("complete", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + seekToBreak(0); + seekToBreak(1); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); AddUntilStep("results displayed", () => getResultsScreen() != null); AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); @@ -44,5 +46,13 @@ namespace osu.Game.Tests.Visual.Gameplay ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } + + private void seekToBreak(int breakIndex) + { + AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime)); + AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime); + + BreakPeriod destBreak() => Player.ChildrenOfType().First().Breaks.ElementAt(breakIndex); + } } } From d99b445720b0b6a6d994ee148192d2903626a116 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 09:59:52 +0900 Subject: [PATCH 097/126] Move non-headless tests to correct namespace --- .../{ => Visual}/Gameplay/TestSceneReplayRecorder.cs | 3 +-- .../{ => Visual}/Gameplay/TestSceneReplayRecording.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) rename osu.Game.Tests/{ => Visual}/Gameplay/TestSceneReplayRecorder.cs (99%) rename osu.Game.Tests/{ => Visual}/Gameplay/TestSceneReplayRecording.cs (99%) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs similarity index 99% rename from osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 734991b868..c7455583e4 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -17,13 +17,12 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; using osuTK.Input; -namespace osu.Game.Tests.Gameplay +namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneReplayRecorder : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs similarity index 99% rename from osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 057d026132..7822f07957 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -13,12 +13,11 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Gameplay +namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneReplayRecording : OsuTestScene { From 09d860d5f5701edaaa292f0797774d6758123952 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 11:52:11 +0900 Subject: [PATCH 098/126] Fix imports with no matching beatmap IDs still retaining a potentially invalid set ID --- osu.Game/Beatmaps/BeatmapManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index abb3f8ac42..40ffb40f52 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); @@ -103,7 +103,11 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); - return updateQueue.UpdateAsync(beatmapSet, cancellationToken); + await updateQueue.UpdateAsync(beatmapSet, cancellationToken); + + // ensure at least one beatmap was able to retrieve an online ID, else drop the set ID. + if (!beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + beatmapSet.OnlineBeatmapSetID = null; } protected override void PreImport(BeatmapSetInfo beatmapSet) From 7db9bd798c4b0d9dedc9097c416d61ce62197fa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 11:59:51 +0900 Subject: [PATCH 099/126] Remove handle overrides --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9aad125ed1..0011faefbb 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -43,10 +43,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public bool HandleUserInput { get; set; } = true; - public override bool HandlePositionalInput => HandleUserInput; - - public override bool HandleNonPositionalInput => HandleUserInput; - public override bool PropagatePositionalInputSubTree => HandleUserInput; public override bool PropagateNonPositionalInputSubTree => HandleUserInput; From 7ecce713bb736069be7f1037df45770482d6f349 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 15:05:40 +0900 Subject: [PATCH 100/126] Keep provided IDs where possible if not online --- osu.Game/Beatmaps/BeatmapManager.cs | 13 ++++++++----- osu.Game/Online/API/APIRequest.cs | 4 ++-- osu.Game/Online/API/Requests/GetRankingsRequest.cs | 3 ++- osu.Game/Online/API/Requests/PaginatedAPIRequest.cs | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 40ffb40f52..797a5160c9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,12 +451,15 @@ namespace osu.Game.Beatmaps var res = req.Result; - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + if (res != null) + { + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } } catch (Exception e) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 30c1018c1e..6a6c7b72a8 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -12,11 +12,11 @@ namespace osu.Game.Online.API /// An API request with a well-defined response type. /// /// Type of the response (used for deserialisation). - public abstract class APIRequest : APIRequest + public abstract class APIRequest : APIRequest where T : class { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result => ((OsuJsonWebRequest)WebRequest).ResponseObject; + public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject; protected APIRequest() { diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index 941691c4c1..1bbaa73bbb 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -6,7 +6,8 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public abstract class GetRankingsRequest : APIRequest + public abstract class GetRankingsRequest : APIRequest where TModel : class + { private readonly RulesetInfo ruleset; private readonly int page; diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs index 52e12f04ee..bddc34a0dc 100644 --- a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -6,7 +6,7 @@ using osu.Framework.IO.Network; namespace osu.Game.Online.API.Requests { - public abstract class PaginatedAPIRequest : APIRequest + public abstract class PaginatedAPIRequest : APIRequest where T : class { private readonly int page; private readonly int itemsPerPage; From f71c8cb30ff8c11084cc48bbe40fbfe437dbd089 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 15:07:56 +0900 Subject: [PATCH 101/126] Only drop online set ID if beatmap IDs were stripped in online retrieval --- osu.Game/Beatmaps/BeatmapManager.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 797a5160c9..6542866936 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -103,11 +103,19 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + await updateQueue.UpdateAsync(beatmapSet, cancellationToken); - // ensure at least one beatmap was able to retrieve an online ID, else drop the set ID. - if (!beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) - beatmapSet.OnlineBeatmapSetID = null; + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + { + if (beatmapSet.OnlineBeatmapSetID != null) + { + beatmapSet.OnlineBeatmapSetID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } } protected override void PreImport(BeatmapSetInfo beatmapSet) From 812583a4cd6e714626f0132a0351b62c1eea99db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 16:17:42 +0900 Subject: [PATCH 102/126] Remove stray newline --- osu.Game/Online/API/Requests/GetRankingsRequest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index 1bbaa73bbb..ddc3298ca7 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { public abstract class GetRankingsRequest : APIRequest where TModel : class - { private readonly RulesetInfo ruleset; private readonly int page; From 4719aac235727c684a2f3d20e80eceff02c4f801 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:18:09 +0900 Subject: [PATCH 103/126] Add basic mania skin parsing --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 12 +- .../Skinning/LegacyManiaSkinConfiguration.cs | 30 +++++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 106 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Skinning/LegacyManiaSkinConfiguration.cs create mode 100644 osu.Game/Skinning/LegacyManiaSkinDecoder.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index e28e235788..bbc0aad467 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -41,6 +41,7 @@ namespace osu.Game.Beatmaps.Formats section = Section.None; } + OnBeginNewSection(section); continue; } @@ -57,6 +58,14 @@ namespace osu.Game.Beatmaps.Formats protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.AsSpan().TrimStart().StartsWith("//".AsSpan(), StringComparison.Ordinal); + /// + /// Invoked when a new has been entered. + /// + /// The entered . + protected virtual void OnBeginNewSection(Section section) + { + } + protected virtual void ParseLine(T output, Section section, string line) { line = StripComments(line); @@ -139,7 +148,8 @@ namespace osu.Game.Beatmaps.Formats Colours, HitObjects, Variables, - Fonts + Fonts, + Mania } internal class LegacyDifficultyControlPoint : DifficultyControlPoint diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs new file mode 100644 index 0000000000..5dd185879b --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.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; + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinConfiguration + { + public readonly int Keys; + + public readonly float[] ColumnLineWidth; + public readonly float[] ColumnSpacing; + public readonly float[] ColumnWidth; + + public float HitPosition = 124.8f; // (480 - 402) * 1.6f + + public LegacyManiaSkinConfiguration(int keys) + { + Keys = keys; + + ColumnLineWidth = new float[keys + 1]; + ColumnSpacing = new float[keys - 1]; + ColumnWidth = new float[keys]; + + ColumnLineWidth.AsSpan().Fill(2); + ColumnWidth.AsSpan().Fill(48); + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs new file mode 100644 index 0000000000..153a2c9626 --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -0,0 +1,106 @@ +// 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.Diagnostics; +using System.Globalization; +using osu.Game.Beatmaps.Formats; + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinDecoder : LegacyDecoder> + { + private const float size_scale_factor = 1.6f; + + public LegacyManiaSkinDecoder() + : base(1) + { + } + + private readonly List pendingLines = new List(); + private LegacyManiaSkinConfiguration currentConfig; + + protected override void OnBeginNewSection(Section section) + { + base.OnBeginNewSection(section); + + // If a new section is reached with pending lines remaining, they can all be discarded as there isn't a valid configuration to parse them into. + pendingLines.Clear(); + currentConfig = null; + } + + protected override void ParseLine(List output, Section section, string line) + { + line = StripComments(line); + + switch (section) + { + case Section.Mania: + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case "Keys": + currentConfig = new LegacyManiaSkinConfiguration(int.Parse(pair.Value, CultureInfo.InvariantCulture)); + output.Add(currentConfig); + + // All existing lines can be flushed now that we have a valid configuration. + flushPendingLines(); + break; + + default: + pendingLines.Add(line); + + // Hold all lines until a "Keys" item is found. + if (currentConfig != null) + flushPendingLines(); + break; + } + + break; + } + } + + private void flushPendingLines() + { + Debug.Assert(currentConfig != null); + + foreach (var line in pendingLines) + { + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case "ColumnLineWidth": + parseArrayValue(pair.Value, currentConfig.ColumnLineWidth); + break; + + case "ColumnSpacing": + parseArrayValue(pair.Value, currentConfig.ColumnSpacing); + break; + + case "ColumnWidth": + parseArrayValue(pair.Value, currentConfig.ColumnWidth); + break; + + case "HitPosition": + currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; + break; + } + } + } + + private void parseArrayValue(string value, float[] output) + { + string[] values = value.Split(','); + + for (int i = 0; i < values.Length; i++) + { + if (i >= output.Length) + break; + + output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * size_scale_factor; + } + } + } +} From 881ec146afca5c8560c811ea3e1370b227aa6a3a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:36:57 +0900 Subject: [PATCH 104/126] Ignore duplicate configs --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 153a2c9626..ae6c8eeb15 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; using osu.Game.Beatmaps.Formats; namespace osu.Game.Skinning @@ -42,7 +43,10 @@ namespace osu.Game.Skinning { case "Keys": currentConfig = new LegacyManiaSkinConfiguration(int.Parse(pair.Value, CultureInfo.InvariantCulture)); - output.Add(currentConfig); + + // Silently ignore duplicate configurations. + if (output.All(c => c.Keys != currentConfig.Keys)) + output.Add(currentConfig); // All existing lines can be flushed now that we have a valid configuration. flushPendingLines(); From 1ce4f7c8545893786590da52a52184da8008af1b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:37:08 +0900 Subject: [PATCH 105/126] Add tests --- .../Resources/mania-skin-duplicate.ini | 9 ++ .../Resources/mania-skin-extra-data.ini | 4 + .../Resources/mania-skin-multiple.ini | 9 ++ .../Resources/mania-skin-single.ini | 4 + .../Skins/LegacyManiaSkinDecoderTest.cs | 87 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 osu.Game.Tests/Resources/mania-skin-duplicate.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-extra-data.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-multiple.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-single.ini create mode 100644 osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs diff --git a/osu.Game.Tests/Resources/mania-skin-duplicate.ini b/osu.Game.Tests/Resources/mania-skin-duplicate.ini new file mode 100644 index 0000000000..2f4fa92c52 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-duplicate.ini @@ -0,0 +1,9 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 + +[Mania] +Keys: 4 +ColumnWidth: 20,20,20,20 +HitPosition: 460 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-extra-data.ini b/osu.Game.Tests/Resources/mania-skin-extra-data.ini new file mode 100644 index 0000000000..e538b5335a --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-extra-data.ini @@ -0,0 +1,4 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10,10,10,10 +HitPosition: 470 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-multiple.ini b/osu.Game.Tests/Resources/mania-skin-multiple.ini new file mode 100644 index 0000000000..247c7738a0 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-multiple.ini @@ -0,0 +1,9 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 + +[Mania] +Keys: 2 +ColumnWidth: 20,20 +HitPosition: 460 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-single.ini b/osu.Game.Tests/Resources/mania-skin-single.ini new file mode 100644 index 0000000000..3ae38fd75e --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-single.ini @@ -0,0 +1,4 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs new file mode 100644 index 0000000000..736f97f39f --- /dev/null +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -0,0 +1,87 @@ +// 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.IO; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Skins +{ + [TestFixture] + public class LegacyManiaSkinDecoderTest + { + [Test] + public void TestParseSingleConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-single.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + + [Test] + public void TestParseMultipleConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-multiple.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(2)); + + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + + Assert.That(configs[1].Keys, Is.EqualTo(2)); + Assert.That(configs[1].ColumnWidth, Is.EquivalentTo(new float[] { 32, 32 })); + Assert.That(configs[1].HitPosition, Is.EqualTo(32)); + } + } + + [Test] + public void TestParseDuplicateConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-single.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + + [Test] + public void TestParseWithUnnecessaryExtraData() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-extra-data.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + } +} From 43367dbe35c313f70156d5ad31d9fe508c69ef01 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:49:50 +0000 Subject: [PATCH 106/126] Bump Microsoft.Build.Traversal from 2.0.24 to 2.0.32 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.24 to 2.0.32. - [Release notes](https://github.com/Microsoft/MSBuildSdks/releases) - [Changelog](https://github.com/microsoft/MSBuildSdks/blob/master/RELEASE.md) - [Commits](https://github.com/Microsoft/MSBuildSdks/compare/Microsoft.Build.Traversal.2.0.24...Microsoft.Build.Traversal.2.0.32) Signed-off-by: dependabot-preview[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6858d4044d..0223dc7330 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.24" + "Microsoft.Build.Traversal": "2.0.32" } } \ No newline at end of file From c4df49954f39d7c5d07352987c281ed1b5296e3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 18:35:01 +0900 Subject: [PATCH 107/126] Reword comment --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 2704680d54..b04d484195 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,8 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - // When the repeat is hit, the arrow should fade out on spot, - // it should no longer follow snaking + // When the repeat is hit, the arrow should fade out on spot rather than following the slider if (IsHit) return; bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; From 113660b6219b7729f28d35d0136a9d0634a59f61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 18:56:35 +0900 Subject: [PATCH 108/126] Merge if statements --- osu.Game/Screens/Ranking/ResultsScreen.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 1c08b763fe..cfba1e6e3e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -89,22 +89,19 @@ namespace osu.Game.Screens.Ranking } }; - if (player != null) + if (player != null && allowRetry) { - if (allowRetry) + buttons.Add(new RetryButton { Width = 300 }); + + AddInternal(new HotkeyRetryOverlay { - buttons.Add(new RetryButton { Width = 300 }); - - AddInternal(new HotkeyRetryOverlay + Action = () => { - Action = () => - { - if (!this.IsCurrentScreen()) return; + if (!this.IsCurrentScreen()) return; - player?.Restart(); - }, - }); - } + player?.Restart(); + }, + }); } } From 0d4830550e4f3cbf6c51599c6b90b6f36816b7ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 19:15:44 +0900 Subject: [PATCH 109/126] Fix tooltips not showing inside ManualInputManagerTestScenes --- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 0da3ae7f87..64f4d7b95b 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -26,16 +26,16 @@ namespace osu.Game.Tests.Visual protected OsuManualInputManagerTestScene() { + MenuCursorContainer cursorContainer; + base.Content.AddRange(new Drawable[] { InputManager = new ManualInputManager { UseParentInput = true, Child = new GlobalActionContainer(null) - { - RelativeSizeAxes = Axes.Both, - Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both } - }, + .WithChild((cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) + .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both })) }, new Container { From f96229c572e812cf2f7fc99b9dee05f62faf9a36 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:21:22 +0300 Subject: [PATCH 110/126] Add support for HitCircleOverlayAboveNumber legacy skin property --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 5 +++++ osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 38ba4c5974..0480449d05 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -80,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Skinning return tex ?? skin.GetTexture($"hitcircle{name}"); } + + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); } private void updateState(ValueChangedEvent state) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 5d99960f10..c6920bd03e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderPathRadius, AllowSliderBallTint, CursorExpand, - CursorRotate + CursorRotate, + HitCircleOverlayAboveNumber } } From 9890544b3692df822dcc97b25fdfad7cd800b0e5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:42:18 +0300 Subject: [PATCH 111/126] Move implementation to better place --- .../Skinning/LegacyMainCirclePiece.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0480449d05..e7486ef9b0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); + state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); @@ -80,11 +85,6 @@ namespace osu.Game.Rulesets.Osu.Skinning return tex ?? skin.GetTexture($"hitcircle{name}"); } - - bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; - - if (!overlayAboveNumber) - ChangeInternalChildDepth(hitCircleText, -float.MaxValue); } private void updateState(ValueChangedEvent state) From 655fab6a976007486b2de2d037a55f2fcb7c1d06 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:02:07 +0900 Subject: [PATCH 112/126] Add mania skinnable test helpers --- .../Skinning/ColumnTestContainer.cs | 38 +++++++++++ .../Skinning/ManiaHitObjectTestScene.cs | 67 +++++++++++++++++++ .../Skinning/ManiaSkinnableTestScene.cs | 58 ++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs new file mode 100644 index 0000000000..c807e98871 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -0,0 +1,38 @@ +// 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.Game.Rulesets.Mania.UI; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A container to be used in a to provide a resolvable dependency. + /// + public class ColumnTestContainer : Container + { + protected override Container Content => content; + + private readonly Container content; + + [Cached] + private readonly Column column; + + public ColumnTestContainer(int column, ManiaAction action) + { + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange + }; + + InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs new file mode 100644 index 0000000000..e65982b240 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -0,0 +1,67 @@ +// 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.Timing; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A test scene for a mania hitobject. + /// + public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene + { + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0.7f, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ColumnTestContainer(0, ManiaAction.Key1) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 80, + Child = new ScrollingHitObjectContainer + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(new StopwatchClock()), + }.With(c => + { + c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + }) + }, + new ColumnTestContainer(1, ManiaAction.Key2) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 80, + Child = new ScrollingHitObjectContainer + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(new StopwatchClock()), + }.With(c => + { + c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + }) + }, + } + }); + } + + protected abstract DrawableManiaHitObject CreateHitObject(); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs new file mode 100644 index 0000000000..41fb7c727e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -0,0 +1,58 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A test scene for skinnable mania components. + /// + public abstract class ManiaSkinnableTestScene : SkinnableTestScene + { + [Cached(Type = typeof(IScrollingInfo))] + private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + + protected ManiaSkinnableTestScene() + { + scrollingInfo.Direction.Value = ScrollingDirection.Down; + + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray.Opacity(0.2f), + Depth = 1 + }); + } + + [Test] + public void TestScrollingDown() + { + AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down); + } + + [Test] + public void TestScrollingUp() + { + AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up); + } + + private class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + + IBindable IScrollingInfo.Direction => Direction; + IBindable IScrollingInfo.TimeRange { get; } = new Bindable(1000); + IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm(); + } + } +} From bd87a4cde8212d97173be42b0bd0caa697f7a84d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:03:36 +0900 Subject: [PATCH 113/126] Re-namespace testscene --- .../{ => Skinning}/TestSceneDrawableJudgement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename osu.Game.Rulesets.Mania.Tests/{ => Skinning}/TestSceneDrawableJudgement.cs (96%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs rename to osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 692d079c16..a6bc64550f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -6,13 +6,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Game.Tests.Visual; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneDrawableJudgement : SkinnableTestScene { From 6ff2273b64bcb9600a71888673e78332050aa292 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:07:32 +0900 Subject: [PATCH 114/126] Make column + stage cached --- osu.Game.Rulesets.Mania/UI/Column.cs | 1 + osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 63c573d344..0eccd27944 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.UI { + [Cached] public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { public const float COLUMN_WIDTH = 80; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index bfe9f1085b..bd21663c4e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// + [Cached] public class ManiaStage : ScrollingPlayfield { public const float COLUMN_SPACING = 1; From c1789140d5aa964c5ee9525aef2f76ae8816ae9d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:17:32 +0900 Subject: [PATCH 115/126] Prepare skin transformer for mania components --- .../Skinning/ManiaLegacySkinTransformer.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index f3739ce7c2..444f153c66 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.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.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Audio.Sample; @@ -15,9 +16,19 @@ namespace osu.Game.Rulesets.Mania.Skinning { private readonly ISkin source; - public ManiaLegacySkinTransformer(ISkin source) + private Lazy isLegacySkin; + + public ManiaLegacySkinTransformer(ISkinSource source) { this.source = source; + + source.SourceChanged += sourceChanged; + sourceChanged(); + } + + private void sourceChanged() + { + isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); } public Drawable GetDrawableComponent(ISkinComponent component) @@ -26,6 +37,12 @@ namespace osu.Game.Rulesets.Mania.Skinning { case GameplaySkinComponent resultComponent: return getResult(resultComponent); + + case ManiaSkinComponent maniaComponent: + if (!isLegacySkin.Value) + return null; + + break; } return null; From c3cde7a16383909f3e1e42abc61fcc935568e1ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:18:38 +0900 Subject: [PATCH 116/126] Combine files --- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 4 ++++ osu.Game.Rulesets.Mania/ManiaSkinComponents.cs | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/ManiaSkinComponents.cs diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 69bd4b0ecf..5340ebc01f 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -16,4 +16,8 @@ namespace osu.Game.Rulesets.Mania protected override string ComponentName => Component.ToString().ToLower(); } + + public enum ManiaSkinComponents + { + } } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs deleted file mode 100644 index 6d85816e5a..0000000000 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs +++ /dev/null @@ -1,9 +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.Rulesets.Mania -{ - public enum ManiaSkinComponents - { - } -} From a8f7d7ea422ba684ed3d88e53c0907f9c188c69c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:21:10 +0900 Subject: [PATCH 117/126] Add structure for mania configuration lookups --- .../Skinning/ManiaLegacySkinTransformer.cs | 2 +- .../LegacyManiaSkinConfigurationLookup.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 444f153c66..ffc69fae49 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Skinning case GameplaySkinComponent resultComponent: return getResult(resultComponent); - case ManiaSkinComponent maniaComponent: + case ManiaSkinComponent _: if (!isLegacySkin.Value) return null; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs new file mode 100644 index 0000000000..bbdd445f66 --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.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. + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinConfigurationLookup + { + public readonly int Keys; + public readonly LegacyManiaSkinConfigurationLookups Lookup; + public readonly int? TargetColumn; + + public LegacyManiaSkinConfigurationLookup(int keys, LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) + { + Keys = keys; + Lookup = lookup; + TargetColumn = targetColumn; + } + } + + public enum LegacyManiaSkinConfigurationLookups + { + } +} From 522bbc1e9c4a84209531fb5538b5d9a2ad799445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Mar 2020 21:37:20 +0200 Subject: [PATCH 118/126] Support widescreen per-layer storyboard masking --- .../Drawables/DrawableStoryboard.cs | 2 +- .../Drawables/DrawableStoryboardLayer.cs | 33 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index bc6e01a729..c4d796e30b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables AddInternal(Content = new Container { - Size = new Vector2(640, 480), + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index def4eed2ca..2ada83c3b4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardLayer : LifetimeManagementContainer + public class DrawableStoryboardLayer : CompositeDrawable { public StoryboardLayer Layer { get; } public bool Enabled; @@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre; Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; + + InternalChild = new LayerElementContainer(layer); } - [BackgroundDependencyLoader] - private void load(CancellationToken? cancellationToken) + private class LayerElementContainer : LifetimeManagementContainer { - foreach (var element in Layer.Elements) - { - cancellationToken?.ThrowIfCancellationRequested(); + private readonly StoryboardLayer storyboardLayer; - if (element.IsDrawable) - AddInternal(element.CreateDrawable()); + public LayerElementContainer(StoryboardLayer layer) + { + storyboardLayer = layer; + + Width = 640; + Height = 480; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(CancellationToken? cancellationToken) + { + foreach (var element in storyboardLayer.Elements) + { + cancellationToken?.ThrowIfCancellationRequested(); + + if (element.IsDrawable) + AddInternal(element.CreateDrawable()); + } } } } From f6f5de7ad16fd416bdbb7ad11fa205be085f66c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 10:13:50 +0900 Subject: [PATCH 119/126] Allow LineBufferedReader to keep underlying stream open --- osu.Game/IO/LineBufferedReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/LineBufferedReader.cs b/osu.Game/IO/LineBufferedReader.cs index aab761afd8..018321dc9a 100644 --- a/osu.Game/IO/LineBufferedReader.cs +++ b/osu.Game/IO/LineBufferedReader.cs @@ -17,9 +17,9 @@ namespace osu.Game.IO private readonly StreamReader streamReader; private readonly Queue lineBuffer; - public LineBufferedReader(Stream stream) + public LineBufferedReader(Stream stream, bool leaveOpen = false) { - streamReader = new StreamReader(stream); + streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen); lineBuffer = new Queue(); } From 2b5e9885f6df85c45c0062fe24956af5e92b85c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 10:14:36 +0900 Subject: [PATCH 120/126] Implement mania skin reading functionality --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 ++ osu.Game/Skinning/LegacySkin.cs | 38 +++++++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index fa7e895a28..1c39fc41bb 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -9,6 +9,8 @@ namespace osu.Game.Skinning { public class LegacyBeatmapSkin : LegacySkin { + protected override bool AllowManiaSkin => false; + public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c71a321e74..fe190740b3 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -26,12 +26,16 @@ namespace osu.Game.Skinning [CanBeNull] protected IResourceStore Samples; + protected virtual bool AllowManiaSkin => true; + public new LegacySkinConfiguration Configuration { get => base.Configuration as LegacySkinConfiguration; set => base.Configuration = value; } + private readonly Dictionary maniaConfigurations = new Dictionary(); + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") { @@ -40,15 +44,26 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) : base(skin) { - Stream stream = storage?.GetStream(filename); - - if (stream != null) + using (var stream = storage?.GetStream(filename)) { - using (LineBufferedReader reader = new LineBufferedReader(stream)) - Configuration = new LegacySkinDecoder().Decode(reader); + if (stream != null) + { + using (LineBufferedReader reader = new LineBufferedReader(stream, true)) + Configuration = new LegacySkinDecoder().Decode(reader); + + stream.Seek(0, SeekOrigin.Begin); + + using (LineBufferedReader reader = new LineBufferedReader(stream)) + { + var maniaList = new LegacyManiaSkinDecoder().Decode(reader); + + foreach (var config in maniaList) + maniaConfigurations[config.Keys] = config; + } + } + else + Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION }; } - else - Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION }; if (storage != null) { @@ -105,6 +120,15 @@ namespace osu.Game.Skinning case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); + case LegacyManiaSkinConfigurationLookup legacy: + if (!AllowManiaSkin) + return null; + + if (!maniaConfigurations.TryGetValue(legacy.Keys, out _)) + maniaConfigurations[legacy.Keys] = new LegacyManiaSkinConfiguration(legacy.Keys); + + break; + default: // handles lookups like GlobalSkinConfiguration From 89d8bf9780cc18aa039e7ec21e54b4d650b36601 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 13:46:20 +0900 Subject: [PATCH 121/126] Fix catcher test resources being at wrong dpi definition --- ...t-catcher-fail.png => fruit-catcher-fail@2x.png} | Bin ...t-catcher-kiai.png => fruit-catcher-kiai@2x.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-fail.png => fruit-catcher-fail@2x.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-kiai.png => fruit-catcher-kiai@2x.png} (100%) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png From 75e43acb1abfc893394c929b708cb0fbbab1add9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:10:15 +0900 Subject: [PATCH 122/126] Add a legacy element to help with texture fallbacks --- .../Skinning/LegacyManiaColumnElement.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs new file mode 100644 index 0000000000..231a55a7e2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -0,0 +1,41 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + /// + /// A which is placed somewhere within a . + /// + public class LegacyManiaColumnElement : CompositeDrawable + { + [Resolved(CanBeNull = true)] + [CanBeNull] + protected ManiaStage Stage { get; private set; } + + [Resolved] + protected Column Column { get; private set; } + + /// + /// The column index to use for texture lookups, in the case of no user-provided configuration. + /// + protected int FallbackColumnIndex { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + if (Stage == null) + FallbackColumnIndex = Column.Index % 2 + 1; + else + { + int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1); + FallbackColumnIndex = dist % 2 + 1; + } + } + } +} From 8a998d600d13ae64d1e838deae92b7f432d30e56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 15:17:27 +0900 Subject: [PATCH 123/126] Fix relax mod pressing too many keys --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 6286c80d7c..9b0759d9d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods void handleHitCircle(DrawableHitCircle circle) { - if (!circle.IsHovered) + if (!circle.HitArea.IsHovered) return; Debug.Assert(circle.HitObject.HitWindows != null); From 9602ab17b0c32fd6f4c30ba0e72f373f8c88bb92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 17:13:42 +0900 Subject: [PATCH 124/126] Fix replay imports failing for certain mod combinations --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c356dd246d..a4a560c8e4 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy { var score = new Score { - ScoreInfo = new ScoreInfo(), Replay = new Replay() }; + WorkingBeatmap workingBeatmap; + using (SerializationReader sr = new SerializationReader(stream)) { currentRuleset = GetRuleset(sr.ReadByte()); @@ -41,7 +42,7 @@ namespace osu.Game.Scoring.Legacy var version = sr.ReadInt32(); - var workingBeatmap = GetBeatmap(sr.ReadString()); + workingBeatmap = GetBeatmap(sr.ReadString()); if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); @@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy CalculateAccuracy(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. + score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo; + return score; } From b7d73f96eaf24ab49f4d67f0903fd768f1dcf577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 18:33:00 +0900 Subject: [PATCH 125/126] Fix osu!catch catcher hit area being too large --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8fa9c61b6f..13935e036b 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherAnimationState CurrentState { get; private set; } + /// + /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. + /// + private const float allowed_catch_range = 0.8f; + /// /// Width of the area that can be used to attempt catches during gameplay. /// - internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; protected bool Dashing { From 03b90fe2dbed5a7851e4a689e42edcc86968970c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 19:01:49 +0900 Subject: [PATCH 126/126] Remove local application of same margin in CatchDifficultyCalculator --- .../Difficulty/CatchDifficultyCalculator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5880a227c2..4d9dbbbc5f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { halfCatcherWidth = catcher.CatchWidth * 0.5f; - halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } return new Skill[] {