1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 13:44:34 +08:00

Calculate clock time when creating difficulty hit objects (#36962)

This change removes `clockRate` precalculation from
`DifficultyCalculator`.

The idea is that clock rate should be calculated in-place (ideally for
every object) since we store and access it using DHOs. This also
prevents anyone from accidentally passing clock rate to skills

Unfortunately osu uses clock rate to calculate OD for the whole map in
`CreateDifficultyAttributes` so we can't make it completely DHO-based,
but I think one single in-place call to `ModUtils.CalculateRateWithMods`
in `CreateDifficultyAttributes` is fine

---------

Co-authored-by: James Wilson <tsunyoku@gmail.com>
This commit is contained in:
StanR
2026-03-17 02:17:49 +05:00
committed by GitHub
Unverified
parent 09bcef1bdd
commit fadb1c3f2c
12 changed files with 59 additions and 49 deletions
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
return new DifficultyAttributes(mods, 0);
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
}
}
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
return new DifficultyAttributes(mods, 0);
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
}
}
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyScrolling
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
return new DifficultyAttributes(mods, 0);
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
}
}
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
return new DifficultyAttributes(mods, 0);
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
}
}
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
if (beatmap.HitObjects.Count == 0)
return new CatchDifficultyAttributes { Mods = mods };
@@ -44,12 +45,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return attributes;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
CatchHitObject? lastObject = null;
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
double clockRate = ModUtils.CalculateRateWithMods(mods);
float halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
@@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return objects;
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
return new Skill[]
{
@@ -19,6 +19,7 @@ using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Difficulty
{
@@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
if (beatmap.HitObjects.Count == 0)
return new ManiaDifficultyAttributes { Mods = mods };
@@ -62,11 +63,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return 1;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
var sortedObjects = beatmap.HitObjects.ToArray();
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
double clockRate = ModUtils.CalculateRateWithMods(mods);
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
@@ -88,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
{
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
};
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return (79.5 - hitWindowGreat) / 6;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };
@@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double difficultSliders = aim.GetDifficultSliders();
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, ModUtils.CalculateRateWithMods(mods));
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
@@ -161,10 +162,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER);
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
double clockRate = ModUtils.CalculateRateWithMods(mods);
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < beatmap.HitObjects.Count; i++)
@@ -175,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return objects;
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
var skills = new List<Skill>
{
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
@@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
isRelax = mods.Any(h => h is TaikoModRelax);
@@ -62,13 +63,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
new TaikoModHardRock(),
};
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
var difficultyHitObjects = new List<DifficultyHitObject>();
var centreObjects = new List<TaikoDifficultyHitObject>();
var rimObjects = new List<TaikoDifficultyHitObject>();
var noteObjects = new List<TaikoDifficultyHitObject>();
double clockRate = ModUtils.CalculateRateWithMods(mods);
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
for (int i = 2; i < beatmap.HitObjects.Count; i++)
{
@@ -92,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return difficultyHitObjects;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
@@ -223,17 +223,17 @@ namespace osu.Game.Tests.NonVisual
protected override Mod[] DifficultyAdjustmentMods { get; }
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
{
throw new NotImplementedException();
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
throw new NotImplementedException();
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
throw new NotImplementedException();
}
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Beatmaps;
using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
@@ -172,13 +173,15 @@ namespace osu.Game.Tests.NonVisual
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
=> new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
double clockRate = ModUtils.CalculateRateWithMods(mods);
foreach (var obj in beatmap.HitObjects.OfType<TestHitObject>())
{
if (!obj.Skip)
@@ -191,7 +194,7 @@ namespace osu.Game.Tests.NonVisual
return objects;
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) };
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { new PassThroughSkill(mods) };
private class PassThroughSkill : Skill
{
@@ -208,13 +208,13 @@ namespace osu.Game.Tests.Visual.UserInterface
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
=> new DifficultyAttributes(mods, mods.OfType<TestMod>().SingleOrDefault()?.Difficulty.Value ?? 0);
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
=> Array.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
=> Array.Empty<Skill>();
}
@@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Difficulty
protected readonly IWorkingBeatmap WorkingBeatmap;
private Mod[] playableMods;
private double clockRate;
private readonly IRulesetInfo ruleset;
@@ -74,10 +73,10 @@ namespace osu.Game.Rulesets.Difficulty
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
preProcess(mods, cancellationToken);
var skills = CreateSkills(Beatmap, playableMods, clockRate);
var skills = CreateSkills(Beatmap, playableMods);
if (!Beatmap.HitObjects.Any())
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
return CreateDifficultyAttributes(Beatmap, playableMods, skills);
foreach (var hitObject in getDifficultyHitObjects())
{
@@ -88,7 +87,7 @@ namespace osu.Game.Rulesets.Difficulty
}
}
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
return CreateDifficultyAttributes(Beatmap, playableMods, skills);
}
/// <summary>
@@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Difficulty
if (!Beatmap.HitObjects.Any())
return attribs;
var skills = CreateSkills(Beatmap, playableMods, clockRate);
var skills = CreateSkills(Beatmap, playableMods);
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
var difficultyObjects = getDifficultyHitObjects().ToArray();
@@ -142,7 +141,7 @@ namespace osu.Game.Rulesets.Difficulty
currentIndex++;
}
attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills)));
}
return attribs;
@@ -174,7 +173,7 @@ namespace osu.Game.Rulesets.Difficulty
/// <summary>
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
/// </summary>
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate));
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, playableMods));
/// <summary>
/// Performs required tasks before every calculation.
@@ -185,8 +184,6 @@ namespace osu.Game.Rulesets.Difficulty
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
clockRate = ModUtils.CalculateRateWithMods(playableMods);
}
/// <summary>
@@ -277,16 +274,15 @@ namespace osu.Game.Rulesets.Difficulty
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
/// <param name="skills">The skills which processed the beatmap.</param>
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills);
/// <summary>
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> providing the <see cref="HitObject"/>s to enumerate.</param>
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
/// <param name="mods">Mods to create difficulty objects with.</param>
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods);
/// <summary>
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
@@ -294,9 +290,8 @@ namespace osu.Game.Rulesets.Difficulty
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
/// <param name="mods">Mods to calculate difficulty with.</param>
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
/// <returns>The <see cref="Skill"/>s.</returns>
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
/// <summary>
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.