From e8d4bc4497c3fe3f7f2606db471a48fce87393cb Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 21:04:26 -0400 Subject: [PATCH 01/51] Allow NaN during beatmap parsing if desired --- osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs | 7 ++++++- osu.Game/Beatmaps/Formats/Parsing.cs | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs index f3673b0e7f..339063633a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs @@ -14,7 +14,12 @@ namespace osu.Game.Tests.Beatmaps.Formats public class ParsingTest { [Test] - public void TestNaNHandling() => allThrow("NaN"); + public void TestNaNHandling() + { + allThrow("NaN"); + Assert.That(Parsing.ParseFloat("NaN", allowNaN: true), Is.NaN); + Assert.That(Parsing.ParseDouble("NaN", allowNaN: true), Is.NaN); + } [Test] public void TestBadStringHandling() => allThrow("Random string 123"); diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs index ce04f27020..9b0d200077 100644 --- a/osu.Game/Beatmaps/Formats/Parsing.cs +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -17,26 +17,26 @@ namespace osu.Game.Beatmaps.Formats public const double MAX_PARSE_VALUE = int.MaxValue; - public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE) + public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE, bool allowNaN = false) { float output = float.Parse(input, CultureInfo.InvariantCulture); if (output < -parseLimit) throw new OverflowException("Value is too low"); if (output > parseLimit) throw new OverflowException("Value is too high"); - if (float.IsNaN(output)) throw new FormatException("Not a number"); + if (!allowNaN && float.IsNaN(output)) throw new FormatException("Not a number"); return output; } - public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE) + public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE, bool allowNaN = false) { double output = double.Parse(input, CultureInfo.InvariantCulture); if (output < -parseLimit) throw new OverflowException("Value is too low"); if (output > parseLimit) throw new OverflowException("Value is too high"); - if (double.IsNaN(output)) throw new FormatException("Not a number"); + if (!allowNaN && double.IsNaN(output)) throw new FormatException("Not a number"); return output; } From 9f08c474caf57d8a84a510653c72a7f01ccf2b8d Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 21:44:25 -0400 Subject: [PATCH 02/51] Treat NaN slider velocity timing points as 1.0x but without slider ticks --- .../Objects/JuiceStream.cs | 5 ++++- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Mods/OsuModStrictTracking.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++++- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Beatmaps/SliderEventGenerationTest.cs | 17 ++++++++++----- .../ControlPoints/DifficultyControlPoint.cs | 15 ++++++++++--- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++++++- .../Rulesets/Objects/SliderEventGenerator.cs | 21 +++++++++++-------- 9 files changed, 57 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 311e15116e..a9c7d938d2 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -40,6 +40,9 @@ namespace osu.Game.Rulesets.Catch.Objects [JsonIgnore] public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; + [JsonIgnore] + public bool GenerateTicks => DifficultyControlPoint.GenerateTicks; + /// /// The length of one span of this . /// @@ -64,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 22fab15c1b..4ec039c405 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0) + if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) return; for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 67b19124e1..eb59122686 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a5468ff613..31117c0836 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -142,6 +142,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; + /// + /// Whether this should generate s. + /// + public bool GenerateTicks { get; private set; } + /// /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. @@ -170,13 +175,14 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; + GenerateTicks = DifficultyControlPoint.GenerateTicks; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index e1619e2857..09e465c37b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0) + if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) return; bool first = true; diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index d30ab3dea1..077398f015 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestLegacyLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, true).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Time, Is.EqualTo(900)); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, true).ToArray(); Assert.Multiple(() => { @@ -114,5 +114,12 @@ namespace osu.Game.Tests.Beatmaps } }); } + + [Test] + public void TestNoTickGeneration() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, false).ToArray(); + Assert.That(events.Any(e => e.Type == SliderEventType.Tick), Is.False); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index c199d1da59..dfa3552469 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -40,13 +40,21 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; set; } = true; + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty - && SliderVelocity == existingDifficulty.SliderVelocity; + && SliderVelocity == existingDifficulty.SliderVelocity + && GenerateTicks == existingDifficulty.GenerateTicks; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; + GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -57,8 +65,9 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) - && SliderVelocity == other.SliderVelocity; + && SliderVelocity == other.SliderVelocity + && GenerateTicks == other.GenerateTicks; - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 89d3465ab6..3d3661745a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -373,7 +373,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = line.Split(','); double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); - double beatLength = Parsing.ParseDouble(split[1].Trim()); + double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true); + + // If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false. double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; TimeSignature timeSignature = TimeSignature.SimpleQuadruple; @@ -412,6 +414,9 @@ namespace osu.Game.Beatmaps.Formats if (timingChange) { + if (double.IsNaN(beatLength)) + throw new InvalidDataException("Beat length cannot be NaN in a timing control point"); + var controlPoint = CreateTimingControlPoint(); controlPoint.BeatLength = beatLength; @@ -425,6 +430,7 @@ namespace osu.Game.Beatmaps.Formats #pragma warning restore 618 { SliderVelocity = speedMultiplier, + GenerateTicks = !double.IsNaN(beatLength), }, timingChange); var effectPoint = new EffectControlPoint diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d32a7cb16d..b77d15d565 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects { // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, CancellationToken cancellationToken = default) + double? legacyLastTickOffset, bool shouldGenerateTicks, CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -41,16 +41,19 @@ namespace osu.Game.Rulesets.Objects double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; - var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - - if (reversed) + if (shouldGenerateTicks) { - // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets - ticks = ticks.Reverse(); - } + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - foreach (var e in ticks) - yield return e; + if (reversed) + { + // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets + ticks = ticks.Reverse(); + } + + foreach (var e in ticks) + yield return e; + } if (span < spanCount - 1) { From 8f708c1dcf30e7300b8f0c71ab48989a234f5cda Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 22:43:44 -0400 Subject: [PATCH 03/51] Turn GenerateTicks into a bindable to pass code quality tests --- .../ControlPoints/DifficultyControlPoint.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index dfa3552469..ca49d316e0 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -16,6 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { SliderVelocityBindable = { Disabled = true }, + GenerateTicksBindable = { Disabled = true }, }; /// @@ -29,6 +30,12 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public readonly BindableBool GenerateTicksBindable = new BindableBool(true); + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -44,7 +51,11 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether or not slider ticks should be generated at this control point. /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). /// - public bool GenerateTicks { get; set; } = true; + public bool GenerateTicks + { + get => GenerateTicksBindable.Value; + set => GenerateTicksBindable.Value = value; + } public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty From a81672f3dc9e885651f78852675fadb76940ace2 Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 23:31:24 -0400 Subject: [PATCH 04/51] Use an infinite tick distance instead of directly disabling tick generation for SliderEventGenerator --- .../Objects/JuiceStream.cs | 5 +---- .../Mods/OsuModStrictTracking.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 10 ++------- .../Beatmaps/SliderEventGenerationTest.cs | 17 +++++---------- .../Rulesets/Objects/SliderEventGenerator.cs | 21 ++++++++----------- 5 files changed, 18 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a9c7d938d2..311e15116e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -40,9 +40,6 @@ namespace osu.Game.Rulesets.Catch.Objects [JsonIgnore] public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; - [JsonIgnore] - public bool GenerateTicks => DifficultyControlPoint.GenerateTicks; - /// /// The length of one span of this . /// @@ -67,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index eb59122686..67b19124e1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 31117c0836..0689c49085 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -142,11 +142,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; - /// - /// Whether this should generate s. - /// - public bool GenerateTicks { get; private set; } - /// /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. @@ -174,15 +169,14 @@ namespace osu.Game.Rulesets.Osu.Objects double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; - GenerateTicks = DifficultyControlPoint.GenerateTicks; + TickDistance = DifficultyControlPoint.GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index 077398f015..d30ab3dea1 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestLegacyLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Time, Is.EqualTo(900)); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); Assert.Multiple(() => { @@ -114,12 +114,5 @@ namespace osu.Game.Tests.Beatmaps } }); } - - [Test] - public void TestNoTickGeneration() - { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, false).ToArray(); - Assert.That(events.Any(e => e.Type == SliderEventType.Tick), Is.False); - } } } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index b77d15d565..d32a7cb16d 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects { // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, bool shouldGenerateTicks, CancellationToken cancellationToken = default) + double? legacyLastTickOffset, CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -41,20 +41,17 @@ namespace osu.Game.Rulesets.Objects double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; - if (shouldGenerateTicks) + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); + + if (reversed) { - var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - - if (reversed) - { - // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets - ticks = ticks.Reverse(); - } - - foreach (var e in ticks) - yield return e; + // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets + ticks = ticks.Reverse(); } + foreach (var e in ticks) + yield return e; + if (span < spanCount - 1) { yield return new SliderEventDescriptor From fbe8de2757ca2a752163bd6a2d61a4d788b55301 Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 00:57:25 -0400 Subject: [PATCH 05/51] Disable the GetHashCode warning instead of using bindables --- .../ControlPoints/DifficultyControlPoint.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index ca49d316e0..ddb2b1e95c 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -16,7 +16,6 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { SliderVelocityBindable = { Disabled = true }, - GenerateTicksBindable = { Disabled = true }, }; /// @@ -30,12 +29,6 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; - /// - /// Whether or not slider ticks should be generated at this control point. - /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). - /// - public readonly BindableBool GenerateTicksBindable = new BindableBool(true); - public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -51,11 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether or not slider ticks should be generated at this control point. /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). /// - public bool GenerateTicks - { - get => GenerateTicksBindable.Value; - set => GenerateTicksBindable.Value = value; - } + public bool GenerateTicks { get; set; } = true; public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty @@ -79,6 +68,7 @@ namespace osu.Game.Beatmaps.ControlPoints && SliderVelocity == other.SliderVelocity && GenerateTicks == other.GenerateTicks; + // ReSharper disable once NonReadonlyMemberInGetHashCode public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); } } From c1ced85b5e4fd5afe86e064774fda164be0ce88c Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:07:18 -0400 Subject: [PATCH 06/51] Move GenerateTicks to LegacyDifficultyControlPoint and remove support for NaN slider velocity support for other rulesets (at least for now) --- .../Objects/JuiceStream.cs | 6 +++++- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 10 +++++++++- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../ControlPoints/DifficultyControlPoint.cs | 16 +++------------ .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 - osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 20 ++++++++++++++++--- 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 311e15116e..2a5564297c 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -50,9 +51,12 @@ namespace osu.Game.Rulesets.Catch.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); +#pragma warning disable 618 + bool generateTicks = (DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint)?.GenerateTicks ?? true; +#pragma warning restore 618 velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; + tickDistanceFactor = generateTicks ? (base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 4ec039c405..22fab15c1b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) + if (tickSpacing == 0) return; for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 0689c49085..df1252c060 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -14,6 +14,8 @@ using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -166,10 +168,16 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + var legacyInfo = controlPointInfo as LegacyControlPointInfo; +#pragma warning disable 618 + var legacyDifficultyPoint = legacyInfo?.DifficultyPointAt(StartTime) as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; +#pragma warning restore 618 + double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; + bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true; Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = DifficultyControlPoint.GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; + TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 09e465c37b..e1619e2857 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) + if (tickSpacing == 0) return; bool first = true; diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index ddb2b1e95c..c199d1da59 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -40,21 +40,13 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } - /// - /// Whether or not slider ticks should be generated at this control point. - /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). - /// - public bool GenerateTicks { get; set; } = true; - public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty - && SliderVelocity == existingDifficulty.SliderVelocity - && GenerateTicks == existingDifficulty.GenerateTicks; + && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; - GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -65,10 +57,8 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) - && SliderVelocity == other.SliderVelocity - && GenerateTicks == other.GenerateTicks; + && SliderVelocity == other.SliderVelocity; - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3d3661745a..f064d89e19 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -430,7 +430,6 @@ namespace osu.Game.Beatmaps.Formats #pragma warning restore 618 { SliderVelocity = speedMultiplier, - GenerateTicks = !double.IsNaN(beatLength), }, timingChange); var effectPoint = new EffectControlPoint diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 52e760a068..c3fd16e86f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -168,11 +168,18 @@ namespace osu.Game.Beatmaps.Formats /// public double BpmMultiplier { get; private set; } + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; private set; } = true; + public LegacyDifficultyControlPoint(double beatLength) : this() { // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; + GenerateTicks = !double.IsNaN(beatLength); } public LegacyDifficultyControlPoint() @@ -180,11 +187,17 @@ namespace osu.Game.Beatmaps.Formats SliderVelocityBindable.Precision = double.Epsilon; } + public override bool IsRedundant(ControlPoint? existing) + => existing is LegacyDifficultyControlPoint existingLegacyDifficulty + && base.IsRedundant(existing) + && GenerateTicks == existingLegacyDifficulty.GenerateTicks; + public override void CopyFrom(ControlPoint other) { base.CopyFrom(other); BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier; + GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks; } public override bool Equals(ControlPoint? other) @@ -193,10 +206,11 @@ namespace osu.Game.Beatmaps.Formats public bool Equals(LegacyDifficultyControlPoint? other) => base.Equals(other) - && BpmMultiplier == other.BpmMultiplier; + && BpmMultiplier == other.BpmMultiplier + && GenerateTicks == other.GenerateTicks; - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier); + // ReSharper disable twice NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks); } internal class LegacySampleControlPoint : SampleControlPoint, IEquatable From ada61e6fe7121960b2f531ff08bb29379ade63b1 Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:10:26 -0400 Subject: [PATCH 07/51] Forgot to undo broken changes in JuiceStream --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 2a5564297c..311e15116e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -11,7 +11,6 @@ using Newtonsoft.Json; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -51,12 +50,9 @@ namespace osu.Game.Rulesets.Catch.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); -#pragma warning disable 618 - bool generateTicks = (DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint)?.GenerateTicks ?? true; -#pragma warning restore 618 velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = generateTicks ? (base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate) : double.PositiveInfinity; + tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From ec9daec93bcea47e7983564db3fb75b1c531733a Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:18:40 -0400 Subject: [PATCH 08/51] Remove unnecessary workaround --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index df1252c060..a7495a2809 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -167,10 +167,8 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - - var legacyInfo = controlPointInfo as LegacyControlPointInfo; #pragma warning disable 618 - var legacyDifficultyPoint = legacyInfo?.DifficultyPointAt(StartTime) as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; + var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; #pragma warning restore 618 double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; From c9f364d6a086c7582040c9b66a94eca09e8ce25e Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:10:19 -0400 Subject: [PATCH 09/51] Document why beatLength can be NaN --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f064d89e19..75500fbc4e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -373,6 +373,8 @@ namespace osu.Game.Beatmaps.Formats string[] split = line.Split(','); double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); + + // beatLength is allowed to be NaN to handle an edge case in which some beatmaps use NaN slider velocity to disable slider tick generation (see LegacyDifficultyControlPoint). double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true); // If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false. From c7d4c739aa067825e89a97f11a8d3ae3e4965602 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:53:55 -0400 Subject: [PATCH 10/51] Add a basic NaN control point test for LegacyBeatmapDecoder --- .../Formats/LegacyBeatmapDecoderTest.cs | 27 +++++++++++++++++++ .../Resources/nan-control-points.osu | 15 +++++++++++ 2 files changed, 42 insertions(+) create mode 100644 osu.Game.Tests/Resources/nan-control-points.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 9acd3a6cab..7bd32eb5bd 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -919,5 +919,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero)); } } + + [Test] + public void TestNaNControlPoints() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("nan-control-points.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; + + Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); + + Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500)); + + Assert.That(controlPoints.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); + +#pragma warning disable 618 + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(1000)).GenerateTicks, Is.True); + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False); + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True); +#pragma warning restore 618 + } + } } } diff --git a/osu.Game.Tests/Resources/nan-control-points.osu b/osu.Game.Tests/Resources/nan-control-points.osu new file mode 100644 index 0000000000..dcaa705116 --- /dev/null +++ b/osu.Game.Tests/Resources/nan-control-points.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[TimingPoints] + +// NaN bpm (should be rejected) +0,NaN,4,2,0,100,1,0 + +// 120 bpm +1000,500,4,2,0,100,1,0 + +// NaN slider velocity +2000,NaN,4,3,0,100,0,1 + +// 1.0x slider velocity +3000,-100,4,3,0,100,0,1 \ No newline at end of file From 9c6968e96dfb322cbcc08a17dcd5ed0dbaf6ea00 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:54:40 -0400 Subject: [PATCH 11/51] Remove unused import in Slider --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a7495a2809..e3c1b1e168 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -15,7 +15,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; From adea29c10604fb50a91d680e751f744c84cfe559 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 03:37:33 -0400 Subject: [PATCH 12/51] Fix test failures --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 +--- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 7bd32eb5bd..9fc1eb7650 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -931,16 +931,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(1)); - Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); + Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(2)); Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500)); - Assert.That(controlPoints.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); #pragma warning disable 618 - Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(1000)).GenerateTicks, Is.True); Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False); Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True); #pragma warning restore 618 diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index c3fd16e86f..3d65ab8e0f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -188,9 +188,8 @@ namespace osu.Game.Beatmaps.Formats } public override bool IsRedundant(ControlPoint? existing) - => existing is LegacyDifficultyControlPoint existingLegacyDifficulty - && base.IsRedundant(existing) - && GenerateTicks == existingLegacyDifficulty.GenerateTicks; + => base.IsRedundant(existing) + && GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true); public override void CopyFrom(ControlPoint other) { From 3ff2058975a72aa69b2d2a7d39fa0ec4be2fe7d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 29 Aug 2022 09:23:53 +0300 Subject: [PATCH 13/51] Fix back-to-front fallback comparison in `HitObjectOrderedSelectionContainer` --- .../Compose/Components/HitObjectOrderedSelectionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 06af232111..e3934025e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (result != 0) return result; } - return CompareReverseChildID(y, x); + return CompareReverseChildID(x, y); } protected override void Dispose(bool isDisposing) From ad5ef529227d3687e8dd94573ea62b9e45b24f5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 19:02:01 +0900 Subject: [PATCH 14/51] Add test coverage of resuming after pause not skipping forward in time --- .../Visual/Gameplay/TestScenePause.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index cad8c62233..61b59747ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -90,6 +90,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("player not playing", () => !Player.LocalUserPlaying.Value); resumeAndConfirm(); + + AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime)); + AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } @@ -378,7 +381,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); private void confirmClockRunning(bool isRunning) => - AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning); + AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => + { + bool completed = Player.GameplayClockContainer.IsRunning == isRunning; + + if (completed) + { + } + + return completed; + }); protected override bool AllowFail => true; @@ -386,6 +398,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected class PausePlayer : TestPlayer { + public double LastPauseTime { get; private set; } + public double LastResumeTime { get; private set; } + public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; @@ -399,6 +414,23 @@ namespace osu.Game.Tests.Visual.Gameplay base.OnEntering(e); GameplayClockContainer.Stop(); } + + private bool? isRunning; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (GameplayClockContainer.IsRunning != isRunning) + { + isRunning = GameplayClockContainer.IsRunning; + + if (isRunning.Value) + LastResumeTime = GameplayClockContainer.CurrentTime; + else + LastPauseTime = GameplayClockContainer.CurrentTime; + } + } } } } From 75531d2d62d3f7e9f7c2fd04dd5ba53d884bcefc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 19:51:16 +0900 Subject: [PATCH 15/51] Fix gameplay skipping forward during resume operation --- .../Screens/Play/GameplayClockContainer.cs | 17 +++++-- .../Play/MasterGameplayClockContainer.cs | 45 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 1ae393d06a..ff82fb96ec 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -88,9 +88,7 @@ namespace osu.Game.Screens.Play ensureSourceClockSet(); - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + PrepareStart(); // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), @@ -111,11 +109,22 @@ namespace osu.Game.Screens.Play }); } + /// + /// When is called, this will be run to give an opportunity to prepare the clock at the correct + /// start location. + /// + protected virtual void PrepareStart() + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); + } + /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. - public void Seek(double time) + public virtual void Seek(double time) { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 238817ad05..f0f5daf64d 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -45,6 +46,17 @@ namespace osu.Game.Screens.Play private readonly List> nonGameplayAdjustments = new List>(); + /// + /// Stores the time at which the last call was triggered. + /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp. + /// + /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled + /// to avoid fails occurring after the pause screen has been shown. + /// + /// In the future I want to change this. + /// + private double? actualStopTime; + public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); /// @@ -86,10 +98,12 @@ namespace osu.Game.Screens.Play protected override void StopGameplayClock() { + actualStopTime = GameplayClock.CurrentTime; + if (IsLoaded) { // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 2000, Easing.Out).OnComplete(_ => { if (IsPaused.Value) base.StopGameplayClock(); @@ -108,6 +122,25 @@ namespace osu.Game.Screens.Play } } + public override void Seek(double time) + { + // Safety in case the clock is seeked while stopped. + actualStopTime = null; + + base.Seek(time); + } + + protected override void PrepareStart() + { + if (actualStopTime != null) + { + Seek(actualStopTime.Value); + actualStopTime = null; + } + else + base.PrepareStart(); + } + protected override void StartGameplayClock() { addSourceClockAdjustments(); @@ -116,7 +149,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 2000, Easing.In); } else { @@ -158,6 +191,14 @@ namespace osu.Game.Screens.Play private bool speedAdjustmentsApplied; + protected override void Update() + { + base.Update(); + + if (GameplayClock.ExternalPauseFrequencyAdjust.Value < 1) + Logger.Log($"{GameplayClock.CurrentTime}"); + } + private void addSourceClockAdjustments() { if (speedAdjustmentsApplied) From 1bff540381f8c7aaea4d4be68fab8bc69ffafa03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 22:04:37 +0900 Subject: [PATCH 16/51] Remove debug changes --- .../Screens/Play/MasterGameplayClockContainer.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index f0f5daf64d..2f1ffa126f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -103,7 +102,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 2000, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => { if (IsPaused.Value) base.StopGameplayClock(); @@ -149,7 +148,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 2000, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); } else { @@ -191,14 +190,6 @@ namespace osu.Game.Screens.Play private bool speedAdjustmentsApplied; - protected override void Update() - { - base.Update(); - - if (GameplayClock.ExternalPauseFrequencyAdjust.Value < 1) - Logger.Log($"{GameplayClock.CurrentTime}"); - } - private void addSourceClockAdjustments() { if (speedAdjustmentsApplied) From 27ad224f13591fb35309d4612b074dd47b22f6cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 01:21:44 +0900 Subject: [PATCH 17/51] Remove probably unnecessary `Seek` on start --- osu.Game/Screens/Play/GameplayClockContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ff82fb96ec..6de88d7ad0 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -115,9 +115,6 @@ namespace osu.Game.Screens.Play /// protected virtual void PrepareStart() { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); } /// From 062a6fcc181da7e3ca597a9d6201491b16fc7898 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 01:21:53 +0900 Subject: [PATCH 18/51] Fix failing large offset test If we are going to continue to let the underlying clock process frames, there needs to be a bit of lenience to allow the backwards seek on resume (to play back over the freq ramp period). The test is meant to be ensuring we don't skip the full offset amount, so div10 seems pretty safe. --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 61b59747ea..a6abdd7ee1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay Player.OnUpdate += _ => { double currentTime = Player.GameplayClockContainer.CurrentTime; - alwaysGoingForward &= currentTime >= lastTime; + alwaysGoingForward &= currentTime >= lastTime - 500; lastTime = currentTime; }; }); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); - AddAssert("time didn't go backwards", () => alwaysGoingForward); + AddAssert("time didn't go too far backwards", () => alwaysGoingForward); AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0)); } From 1484ae19f0a7a3f4a069bf8628dd5c8805d923db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:33:08 +0900 Subject: [PATCH 19/51] Initial design update pass --- osu.Game/Overlays/NotificationOverlay.cs | 6 +- .../Overlays/Notifications/Notification.cs | 147 ++++++++++-------- .../Notifications/ProgressNotification.cs | 3 +- .../Notifications/SimpleNotification.cs | 32 ++-- 4 files changed, 100 insertions(+), 88 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index cbcc7b6886..bd483e073c 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Threading; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; @@ -35,6 +34,9 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } = null!; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly IBindable firstRunSetupVisibility = new Bindable(); [BackgroundDependencyLoader] @@ -49,7 +51,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.05f), + Colour = colourProvider.Background4, }, new OsuScrollContainer { diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index fbb906e637..5cd0db628d 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -46,8 +45,9 @@ namespace osu.Game.Overlays.Notifications public virtual string PopInSampleName => "UI/notification-pop-in"; protected NotificationLight Light; - private readonly CloseButton closeButton; + protected Container IconContent; + private readonly Container content; protected override Container Content => content; @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Notifications RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - AddRangeInternal(new Drawable[] + InternalChildren = new Drawable[] { Light = new NotificationLight { @@ -79,64 +79,68 @@ namespace osu.Game.Overlays.Notifications AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new Container + new GridContainer { RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(5), AutoSizeAxes = Axes.Y, - Children = new Drawable[] + RowDimensions = new[] { - IconContent = new Container - { - Size = new Vector2(40), - Masking = true, - CornerRadius = 5, - }, - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Left = 45, - Right = 30 - }, - } - } - }, - closeButton = new CloseButton - { - Alpha = 0, - Action = Close, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding - { - Right = 5 + new Dimension(GridSizeMode.AutoSize, minSize: 60) }, - } + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + IconContent = new Container + { + Width = 40, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + }, + new CloseButton + { + Action = Close, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + }, + }, } } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + NotificationContent.Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + Depth = float.MaxValue }); } - protected override bool OnHover(HoverEvent e) - { - closeButton.FadeIn(75); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - closeButton.FadeOut(75); - base.OnHoverLost(e); - } - protected override bool OnClick(ClickEvent e) { if (Activated?.Invoke() ?? true) @@ -150,6 +154,7 @@ namespace osu.Game.Overlays.Notifications base.LoadComplete(); this.FadeInFromZero(200); + NotificationContent.MoveToX(DrawSize.X); NotificationContent.MoveToX(0, 500, Easing.OutQuint); } @@ -169,40 +174,48 @@ namespace osu.Game.Overlays.Notifications private class CloseButton : OsuClickableContainer { - private Color4 hoverColour; + private SpriteIcon icon = null!; + private Box background = null!; - public CloseButton() + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() { - Colour = OsuColour.Gray(0.2f); - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; + Width = 24; - Children = new[] + Children = new Drawable[] { - new SpriteIcon + background = new Box + { + Colour = colourProvider.Background4, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + icon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.TimesCircle, - Size = new Vector2(20), + Icon = FontAwesome.Solid.Check, + Size = new Vector2(12), + Colour = colourProvider.Foreground1, } }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - hoverColour = colours.Yellow; - } - protected override bool OnHover(HoverEvent e) { - this.FadeColour(hoverColour, 200); + background.FadeIn(200); + icon.FadeColour(colourProvider.Content1, 200); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeColour(OsuColour.Gray(0.2f), 200); + background.FadeOut(200); + icon.FadeColour(colourProvider.Foreground1, 200); base.OnHoverLost(e); } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 15346930a3..6390d9a54f 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.Notifications set { progress = value; - Scheduler.AddOnce(updateProgress, progress); + Scheduler.AddOnce(p => progressBar.Progress = p, progress); } } @@ -174,7 +174,6 @@ namespace osu.Game.Overlays.Notifications { Content.Add(textDrawable = new OsuTextFlowContainer { - Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }); diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index b9a1cc6d90..ae4d183258 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -24,7 +23,8 @@ namespace osu.Game.Overlays.Notifications set { text = value; - textDrawable.Text = text; + if (textDrawable != null) + textDrawable.Text = text; } } @@ -36,48 +36,46 @@ namespace osu.Game.Overlays.Notifications set { icon = value; - iconDrawable.Icon = icon; + if (iconDrawable != null) + iconDrawable.Icon = icon; } } - private readonly TextFlowContainer textDrawable; - private readonly SpriteIcon iconDrawable; + protected Box IconBackground = null!; - protected Box IconBackground; + private TextFlowContainer? textDrawable; - public SimpleNotification() + private SpriteIcon? iconDrawable; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + Light.Colour = colours.Green; + IconContent.AddRange(new Drawable[] { IconBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.6f)) + Colour = colourProvider.Background5, }, iconDrawable = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = icon, - Size = new Vector2(20), + Size = new Vector2(16), } }); - Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14)) + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { - Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Text = text }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Light.Colour = colours.Green; - } - public override bool Read { get => base.Read; From 0f203531d938fc39e0b8c99274cf35ccf844b5a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:37:43 +0900 Subject: [PATCH 20/51] Allow customising the "close" button icon --- osu.Game/Overlays/Notifications/Notification.cs | 13 +++++++++++-- .../Overlays/Notifications/ProgressNotification.cs | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 5cd0db628d..2b57423305 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -56,6 +56,8 @@ namespace osu.Game.Overlays.Notifications public virtual bool Read { get; set; } + protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check; + protected Notification() { RelativeSizeAxes = Axes.X; @@ -116,7 +118,7 @@ namespace osu.Game.Overlays.Notifications }, } }, - new CloseButton + new CloseButton(CloseButtonIcon) { Action = Close, Anchor = Anchor.TopRight, @@ -177,9 +179,16 @@ namespace osu.Game.Overlays.Notifications private SpriteIcon icon = null!; private Box background = null!; + private readonly IconUsage iconUsage; + [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public CloseButton(IconUsage iconUsage) + { + this.iconUsage = iconUsage; + } + [BackgroundDependencyLoader] private void load() { @@ -198,7 +207,7 @@ namespace osu.Game.Overlays.Notifications { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Check, + Icon = iconUsage, Size = new Vector2(12), Colour = colourProvider.Foreground1, } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 6390d9a54f..594537bce7 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Notifications } } - private void updateProgress(float progress) => progressBar.Progress = progress; + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; protected override void LoadComplete() { From 09aa3e065d790ad2f1cb44024dc7b06bd9cc8b73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:40:35 +0900 Subject: [PATCH 21/51] Move colouring to full icon content rather than background --- osu.Desktop/Security/ElevatedPrivilegesChecker.cs | 2 +- osu.Game/Database/TooManyDownloadsNotification.cs | 2 +- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- .../Notifications/ProgressCompletionNotification.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 11 +++++------ osu.Game/Overlays/Notifications/SimpleNotification.cs | 4 +--- osu.Game/Screens/Play/PlayerLoader.cs | 4 ++-- osu.Game/Updater/UpdateManager.cs | 2 +- 8 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 9959b24b35..cc4337fb02 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -76,7 +76,7 @@ namespace osu.Desktop.Security private void load(OsuColour colours) { Icon = FontAwesome.Solid.ShieldAlt; - IconBackground.Colour = colours.YellowDark; + IconContent.Colour = colours.YellowDark; } } } diff --git a/osu.Game/Database/TooManyDownloadsNotification.cs b/osu.Game/Database/TooManyDownloadsNotification.cs index aa88fed43c..14012e1d34 100644 --- a/osu.Game/Database/TooManyDownloadsNotification.cs +++ b/osu.Game/Database/TooManyDownloadsNotification.cs @@ -20,7 +20,7 @@ namespace osu.Game.Database [BackgroundDependencyLoader] private void load(OsuColour colours) { - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; } } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 3fc6a008e8..22c2b4690e 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -174,7 +174,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) { - IconBackground.Colour = colours.PurpleDark; + IconContent.Colour = colours.PurpleDark; Activated = delegate { diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index cb9d54c14c..49d558285c 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Notifications [BackgroundDependencyLoader] private void load(OsuColour colours) { - IconBackground.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight); + IconContent.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight); } } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 594537bce7..c0342f1c2a 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Notifications Light.Pulsate = false; progressBar.Active = false; - iconBackground.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Show(); break; @@ -112,14 +112,14 @@ namespace osu.Game.Overlays.Notifications Light.Pulsate = true; progressBar.Active = true; - iconBackground.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Show(); break; case ProgressNotificationState.Cancelled: cancellationTokenSource.Cancel(); - iconBackground.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Hide(); var icon = new SpriteIcon @@ -168,7 +168,6 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourCancelled; - private Box iconBackground = null!; private LoadingSpinner loadingSpinner = null!; private readonly TextFlowContainer textDrawable; @@ -206,10 +205,10 @@ namespace osu.Game.Overlays.Notifications IconContent.AddRange(new Drawable[] { - iconBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.White, + Colour = colourProvider.Background5, }, loadingSpinner = new LoadingSpinner { diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index ae4d183258..1dba60fb5f 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -41,8 +41,6 @@ namespace osu.Game.Overlays.Notifications } } - protected Box IconBackground = null!; - private TextFlowContainer? textDrawable; private SpriteIcon? iconDrawable; @@ -54,7 +52,7 @@ namespace osu.Game.Overlays.Notifications IconContent.AddRange(new Drawable[] { - IconBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index e6bd1367ef..a9fcab063c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -530,7 +530,7 @@ namespace osu.Game.Screens.Play private void load(OsuColour colours, AudioManager audioManager, INotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay) { Icon = FontAwesome.Solid.VolumeMute; - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; Activated = delegate { @@ -584,7 +584,7 @@ namespace osu.Game.Screens.Play private void load(OsuColour colours, INotificationOverlay notificationOverlay) { Icon = FontAwesome.Solid.BatteryQuarter; - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; Activated = delegate { diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index d60f2e4a4b..4790055cd1 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -99,7 +99,7 @@ namespace osu.Game.Updater private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay) { Icon = FontAwesome.Solid.CheckSquare; - IconBackground.Colour = colours.BlueDark; + IconContent.Colour = colours.BlueDark; Activated = delegate { From bea12ab3c262bb777dbba82b4fbc74ce1dd38c60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:49:29 +0900 Subject: [PATCH 22/51] Rename `NotificationContent` to `MainContent` --- osu.Game/Overlays/Notifications/Notification.cs | 10 +++++----- .../Overlays/Notifications/ProgressNotification.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 2b57423305..b04e732f92 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Notifications protected override Container Content => content; - protected Container NotificationContent; + protected Container MainContent; public virtual bool Read { get; set; } @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Notifications Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, }, - NotificationContent = new Container + MainContent = new Container { CornerRadius = 8, Masking = true, @@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Notifications [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - NotificationContent.Add(new Box + MainContent.Add(new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, @@ -157,8 +157,8 @@ namespace osu.Game.Overlays.Notifications this.FadeInFromZero(200); - NotificationContent.MoveToX(DrawSize.X); - NotificationContent.MoveToX(0, 500, Easing.OutQuint); + MainContent.MoveToX(DrawSize.X); + MainContent.MoveToX(0, 500, Easing.OutQuint); } public bool WasClosed; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index c0342f1c2a..2f813b4ad5 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); - NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); + MainContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); this.FadeOut(200).Finally(_ => Completed()); break; } @@ -180,7 +180,7 @@ namespace osu.Game.Overlays.Notifications RelativeSizeAxes = Axes.X, }); - NotificationContent.Add(progressBar = new ProgressBar + MainContent.Add(progressBar = new ProgressBar { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, From c846bf20a76ffb9adf5c698f64f3c668b04f90aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:58:32 +0900 Subject: [PATCH 23/51] Add background hover and adjust remaining metrics --- .../Overlays/Notifications/Notification.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index b04e732f92..a759e2efd1 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -58,6 +58,11 @@ namespace osu.Game.Overlays.Notifications protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box background = null!; + protected Notification() { RelativeSizeAxes = Axes.X; @@ -73,7 +78,7 @@ namespace osu.Game.Overlays.Notifications }, MainContent = new Container { - CornerRadius = 8, + CornerRadius = 6, Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -133,9 +138,9 @@ namespace osu.Game.Overlays.Notifications } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { - MainContent.Add(new Box + MainContent.Add(background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, @@ -143,6 +148,18 @@ namespace osu.Game.Overlays.Notifications }); } + protected override bool OnHover(HoverEvent e) + { + background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + protected override bool OnClick(ClickEvent e) { if (Activated?.Invoke() ?? true) @@ -193,7 +210,7 @@ namespace osu.Game.Overlays.Notifications private void load() { RelativeSizeAxes = Axes.Y; - Width = 24; + Width = 28; Children = new Drawable[] { @@ -216,15 +233,15 @@ namespace osu.Game.Overlays.Notifications protected override bool OnHover(HoverEvent e) { - background.FadeIn(200); - icon.FadeColour(colourProvider.Content1, 200); + background.FadeIn(200, Easing.OutQuint); + icon.FadeColour(colourProvider.Content1, 200, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - background.FadeOut(200); - icon.FadeColour(colourProvider.Foreground1, 200); + background.FadeOut(200, Easing.OutQuint); + icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); base.OnHoverLost(e); } } From 6b71b4656d6a10e085d9ab9119110293c7250cbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 19:16:59 +0900 Subject: [PATCH 24/51] Remove `ProgressNotification` vertical movement and delay --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 2f813b4ad5..36b8bac873 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -141,8 +141,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); - MainContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); - this.FadeOut(200).Finally(_ => Completed()); + Completed(); break; } } From 60413e3e7bac003d6d7ceab82b704d4efd236bb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 19:17:40 +0900 Subject: [PATCH 25/51] Enable masking for main content to avoid underlap with close button on word wrap failure. --- osu.Game/Overlays/Notifications/Notification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index a759e2efd1..90d9b88b79 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -118,6 +118,7 @@ namespace osu.Game.Overlays.Notifications { content = new Container { + Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, From 7b006f1f2249278d67a073bbd747e0957aede85a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 20:34:27 +0900 Subject: [PATCH 26/51] Add flash when a new notification is displayed to draw attention --- osu.Game/Overlays/Notifications/Notification.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 90d9b88b79..3407995502 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -61,6 +61,8 @@ namespace osu.Game.Overlays.Notifications [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private readonly Box initialFlash; + private Box background = null!; protected Notification() @@ -133,6 +135,12 @@ namespace osu.Game.Overlays.Notifications } }, }, + initialFlash = new Box + { + Colour = Color4.White.Opacity(0.8f), + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + }, } } }; @@ -177,6 +185,8 @@ namespace osu.Game.Overlays.Notifications MainContent.MoveToX(DrawSize.X); MainContent.MoveToX(0, 500, Easing.OutQuint); + + initialFlash.FadeOutFromOne(2000, Easing.OutQuart); } public bool WasClosed; From b8300ae60a29216e63790857e486614f3c295eac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 20:57:12 +0900 Subject: [PATCH 27/51] Add toast notification tray --- osu.Game/Overlays/NotificationOverlay.cs | 81 +++++++++--- .../Overlays/NotificationOverlayToastTray.cs | 124 ++++++++++++++++++ 2 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Overlays/NotificationOverlayToastTray.cs diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index bd483e073c..096de558dd 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -15,6 +15,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; +using osuTK; using NotificationsStrings = osu.Game.Localisation.NotificationsStrings; namespace osu.Game.Overlays @@ -39,6 +40,23 @@ namespace osu.Game.Overlays private readonly IBindable firstRunSetupVisibility = new Bindable(); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + if (State.Value == Visibility.Visible) + return base.ReceivePositionalInputAt(screenSpacePos); + + if (toastTray.IsDisplayingToasts) + return toastTray.ReceivePositionalInputAt(screenSpacePos); + + return false; + } + + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree || toastTray.IsDisplayingToasts; + + private NotificationOverlayToastTray toastTray = null!; + + private Container mainContent = null!; + [BackgroundDependencyLoader] private void load(FirstRunSetupOverlay? firstRunSetup) { @@ -48,30 +66,41 @@ namespace osu.Game.Overlays Children = new Drawable[] { - new Box + toastTray = new NotificationOverlayToastTray { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, + Origin = Anchor.TopRight, }, - new OsuScrollContainer + mainContent = new Container { - Masking = true, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - sections = new FillFlowContainer + new Box { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new OsuScrollContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), - new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + sections = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new[] + { + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), + new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + } + } } } } - } + }, }; if (firstRunSetup != null) @@ -129,14 +158,22 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); - var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); - section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - if (notification.IsImportant) - Show(); + if (State.Value == Visibility.Hidden) + toastTray.Post(notification, addPermanently); + else + addPermanently(); - updateCounts(); - playDebouncedSample(notification.PopInSampleName); + void addPermanently() + { + var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + + section.Add(notification, depth); + + updateCounts(); + playDebouncedSample(notification.PopInSampleName); + } }); protected override void Update() @@ -152,7 +189,9 @@ namespace osu.Game.Overlays base.PopIn(); this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + + toastTray.FlushAllToasts(); } protected override void PopOut() @@ -162,7 +201,7 @@ namespace osu.Game.Overlays markAllRead(); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); } private void notificationClosed() diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs new file mode 100644 index 0000000000..338e84c472 --- /dev/null +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -0,0 +1,124 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Overlays.Notifications; +using osuTK; + +namespace osu.Game.Overlays +{ + /// + /// A tray which attaches to the left of to show temporary toasts. + /// + public class NotificationOverlayToastTray : CompositeDrawable + { + public bool IsDisplayingToasts => toastFlow.Count > 0; + + private FillFlowContainer toastFlow = null!; + private BufferedContainer toastContentBackground = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private readonly List pendingToastOperations = new List(); + + private int runningDepth; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding(20); + + InternalChildren = new Drawable[] + { + toastContentBackground = (new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = ColourInfo.GradientVertical( + colourProvider.Background6.Opacity(0.7f), + colourProvider.Background6.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + }.WithEffect(new BlurEffect + { + PadExtent = true, + Sigma = new Vector2(20), + }).With(postEffectDrawable => + { + postEffectDrawable.Scale = new Vector2(1.5f, 1); + postEffectDrawable.Position += new Vector2(70, -50); + postEffectDrawable.AutoSizeAxes = Axes.None; + postEffectDrawable.RelativeSizeAxes = Axes.X; + })), + toastFlow = new FillFlowContainer + { + LayoutDuration = 150, + LayoutEasing = Easing.OutQuart, + Spacing = new Vector2(3), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }; + } + + public void FlushAllToasts() + { + foreach (var d in pendingToastOperations.Where(d => !d.Completed)) + d.RunTask(); + + pendingToastOperations.Clear(); + } + + public void Post(Notification notification, Action addPermanently) + { + ++runningDepth; + + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + + toastFlow.Insert(depth, notification); + + pendingToastOperations.Add(Scheduler.AddDelayed(() => + { + // add notification to permanent overlay unless it was already dismissed by the user. + if (notification.WasClosed) + return; + + toastFlow.Remove(notification); + AddInternal(notification); + + notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); + notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => + { + RemoveInternal(notification); + addPermanently(); + + notification.FadeIn(300, Easing.OutQuint); + }); + }, notification.IsImportant ? 15000 : 3000)); + } + + protected override void Update() + { + base.Update(); + + float height = toastFlow.DrawHeight + 120; + float alpha = IsDisplayingToasts ? MathHelper.Clamp(toastFlow.DrawHeight / 40, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + + toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); + toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); + } + } +} From e9cfaa76c954bf2375f3f0c44b24354a2ead697d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:04:28 +0900 Subject: [PATCH 28/51] Change global overlay ordering so notification toasts display above settings --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f7747c5d64..108153fd9d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -804,8 +804,8 @@ namespace osu.Game Children = new Drawable[] { overlayContent = new Container { RelativeSizeAxes = Axes.Both }, - rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, + rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, } }, topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, From 95ce78a50c72df36eaaa2c63101ea9939f930a28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:04:38 +0900 Subject: [PATCH 29/51] Reduce notification post delay now that it's less important --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 096de558dd..b6354179e0 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays if (enabled) // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. - notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 1000); + notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100); else processingPosts = false; } From a7110666a0c43b214b3f83d48587eadc3067d0ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:13:27 +0900 Subject: [PATCH 30/51] Play notification appear sample immediately --- osu.Game/Overlays/NotificationOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b6354179e0..b0ebf7e666 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -160,6 +160,8 @@ namespace osu.Game.Overlays int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + playDebouncedSample(notification.PopInSampleName); + if (State.Value == Visibility.Hidden) toastTray.Post(notification, addPermanently); else @@ -172,7 +174,6 @@ namespace osu.Game.Overlays section.Add(notification, depth); updateCounts(); - playDebouncedSample(notification.PopInSampleName); } }); From 403fc18976e8593e93e7724a9069604fd70a242d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:13:37 +0900 Subject: [PATCH 31/51] Fix notification completion events not being run when overlay not visible --- osu.Game/Overlays/NotificationOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b0ebf7e666..80071db37f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -72,6 +72,7 @@ namespace osu.Game.Overlays }, mainContent = new Container { + AlwaysPresent = true, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { From 224ab29ef405428d9ee4634879fbd6f10658d507 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:19:51 +0900 Subject: [PATCH 32/51] Don't dismiss toasts while hovered (and adjust timings slightly) --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 338e84c472..38f790e88e 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -90,12 +90,20 @@ namespace osu.Game.Overlays toastFlow.Insert(depth, notification); - pendingToastOperations.Add(Scheduler.AddDelayed(() => + pendingToastOperations.Add(scheduleDismissal()); + + ScheduledDelegate scheduleDismissal() => Scheduler.AddDelayed(() => { // add notification to permanent overlay unless it was already dismissed by the user. if (notification.WasClosed) return; + if (notification.IsHovered) + { + pendingToastOperations.Add(scheduleDismissal()); + return; + } + toastFlow.Remove(notification); AddInternal(notification); @@ -107,7 +115,7 @@ namespace osu.Game.Overlays notification.FadeIn(300, Easing.OutQuint); }); - }, notification.IsImportant ? 15000 : 3000)); + }, notification.IsImportant ? 12000 : 2500); } protected override void Update() From ed11b1ba6f57063527ec2fb96965c64671365b8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:31:06 +0900 Subject: [PATCH 33/51] Improve forwarding flow to not use piling delegates --- osu.Game/Overlays/NotificationOverlay.cs | 30 ++++----- .../Overlays/NotificationOverlayToastTray.cs | 62 +++++++++++-------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 80071db37f..593618ab51 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays { toastTray = new NotificationOverlayToastTray { + ForwardNotificationToPermanentStore = addPermanently, Origin = Anchor.TopRight, }, mainContent = new Container @@ -157,27 +158,26 @@ namespace osu.Game.Overlays if (notification is IHasCompletionTarget hasCompletionTarget) hasCompletionTarget.CompletionTarget = Post; - var ourType = notification.GetType(); - - int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) - toastTray.Post(notification, addPermanently); + toastTray.Post(notification); else - addPermanently(); - - void addPermanently() - { - var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); - - section.Add(notification, depth); - - updateCounts(); - } + addPermanently(notification); }); + private void addPermanently(Notification notification) + { + var ourType = notification.GetType(); + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + + var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + + section.Add(notification, depth); + + updateCounts(); + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 38f790e88e..7017365dbd 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Overlays.Notifications; using osuTK; @@ -25,13 +24,13 @@ namespace osu.Game.Overlays { public bool IsDisplayingToasts => toastFlow.Count > 0; - private FillFlowContainer toastFlow = null!; + private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly List pendingToastOperations = new List(); + public Action? ForwardNotificationToPermanentStore { get; set; } private int runningDepth; @@ -63,7 +62,7 @@ namespace osu.Game.Overlays postEffectDrawable.AutoSizeAxes = Axes.None; postEffectDrawable.RelativeSizeAxes = Axes.X; })), - toastFlow = new FillFlowContainer + toastFlow = new FillFlowContainer { LayoutDuration = 150, LayoutEasing = Easing.OutQuart, @@ -76,13 +75,13 @@ namespace osu.Game.Overlays public void FlushAllToasts() { - foreach (var d in pendingToastOperations.Where(d => !d.Completed)) - d.RunTask(); - - pendingToastOperations.Clear(); + foreach (var notification in toastFlow.ToArray()) + { + forwardNotification(notification); + } } - public void Post(Notification notification, Action addPermanently) + public void Post(Notification notification) { ++runningDepth; @@ -90,34 +89,47 @@ namespace osu.Game.Overlays toastFlow.Insert(depth, notification); - pendingToastOperations.Add(scheduleDismissal()); + scheduleDismissal(); - ScheduledDelegate scheduleDismissal() => Scheduler.AddDelayed(() => + void scheduleDismissal() => Scheduler.AddDelayed(() => { - // add notification to permanent overlay unless it was already dismissed by the user. + // Notification dismissed by user. if (notification.WasClosed) return; + // Notification forwarded away. + if (notification.Parent != toastFlow) + return; + + // Notification hovered; delay dismissal. if (notification.IsHovered) { - pendingToastOperations.Add(scheduleDismissal()); + scheduleDismissal(); return; } - toastFlow.Remove(notification); - AddInternal(notification); - - notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); - notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => - { - RemoveInternal(notification); - addPermanently(); - - notification.FadeIn(300, Easing.OutQuint); - }); + // All looks good, forward away! + forwardNotification(notification); }, notification.IsImportant ? 12000 : 2500); } + private void forwardNotification(Notification notification) + { + Debug.Assert(notification.Parent == toastFlow); + + toastFlow.Remove(notification); + AddInternal(notification); + + notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); + notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => + { + RemoveInternal(notification); + ForwardNotificationToPermanentStore?.Invoke(notification); + + notification.FadeIn(300, Easing.OutQuint); + }); + } + protected override void Update() { base.Update(); From a62ba9e0d9bda715dabc408df5b0b1f85ed76cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 00:57:18 +0900 Subject: [PATCH 34/51] Remove notification blocking behaviour of first run setup --- .../Visual/Navigation/TestSceneFirstRunGame.cs | 9 --------- osu.Game/Overlays/NotificationOverlay.cs | 10 ++-------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs index f4078676c9..fe26d59812 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs @@ -26,16 +26,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestImportantNotificationDoesntInterruptSetup() { AddStep("post important notification", () => Game.Notifications.Post(new SimpleNotification { Text = "Important notification" })); - AddAssert("no notification posted", () => Game.Notifications.UnreadCount.Value == 0); AddAssert("first-run setup still visible", () => Game.FirstRunOverlay.State.Value == Visibility.Visible); - - AddUntilStep("finish first-run setup", () => - { - Game.FirstRunOverlay.NextButton.TriggerClick(); - return Game.FirstRunOverlay.State.Value == Visibility.Hidden; - }); - AddWaitStep("wait for post delay", 5); - AddAssert("notifications shown", () => Game.Notifications.State.Value == Visibility.Visible); AddAssert("notification posted", () => Game.Notifications.UnreadCount.Value == 1); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 593618ab51..bb0beeef41 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -38,8 +38,6 @@ namespace osu.Game.Overlays [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly IBindable firstRunSetupVisibility = new Bindable(); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { if (State.Value == Visibility.Visible) @@ -58,7 +56,7 @@ namespace osu.Game.Overlays private Container mainContent = null!; [BackgroundDependencyLoader] - private void load(FirstRunSetupOverlay? firstRunSetup) + private void load() { X = WIDTH; Width = WIDTH; @@ -104,16 +102,13 @@ namespace osu.Game.Overlays } }, }; - - if (firstRunSetup != null) - firstRunSetupVisibility.BindTo(firstRunSetup.State); } private ScheduledDelegate? notificationsEnabler; private void updateProcessingMode() { - bool enabled = (OverlayActivationMode.Value == OverlayActivation.All && firstRunSetupVisibility.Value != Visibility.Visible) || State.Value == Visibility.Visible; + bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible; notificationsEnabler?.Cancel(); @@ -129,7 +124,6 @@ namespace osu.Game.Overlays base.LoadComplete(); State.BindValueChanged(_ => updateProcessingMode()); - firstRunSetupVisibility.BindValueChanged(_ => updateProcessingMode()); OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true); } From 31a9980686a620c5e434a0ea55851e112fe183a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 00:57:30 +0900 Subject: [PATCH 35/51] Update remaining test expectations with new behaviour --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 5 +++-- osu.Game/Overlays/NotificationOverlay.cs | 7 ++++++- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index c196b204b9..38eecaa052 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -110,7 +110,8 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep(@"simple #1", sendHelloNotification); - AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible); + AddAssert("toast displayed", () => notificationOverlay.ToastCount == 1); + AddAssert("is not visible", () => notificationOverlay.State.Value == Visibility.Hidden); checkDisplayedCount(1); @@ -183,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface } private void checkDisplayedCount(int expected) => - AddAssert($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected); + AddUntilStep($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected); private void sendDownloadProgress() { diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index bb0beeef41..3769a7080f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -129,6 +129,8 @@ namespace osu.Game.Overlays public IBindable UnreadCount => unreadCount; + public int ToastCount => toastTray.UnreadCount; + private readonly BindableInt unreadCount = new BindableInt(); private int runningDepth; @@ -155,7 +157,10 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) + { toastTray.Post(notification); + updateCounts(); + } else addPermanently(notification); }); @@ -220,7 +225,7 @@ namespace osu.Game.Overlays private void updateCounts() { - unreadCount.Value = sections.Select(c => c.UnreadCount).Sum(); + unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; } private void markAllRead() diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 7017365dbd..368996f395 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -32,6 +32,8 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } + public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read); + private int runningDepth; [BackgroundDependencyLoader] From 9eb615f9421202ebec908f97c5a1524005e0f3dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 01:40:27 +0900 Subject: [PATCH 36/51] Fix remaining test failures by strengthening `PlayerLoader` tests - Click using `TriggerClick` as notifications move around quite a bit. - Ensure any notifications from a previous test method are cleaned up. --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 44 ++++++++++--------- osu.Game/Overlays/NotificationOverlay.cs | 6 +-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 922529cf19..b6c17fbaca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -14,11 +14,10 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -30,7 +29,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; using osuTK.Input; -using SkipOverlay = osu.Game.Screens.Play.SkipOverlay; namespace osu.Game.Tests.Visual.Gameplay { @@ -83,6 +81,20 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => player = null); + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("read all notifications", () => + { + notificationOverlay.Show(); + notificationOverlay.Hide(); + }); + + AddUntilStep("wait for no notifications", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(0)); + } + /// /// Sets the input manager child to a new test player loader container instance. /// @@ -287,16 +299,9 @@ namespace osu.Game.Tests.Visual.Gameplay saveVolumes(); - AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); - AddStep("click notification", () => - { - var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); - var flowContainer = scrollContainer.Children.OfType>().First(); - var notification = flowContainer.First(); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(1)); - InputManager.MoveMouseTo(notification); - InputManager.Click(MouseButton.Left); - }); + clickNotificationIfAny(); AddAssert("check " + volumeName, assert); @@ -366,15 +371,7 @@ namespace osu.Game.Tests.Visual.Gameplay })); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0)); - AddStep("click notification", () => - { - var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); - var flowContainer = scrollContainer.Children.OfType>().First(); - var notification = flowContainer.First(); - - InputManager.MoveMouseTo(notification); - InputManager.Click(MouseButton.Left); - }); + clickNotificationIfAny(); AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -439,6 +436,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); } + private void clickNotificationIfAny() + { + AddStep("click notification", () => notificationOverlay.ChildrenOfType().FirstOrDefault()?.TriggerClick()); + } + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); private class TestPlayerLoader : PlayerLoader diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 3769a7080f..488f3eab0d 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -157,12 +157,11 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) - { toastTray.Post(notification); - updateCounts(); - } else addPermanently(notification); + + updateCounts(); }); private void addPermanently(Notification notification) @@ -231,7 +230,6 @@ namespace osu.Game.Overlays private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); - updateCounts(); } } From ad650adab0b9ea2b226b10523193f96439265c6c Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:03:44 +0100 Subject: [PATCH 37/51] Fix speed note count sum --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index a156726f94..c39d61020c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (maxStrain == 0) return 0; - return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0))))); + return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); } } } From 0558dae917899a7cafb5dd14adaf016ff7701ce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:03 +0900 Subject: [PATCH 38/51] Mark toasts as read when closing the overlay for added safety I'm not sure how the read status will play out going forward so I'm just adding this to keep things conforming for now. --- osu.Game/Overlays/NotificationOverlay.cs | 1 + osu.Game/Overlays/NotificationOverlayToastTray.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 488f3eab0d..6eb327cbce 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -230,6 +230,7 @@ namespace osu.Game.Overlays private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); + toastTray.MarkAllRead(); updateCounts(); } } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 368996f395..800751072c 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -75,6 +75,12 @@ namespace osu.Game.Overlays }; } + public void MarkAllRead() + { + toastFlow.Children.ForEach(n => n.Read = true); + InternalChildren.OfType().ForEach(n => n.Read = true); + } + public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) From 7c72c6b43faef9a78400a8b0f5807fc8f927885f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:43 +0900 Subject: [PATCH 39/51] Fix unread count potentially missing notifications in a transforming state --- osu.Game/Overlays/NotificationOverlay.cs | 10 +++++----- osu.Game/Overlays/NotificationOverlayToastTray.cs | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6eb327cbce..2c39ebcc87 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -222,16 +222,16 @@ namespace osu.Game.Overlays } } - private void updateCounts() - { - unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; - } - private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); toastTray.MarkAllRead(); updateCounts(); } + + private void updateCounts() + { + unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; + } } } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 800751072c..4417b5e0d0 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -32,7 +33,8 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } - public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read); + public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read) + + InternalChildren.OfType().Count(n => !n.WasClosed && !n.Read); private int runningDepth; @@ -64,7 +66,7 @@ namespace osu.Game.Overlays postEffectDrawable.AutoSizeAxes = Axes.None; postEffectDrawable.RelativeSizeAxes = Axes.X; })), - toastFlow = new FillFlowContainer + toastFlow = new AlwaysUpdateFillFlowContainer { LayoutDuration = 150, LayoutEasing = Easing.OutQuart, @@ -84,9 +86,7 @@ namespace osu.Game.Overlays public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) - { forwardNotification(notification); - } } public void Post(Notification notification) @@ -125,6 +125,7 @@ namespace osu.Game.Overlays { Debug.Assert(notification.Parent == toastFlow); + // Temporarily remove from flow so we can animate the position off to the right. toastFlow.Remove(notification); AddInternal(notification); From c573396ab6f5c1ff4d66d89cc3e73eaaa0aefdc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:54 +0900 Subject: [PATCH 40/51] Fix `IntroTestScene` not clearing previous notifications hard enough --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 66e3612113..e0f0df0554 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -52,6 +52,7 @@ namespace osu.Game.Tests.Visual.Menus }, notifications = new NotificationOverlay { + Depth = float.MinValue, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, } @@ -82,7 +83,14 @@ namespace osu.Game.Tests.Visual.Menus [Test] public virtual void TestPlayIntroWithFailingAudioDevice() { - AddStep("hide notifications", () => notifications.Hide()); + AddStep("reset notifications", () => + { + notifications.Show(); + notifications.Hide(); + }); + + AddUntilStep("wait for no notifications", () => notifications.UnreadCount.Value, () => Is.EqualTo(0)); + AddStep("restart sequence", () => { logo.FinishTransforms(); From 85442fe0328395bb76209b7478bbf69f0a85a5cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:48:30 +0900 Subject: [PATCH 41/51] Adjust dismiss button background colour to avoid conflict with background --- osu.Game/Overlays/Notifications/Notification.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 90d9b88b79..a2ffdbe208 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -217,7 +218,7 @@ namespace osu.Game.Overlays.Notifications { background = new Box { - Colour = colourProvider.Background4, + Colour = OsuColour.Gray(0).Opacity(0.15f), Alpha = 0, RelativeSizeAxes = Axes.Both, }, From 8b9ccc66b7b9bd8c01385083b9b55fd9943f5e68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:49:28 +0900 Subject: [PATCH 42/51] Update `ProgressNotification` font spec to match other notifications --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 36b8bac873..14cf6b3013 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -173,7 +173,7 @@ namespace osu.Game.Overlays.Notifications public ProgressNotification() { - Content.Add(textDrawable = new OsuTextFlowContainer + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, From 7ce1cf7560d00b9c92df6a6b18eda8a5999470d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:19:58 +0900 Subject: [PATCH 43/51] Add test coverage of skip button failure with equal time --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index b6b3650c83..6b02449aa3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -27,8 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay private const double skip_time = 6000; - [SetUp] - public void SetUp() => Schedule(() => + private void createTest(double skipTime = skip_time) => AddStep("create test", () => { requestCount = 0; increment = skip_time; @@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - skip = new TestSkipOverlay(skip_time) + skip = new TestSkipOverlay(skipTime) { RequestSkip = () => { @@ -55,9 +54,25 @@ namespace osu.Game.Tests.Visual.Gameplay gameplayClock = gameplayClockContainer; }); + [Test] + public void TestSkipTimeZero() + { + createTest(0); + AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive); + } + + [Test] + public void TestSkipTimeEqualToSkip() + { + createTest(MasterGameplayClockContainer.MINIMUM_SKIP_TIME); + AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive); + } + [Test] public void TestFadeOnIdle() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero)); AddUntilStep("fully visible", () => skip.FadingContent.Alpha == 1); AddUntilStep("wait for fade", () => skip.FadingContent.Alpha < 1); @@ -70,6 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickableAfterFade() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddUntilStep("wait for fade", () => skip.FadingContent.Alpha == 0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -79,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickOnlyActuatesOnce() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => { @@ -94,6 +113,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickOnlyActuatesMultipleTimes() { + createTest(); + AddStep("set increment lower", () => increment = 3000); AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -106,6 +127,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestDoesntFadeOnMouseDown() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); AddUntilStep("wait for overlay disappear", () => !skip.OverlayContent.IsPresent); From 51346e015417f634928c169d641a332f9bae0590 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:05:57 +0900 Subject: [PATCH 44/51] Fix skip button getting stuck on screen for certain beatmaps Closes #20034. --- osu.Game/Screens/Play/SkipOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 5c9a706549..974d40b538 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -114,16 +114,17 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); + displayTime = gameplayClock.CurrentTime; + // skip is not required if there is no extra "empty" time to skip. // we may need to remove this if rewinding before the initial player load position becomes a thing. - if (fadeOutBeginTime < gameplayClock.CurrentTime) + if (fadeOutBeginTime <= displayTime) { Expire(); return; } button.Action = () => RequestSkip?.Invoke(); - displayTime = gameplayClock.CurrentTime; fadeContainer.TriggerShow(); @@ -146,7 +147,12 @@ namespace osu.Game.Screens.Play { base.Update(); - double progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + // This case causes an immediate expire in `LoadComplete`, but `Update` may run once after that. + // Avoid div-by-zero below. + if (fadeOutBeginTime <= displayTime) + return; + + double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From 93bc4b9294cf08d32d6ba6af00ccf72200a79cc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:44:06 +0900 Subject: [PATCH 45/51] Add toggle for tournament client "auto progression" behaviour Addresses https://github.com/ppy/osu/discussions/20038. --- osu.Game.Tournament/Models/LadderInfo.cs | 2 ++ .../Screens/Gameplay/GameplayScreen.cs | 21 +++++++++++-------- .../Screens/MapPool/MapPoolScreen.cs | 9 +++++--- .../Screens/Setup/SetupScreen.cs | 6 ++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 0f73b60774..6b64a1156e 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -40,5 +40,7 @@ namespace osu.Game.Tournament.Models MinValue = 3, MaxValue = 4, }; + + public Bindable AutoProgressScreens = new BindableBool(true); } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 54ae4c0366..8a23ee65da 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -199,16 +199,19 @@ namespace osu.Game.Tournament.Screens.Gameplay case TourneyState.Idle: contract(); - const float delay_before_progression = 4000; - - // if we've returned to idle and the last screen was ranking - // we should automatically proceed after a short delay - if (lastState == TourneyState.Ranking && !warmup.Value) + if (LadderInfo.AutoProgressScreens.Value) { - if (CurrentMatch.Value?.Completed.Value == true) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); - else if (CurrentMatch.Value?.Completed.Value == false) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); + const float delay_before_progression = 4000; + + // if we've returned to idle and the last screen was ranking + // we should automatically proceed after a short delay + if (lastState == TourneyState.Ranking && !warmup.Value) + { + if (CurrentMatch.Value?.Completed.Value == true) + scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); + else if (CurrentMatch.Value?.Completed.Value == false) + scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); + } } break; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 5eb2142fae..4d36515316 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -197,10 +197,13 @@ namespace osu.Game.Tournament.Screens.MapPool setNextMode(); - if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) + if (LadderInfo.AutoProgressScreens.Value) { - scheduledChange?.Cancel(); - scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); + if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) + { + scheduledChange?.Cancel(); + scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); + } } } diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index 2b2dce3664..ff781dec80 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -131,6 +131,12 @@ namespace osu.Game.Tournament.Screens.Setup windowSize.Value = new Size((int)(height * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), height); } }, + new LabelledSwitchButton + { + Label = "Auto advance screens", + Description = "Screens will progress automatically from gameplay -> results -> map pool", + Current = LadderInfo.AutoProgressScreens, + }, }; } From e9463f3c192aa635dc73c891e61ceb58206572e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 14:07:55 +0900 Subject: [PATCH 46/51] Test editor `ComposeScreen` tests not adding beatmap to hierarchy Makes it hard to test anything because `EditorBeatmap`'s `Update` method updates whether a beatmap has timing or not (enabling the placement controls). Also adds a basic timing point to allow for better testing. --- .../Editor/TestSceneManiaComposeScreen.cs | 15 ++++++++++++--- .../Visual/Editing/TestSceneComposeScreen.cs | 11 +++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 15b38b39ba..0354228cca 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -34,10 +35,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { AddStep("setup compose screen", () => { - var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 }) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, - }); + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + + var editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null)); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); @@ -50,7 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), }, - Child = new ComposeScreen { State = { Value = Visibility.Visible } }, + Children = new Drawable[] + { + editorBeatmap, + new ComposeScreen { State = { Value = Visibility.Visible } }, + } }; }); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs index 291630fa3a..0df44b9ac4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; @@ -34,9 +35,11 @@ namespace osu.Game.Tests.Visual.Editing { var beatmap = new OsuBeatmap { - BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, }; + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null)); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); @@ -50,7 +53,11 @@ namespace osu.Game.Tests.Visual.Editing (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), }, - Child = new ComposeScreen { State = { Value = Visibility.Visible } }, + Children = new Drawable[] + { + editorBeatmap, + new ComposeScreen { State = { Value = Visibility.Visible } }, + } }; }); From cc9dc604a069506f443aa8d3f77bcc35d9d4025a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 31 Aug 2022 17:21:58 +0900 Subject: [PATCH 47/51] Refactor feedback sample playback logic --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1984840553..a5f133506e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -40,30 +42,26 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Left = 2 }, }; - private readonly Sample?[] textAddedSamples = new Sample[4]; - private Sample? capsTextAddedSample; - private Sample? textRemovedSample; - private Sample? textCommittedSample; - private Sample? caretMovedSample; - - private Sample? selectCharSample; - private Sample? selectWordSample; - private Sample? selectAllSample; - private Sample? deselectSample; - private OsuCaret? caret; private bool selectionStarted; private double sampleLastPlaybackTime; - private enum SelectionSampleType + private enum FeedbackSampleType { - Character, - Word, - All, + TextAdd, + TextAddCaps, + TextRemove, + TextConfirm, + CaretMove, + SelectCharacter, + SelectWord, + SelectAll, Deselect } + private Dictionary sampleMap = new Dictionary(); + public OsuTextBox() { Height = 40; @@ -87,18 +85,22 @@ namespace osu.Game.Graphics.UserInterface Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255); + var textAddedSamples = new Sample?[4]; for (int i = 0; i < textAddedSamples.Length; i++) textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); - capsTextAddedSample = audio.Samples.Get(@"Keyboard/key-caps"); - textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete"); - textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm"); - caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); - - selectCharSample = audio.Samples.Get(@"Keyboard/select-char"); - selectWordSample = audio.Samples.Get(@"Keyboard/select-word"); - selectAllSample = audio.Samples.Get(@"Keyboard/select-all"); - deselectSample = audio.Samples.Get(@"Keyboard/deselect"); + sampleMap = new Dictionary + { + { FeedbackSampleType.TextAdd, textAddedSamples }, + { FeedbackSampleType.TextAddCaps, new[] { audio.Samples.Get(@"Keyboard/key-caps") } }, + { FeedbackSampleType.TextRemove, new[] { audio.Samples.Get(@"Keyboard/key-delete") } }, + { FeedbackSampleType.TextConfirm, new[] { audio.Samples.Get(@"Keyboard/key-confirm") } }, + { FeedbackSampleType.CaretMove, new[] { audio.Samples.Get(@"Keyboard/key-movement") } }, + { FeedbackSampleType.SelectCharacter, new[] { audio.Samples.Get(@"Keyboard/select-char") } }, + { FeedbackSampleType.SelectWord, new[] { audio.Samples.Get(@"Keyboard/select-word") } }, + { FeedbackSampleType.SelectAll, new[] { audio.Samples.Get(@"Keyboard/select-all") } }, + { FeedbackSampleType.Deselect, new[] { audio.Samples.Get(@"Keyboard/deselect") } } + }; } private Color4 selectionColour; @@ -110,23 +112,23 @@ namespace osu.Game.Graphics.UserInterface base.OnUserTextAdded(added); if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) - capsTextAddedSample?.Play(); + playSample(FeedbackSampleType.TextAddCaps); else - playTextAddedSample(); + playSample(FeedbackSampleType.TextAdd); } protected override void OnUserTextRemoved(string removed) { base.OnUserTextRemoved(removed); - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); } protected override void OnTextCommitted(bool textChanged) { base.OnTextCommitted(textChanged); - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } protected override void OnCaretMoved(bool selecting) @@ -134,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface base.OnCaretMoved(selecting); if (!selecting) - caretMovedSample?.Play(); + playSample(FeedbackSampleType.CaretMove); } protected override void OnTextSelectionChanged(TextSelectionType selectionType) @@ -144,15 +146,15 @@ namespace osu.Game.Graphics.UserInterface switch (selectionType) { case TextSelectionType.Character: - playSelectSample(SelectionSampleType.Character); + playSample(FeedbackSampleType.SelectCharacter); break; case TextSelectionType.Word: - playSelectSample(selectionStarted ? SelectionSampleType.Character : SelectionSampleType.Word); + playSample(selectionStarted ? FeedbackSampleType.SelectCharacter : FeedbackSampleType.SelectWord); break; case TextSelectionType.All: - playSelectSample(SelectionSampleType.All); + playSample(FeedbackSampleType.SelectAll); break; } @@ -165,7 +167,7 @@ namespace osu.Game.Graphics.UserInterface if (!selectionStarted) return; - playSelectSample(SelectionSampleType.Deselect); + playSample(FeedbackSampleType.Deselect); selectionStarted = false; } @@ -184,13 +186,13 @@ namespace osu.Game.Graphics.UserInterface case 1: // composition probably ended by pressing backspace, or was cancelled. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; default: // longer text removed, composition ended because it was cancelled. // could be a different sample if desired. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; } } @@ -198,7 +200,7 @@ namespace osu.Game.Graphics.UserInterface if (addedTextLength > 0) { // some text was added, probably due to typing new text or by changing the candidate. - playTextAddedSample(); + playSample(FeedbackSampleType.TextAdd); return; } @@ -206,14 +208,14 @@ namespace osu.Game.Graphics.UserInterface { // text was probably removed by backspacing. // it's also possible that a candidate that only removed text was changed to. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; } if (caretMoved) { // only the caret/selection was moved. - caretMovedSample?.Play(); + playSample(FeedbackSampleType.CaretMove); } } @@ -224,13 +226,13 @@ namespace osu.Game.Graphics.UserInterface if (successful) { // composition was successfully completed, usually by pressing the enter key. - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } else { // composition was prematurely ended, eg. by clicking inside the textbox. // could be a different sample if desired. - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } } @@ -259,43 +261,35 @@ namespace osu.Game.Graphics.UserInterface SelectionColour = SelectionColour, }; - private void playSelectSample(SelectionSampleType selectionType) + private SampleChannel? getSampleChannel(FeedbackSampleType feedbackSampleType) + { + var samples = sampleMap[feedbackSampleType]; + + if (samples == null || samples.Length == 0) + return null; + + return samples[RNG.Next(0, samples.Length)]?.GetChannel(); + } + + private void playSample(FeedbackSampleType feedbackSample) { if (Time.Current < sampleLastPlaybackTime + 15) return; - SampleChannel? channel; - double pitch = 0.98 + RNG.NextDouble(0.04); - - switch (selectionType) - { - case SelectionSampleType.All: - channel = selectAllSample?.GetChannel(); - break; - - case SelectionSampleType.Word: - channel = selectWordSample?.GetChannel(); - break; - - case SelectionSampleType.Deselect: - channel = deselectSample?.GetChannel(); - break; - - default: - channel = selectCharSample?.GetChannel(); - pitch += (SelectedText.Length / (double)Text.Length) * 0.15f; - break; - } + SampleChannel? channel = getSampleChannel(feedbackSample); if (channel == null) return; + double pitch = 0.98 + RNG.NextDouble(0.04); + + if (feedbackSample == FeedbackSampleType.SelectCharacter) + pitch += ((double)SelectedText.Length / Math.Max(1, Text.Length)) * 0.15f; + channel.Frequency.Value = pitch; channel.Play(); sampleLastPlaybackTime = Time.Current; } - private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); - private class OsuCaret : Caret { private const float caret_move_time = 60; From 212d76a11f3b3bbb6e8bafdcc9c935076a34c61c Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 31 Aug 2022 17:23:22 +0900 Subject: [PATCH 48/51] Add audio feedback for invalid textbox input --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index a5f133506e..e5341cfd4b 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -53,6 +53,7 @@ namespace osu.Game.Graphics.UserInterface TextAddCaps, TextRemove, TextConfirm, + TextInvalid, CaretMove, SelectCharacter, SelectWord, @@ -95,6 +96,7 @@ namespace osu.Game.Graphics.UserInterface { FeedbackSampleType.TextAddCaps, new[] { audio.Samples.Get(@"Keyboard/key-caps") } }, { FeedbackSampleType.TextRemove, new[] { audio.Samples.Get(@"Keyboard/key-delete") } }, { FeedbackSampleType.TextConfirm, new[] { audio.Samples.Get(@"Keyboard/key-confirm") } }, + { FeedbackSampleType.TextInvalid, new[] { audio.Samples.Get(@"Keyboard/key-invalid") } }, { FeedbackSampleType.CaretMove, new[] { audio.Samples.Get(@"Keyboard/key-movement") } }, { FeedbackSampleType.SelectCharacter, new[] { audio.Samples.Get(@"Keyboard/select-char") } }, { FeedbackSampleType.SelectWord, new[] { audio.Samples.Get(@"Keyboard/select-word") } }, @@ -111,6 +113,9 @@ namespace osu.Game.Graphics.UserInterface { base.OnUserTextAdded(added); + if (!added.Any(CanAddCharacter)) + return; + if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) playSample(FeedbackSampleType.TextAddCaps); else @@ -124,6 +129,13 @@ namespace osu.Game.Graphics.UserInterface playSample(FeedbackSampleType.TextRemove); } + protected override void NotifyInputError() + { + base.NotifyInputError(); + + playSample(FeedbackSampleType.TextInvalid); + } + protected override void OnTextCommitted(bool textChanged) { base.OnTextCommitted(textChanged); From 50e8052f0706d00cb9cb8fae11d140566acb1e92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 22:08:20 +0900 Subject: [PATCH 49/51] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index bff3627af7..85857771a5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5fc69e475f..f757fd77b9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f763e411be..9fcc3753eb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From 6af8143c8caed96fbfb18fcd02c226cb311a18bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 22:34:07 +0900 Subject: [PATCH 50/51] Fix typing of new setting to allow it to be visible to tools export --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b48f0b4ccd..5e2a92e5e9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods }; [SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")] - public BindableBool Metronome { get; } = new BindableBool(true); + public Bindable Metronome { get; } = new BindableBool(true); #region Constants From ba20044af499ea5555e1cfc61e91ab64630a0cad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 23:24:39 +0900 Subject: [PATCH 51/51] Fix missing nullability consideraition --- .../Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index b845b85e1f..16d564f0ee 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("notification arrived", () => notificationOverlay.Verify(n => n.Post(It.IsAny()), Times.Once)); - AddStep("run notification action", () => lastNotification.Activated()); + AddStep("run notification action", () => lastNotification.Activated?.Invoke()); AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible); AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);