From 9f08c474caf57d8a84a510653c72a7f01ccf2b8d Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 21:44:25 -0400 Subject: [PATCH] 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) {