diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 49c1a055a6..cce0944564 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -10,10 +10,11 @@ using System.Linq; using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy @@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); double beatLength; - if (hitObject.LegacyBpmMultiplier.HasValue) - beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value; - else if (hitObject is IHasSliderVelocity hasSliderVelocity) - beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier; + + if (hitObject is IHasSliderVelocity hasSliderVelocity) + beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME); else beatLength = timingPoint.BeatLength; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1e7e8e2981..8bf19f030f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -136,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.Objects public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 3e9252e1fd..e46e2ec09c 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -14,6 +14,7 @@ using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Rulesets.Taiko.Beatmaps { @@ -186,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); double beatLength; - if (obj.LegacyBpmMultiplier.HasValue) - beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value; - else if (obj is IHasSliderVelocity hasSliderVelocity) - beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier; + + if (obj is IHasSliderVelocity hasSliderVelocity) + beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME); else beatLength = timingPoint.BeatLength; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 970b6aaf60..be53a3e18a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1024,10 +1024,8 @@ namespace osu.Game.Tests.Beatmaps.Formats 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 + Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False); + Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True); } } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index e6f1609d7f..05230c85f4 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -23,11 +23,16 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, 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 bool GenerateTicks { get; set; } = true; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -41,11 +46,13 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty + && GenerateTicks == existingDifficulty.GenerateTicks && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; + GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -56,8 +63,10 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) + && GenerateTicks == other.GenerateTicks && SliderVelocity == other.SliderVelocity; - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); + // ReSharper disable once NonReadonlyMemberInGetHashCode + 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 7c375b7604..37aa7950d3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#pragma warning disable 618 - using System; using System.Collections.Generic; using System.IO; @@ -103,12 +101,8 @@ namespace osu.Game.Beatmaps.Formats { DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT; - if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint) - { - hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier; - if (hitObject is IHasGenerateTicks hasGenerateTicks) - hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks; - } + if (hitObject is IHasGenerateTicks hasGenerateTicks) + hasGenerateTicks.GenerateTicks = difficultyControlPoint.GenerateTicks; if (hitObject is IHasSliderVelocity hasSliderVelocity) hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity; @@ -497,8 +491,9 @@ namespace osu.Game.Beatmaps.Formats int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; - addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength) + addControlPoint(time, new DifficultyControlPoint { + GenerateTicks = !double.IsNaN(beatLength), SliderVelocity = speedMultiplier, }, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 23440b8a1d..93af9cf41c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -163,63 +163,6 @@ namespace osu.Game.Beatmaps.Formats Mania, } - [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")] - public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable - { - /// - /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. - /// DO NOT USE THIS UNLESS 100% SURE. - /// - 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(int rulesetId, 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?). - if (rulesetId == 1 || rulesetId == 3) - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; - else - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 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) - => other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint - && Equals(otherLegacyDifficultyControlPoint); - - public bool Equals(LegacyDifficultyControlPoint? other) - => base.Equals(other) - && BpmMultiplier == other.BpmMultiplier - && GenerateTicks == other.GenerateTicks; - - // ReSharper disable twice NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks); - } - internal class LegacySampleControlPoint : SampleControlPoint, IEquatable { public int CustomSampleBank; diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ed3d3a6eb2..ec2a4a31f6 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -76,12 +76,6 @@ namespace osu.Game.Rulesets.Objects /// public virtual IList AuxiliarySamples => ImmutableList.Empty; - /// - /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. - /// DO NOT USE THIS UNLESS 100% SURE. - /// - public double? LegacyBpmMultiplier { get; set; } - /// /// Whether this is in Kiai time. /// diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs new file mode 100644 index 0000000000..6cff4b12c4 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Objects.Legacy +{ + public static class LegacyRulesetExtensions + { + /// + /// Introduces floating-point errors to post-multiplied beat length for legacy rulesets that depend on it. + /// You should definitely not use this unless you know exactly what you're doing. + /// + public static double GetPrecisionAdjustedBeatLength(IHasSliderVelocity hasSliderVelocity, TimingControlPoint timingControlPoint, string rulesetShortName) + { + double sliderVelocityAsBeatLength = -100 / hasSliderVelocity.SliderVelocityMultiplier; + + // 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?). + double bpmMultiplier; + + switch (rulesetShortName) + { + case "taiko": + case "mania": + bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 10000) / 100.0 : 1; + break; + + case "osu": + case "fruits": + bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 1000) / 100.0 : 1; + break; + + default: + throw new ArgumentException("Must be a legacy ruleset", nameof(rulesetShortName)); + } + + return timingControlPoint.BeatLength * bpmMultiplier; + } + } +}