Skip to content

Commit

Permalink
Merge pull request #25762 from bdach/if-i-speak-i-am-in-big-trouble-pt-2
Browse files Browse the repository at this point in the history
Fix `GetRateAdjustedDisplayDifficulty()` (partially incorrectly) locally reimplementing difficulty range calculations
  • Loading branch information
peppy committed Dec 15, 2023
2 parents 73c0a0e + 0f4d054 commit 23fbc75
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;

namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class CatchRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}

[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}

[Test]
public void TestRateBelowOne()
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}

[Test]
public void TestRateAboveOne()
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
}
}
10 changes: 4 additions & 6 deletions osu.Game.Rulesets.Catch/CatchRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Catch.Skinning.Argon;
Expand Down Expand Up @@ -236,17 +237,14 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
};
}

/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);

double preempt = adjustedDifficulty.ApproachRate < 6
? 1200.0 + 600.0 * (5 - adjustedDifficulty.ApproachRate) / 5
: 1200.0 - 750.0 * (adjustedDifficulty.ApproachRate - 5) / 5;

double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;

adjustedDifficulty.ApproachRate = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);

return adjustedDifficulty;
}
Expand Down
17 changes: 16 additions & 1 deletion osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);

Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
Expand Down Expand Up @@ -189,6 +189,21 @@ public void UpdateComboInformation(IHasComboInformation? lastObj)
// The half of the height of the osu! playfield.
public const float DEFAULT_LEGACY_CONVERT_Y = 192;

/// <summary>
/// Minimum preempt time at AR=10.
/// </summary>
public const double PREEMPT_MIN = 450;

/// <summary>
/// Median preempt time at AR=5.
/// </summary>
public const double PREEMPT_MID = 1200;

/// <summary>
/// Maximum preempt time at AR=0.
/// </summary>
public const double PREEMPT_MAX = 1800;

/// <summary>
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;

namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}

[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}

[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);

Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}

[Test]
public void TestRateBelowOne()
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
}

[Test]
public void TestRateAboveOne()
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);

Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
}
}
}
12 changes: 11 additions & 1 deletion osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPositi
/// </summary>
public const double PREEMPT_MIN = 450;

/// <summary>
/// Median preempt time at AR=5.
/// </summary>
public const double PREEMPT_MID = 1200;

/// <summary>
/// Maximum preempt time at AR=0.
/// </summary>
public const double PREEMPT_MAX = 1800;

public double TimePreempt = 600;
public double TimeFadeIn = 400;

Expand Down Expand Up @@ -148,7 +158,7 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, I
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);

// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
Expand Down
19 changes: 8 additions & 11 deletions osu.Game.Rulesets.Osu/OsuRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,23 +332,20 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm

public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();

/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);

double preempt = adjustedDifficulty.ApproachRate < 5
? 1200.0 + 600.0 * (5 - adjustedDifficulty.ApproachRate) / 5
: 1200.0 - 750.0 * (adjustedDifficulty.ApproachRate - 5) / 5;

double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
preempt /= rate;
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);

adjustedDifficulty.ApproachRate = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);

double hitwindow = 80.0 - 6 * adjustedDifficulty.OverallDifficulty;

hitwindow /= rate;

adjustedDifficulty.OverallDifficulty = (float)(80.0 - hitwindow) / 6;
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);

return adjustedDifficulty;
}
Expand Down
4 changes: 2 additions & 2 deletions osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class OsuHitWindows : HitWindows
/// </summary>
public const double MISS_WINDOW = 400;

private static readonly DifficultyRange[] osu_ranges =
internal static readonly DifficultyRange[] OSU_RANGES =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
Expand All @@ -34,6 +34,6 @@ public override bool IsHitResultAllowed(HitResult result)
return false;
}

protected override DifficultyRange[] GetRanges() => osu_ranges;
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;

namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TaikoRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}

[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);

Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}

[Test]
public void TestRateBelowOne()
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);

Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
}

[Test]
public void TestRateAboveOne()
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();

var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);

Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
}
}
}
4 changes: 2 additions & 2 deletions osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoHitWindows : HitWindows
{
private static readonly DifficultyRange[] taiko_ranges =
internal static readonly DifficultyRange[] TAIKO_RANGES =
{
new DifficultyRange(HitResult.Great, 50, 35, 20),
new DifficultyRange(HitResult.Ok, 120, 80, 50),
Expand All @@ -27,6 +27,6 @@ public override bool IsHitResultAllowed(HitResult result)
return false;
}

protected override DifficultyRange[] GetRanges() => taiko_ranges;
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
}
}
10 changes: 5 additions & 5 deletions osu.Game.Rulesets.Taiko/TaikoRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,15 @@ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatm
};
}

/// <seealso cref="TaikoHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);

double hitWindow = 35.0 - 15.0 * (adjustedDifficulty.OverallDifficulty - 5) / 5;

hitWindow /= rate;

adjustedDifficulty.OverallDifficulty = (float)(5 * (35 - hitWindow) / 15 + 5);
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);

return adjustedDifficulty;
}
Expand Down
18 changes: 18 additions & 0 deletions osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;

namespace osu.Game.Beatmaps
{
/// <summary>
Expand Down Expand Up @@ -92,5 +94,21 @@ static double DifficultyRange(double difficulty, double min, double mid, double
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);

/// <summary>
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
/// Maps a value returned by the function above back to the difficulty that produced it.
/// </summary>
/// <param name="difficultyValue">The difficulty-dependent value to be unmapped.</param>
/// <param name="diff0">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="diff5">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="diff10">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double InverseDifficultyRange(double difficultyValue, double diff0, double diff5, double diff10)
{
return Math.Sign(difficultyValue - diff5) == Math.Sign(diff10 - diff5)
? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5
: (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5;
}
}
}

0 comments on commit 23fbc75

Please sign in to comment.