Skip to content

Commit

Permalink
Merge pull request #19919 from khang06/nan-sv
Browse files Browse the repository at this point in the history
Emulate osu!stable's NaN slider velocity behavior
  • Loading branch information
smoogipoo authored Aug 31, 2022
2 parents 6cadcc2 + adea29c commit 837b19a
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 10 deletions.
7 changes: 6 additions & 1 deletion osu.Game.Rulesets.Osu/Objects/Slider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
Expand Down Expand Up @@ -165,11 +166,15 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
#pragma warning disable 618
var legacyDifficultyPoint = DifficultyControlPoint 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 = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
}

protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
Expand Down
25 changes: 25 additions & 0 deletions osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -919,5 +919,30 @@ public void TestLegacyDuplicateInitialCatmullPointIsMerged()
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(2));

Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500));

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(2000)).GenerateTicks, Is.False);
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
#pragma warning restore 618
}
}
}
}
7 changes: 6 additions & 1 deletion osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
public class ParsingTest
{
[Test]
public void TestNaNHandling() => allThrow<FormatException>("NaN");
public void TestNaNHandling()
{
allThrow<FormatException>("NaN");
Assert.That(Parsing.ParseFloat("NaN", allowNaN: true), Is.NaN);
Assert.That(Parsing.ParseDouble("NaN", allowNaN: true), Is.NaN);
}

[Test]
public void TestBadStringHandling() => allThrow<FormatException>("Random string 123");
Expand Down
15 changes: 15 additions & 0 deletions osu.Game.Tests/Resources/nan-control-points.osu
Original file line number Diff line number Diff line change
@@ -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
9 changes: 8 additions & 1 deletion osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,11 @@ private void handleTimingPoint(string line)
string[] split = line.Split(',');

double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
double beatLength = Parsing.ParseDouble(split[1].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.
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;

TimeSignature timeSignature = TimeSignature.SimpleQuadruple;
Expand Down Expand Up @@ -412,6 +416,9 @@ private void handleTimingPoint(string line)

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;
Expand Down
19 changes: 16 additions & 3 deletions osu.Game/Beatmaps/Formats/LegacyDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,23 +168,35 @@ public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable<L
/// </summary>
public double BpmMultiplier { get; private set; }

/// <summary>
/// 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).
/// </summary>
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()
{
SliderVelocityBindable.Precision = double.Epsilon;
}

public override bool IsRedundant(ControlPoint? existing)
=> base.IsRedundant(existing)
&& GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true);

public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);

BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks;
}

public override bool Equals(ControlPoint? other)
Expand All @@ -193,10 +205,11 @@ public override bool Equals(ControlPoint? other)

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<LegacySampleControlPoint>
Expand Down
8 changes: 4 additions & 4 deletions osu.Game/Beatmaps/Formats/Parsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@ public static class Parsing

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;
}
Expand Down

0 comments on commit 837b19a

Please sign in to comment.