mirror of
https://github.com/ppy/osu.git
synced 2026-05-22 18:20:47 +08:00
fadb1c3f2c
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>
430 lines
18 KiB
C#
430 lines
18 KiB
C#
// 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.
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using JetBrains.Annotations;
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
using osu.Framework.Lists;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Beatmaps.Timing;
|
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
using osu.Game.Rulesets.Difficulty.Skills;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Utils;
|
|
|
|
namespace osu.Game.Rulesets.Difficulty
|
|
{
|
|
public abstract class DifficultyCalculator
|
|
{
|
|
/// <summary>
|
|
/// The beatmap for which difficulty will be calculated.
|
|
/// </summary>
|
|
protected IBeatmap Beatmap { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The working beatmap for which difficulty will be calculated.
|
|
/// </summary>
|
|
protected readonly IWorkingBeatmap WorkingBeatmap;
|
|
|
|
private Mod[] playableMods;
|
|
|
|
private readonly IRulesetInfo ruleset;
|
|
|
|
/// <summary>
|
|
/// A yymmdd version which is used to discern when reprocessing is required.
|
|
/// </summary>
|
|
public virtual int Version => 0;
|
|
|
|
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
|
{
|
|
this.ruleset = ruleset;
|
|
WorkingBeatmap = beatmap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap with no mods applied.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
|
public DifficultyAttributes Calculate(CancellationToken cancellationToken = default)
|
|
=> Calculate(Array.Empty<Mod>(), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using a specific mod combination.
|
|
/// </summary>
|
|
/// <param name="mods">The mods that should be applied to the beatmap.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
|
public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
|
{
|
|
using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
if (!cancellationToken.CanBeCanceled)
|
|
cancellationToken = timedCancellationSource.Token;
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
|
|
preProcess(mods, cancellationToken);
|
|
|
|
var skills = CreateSkills(Beatmap, playableMods);
|
|
|
|
if (!Beatmap.HitObjects.Any())
|
|
return CreateDifficultyAttributes(Beatmap, playableMods, skills);
|
|
|
|
foreach (var hitObject in getDifficultyHitObjects())
|
|
{
|
|
foreach (var skill in skills)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
skill.Process(hitObject);
|
|
}
|
|
}
|
|
|
|
return CreateDifficultyAttributes(Beatmap, playableMods, skills);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap with no mods applied and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
|
|
public List<TimedDifficultyAttributes> CalculateTimed(CancellationToken cancellationToken = default)
|
|
=> CalculateTimed(Array.Empty<Mod>(), cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using a specific mod combination and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
|
|
/// </summary>
|
|
/// <param name="mods">The mods that should be applied to the beatmap.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
|
|
public List<TimedDifficultyAttributes> CalculateTimed([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
|
{
|
|
using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
if (!cancellationToken.CanBeCanceled)
|
|
cancellationToken = timedCancellationSource.Token;
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
|
|
preProcess(mods, cancellationToken);
|
|
|
|
var attribs = new List<TimedDifficultyAttributes>();
|
|
|
|
if (!Beatmap.HitObjects.Any())
|
|
return attribs;
|
|
|
|
var skills = CreateSkills(Beatmap, playableMods);
|
|
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
|
|
var difficultyObjects = getDifficultyHitObjects().ToArray();
|
|
|
|
int currentIndex = 0;
|
|
|
|
foreach (var obj in Beatmap.HitObjects)
|
|
{
|
|
progressiveBeatmap.HitObjects.Add(obj);
|
|
|
|
while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime())
|
|
{
|
|
foreach (var skill in skills)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
skill.Process(difficultyObjects[currentIndex]);
|
|
}
|
|
|
|
currentIndex++;
|
|
}
|
|
|
|
attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills)));
|
|
}
|
|
|
|
return attribs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can only be used to compute difficulties for legacy mod combinations.
|
|
/// </remarks>
|
|
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
|
public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
|
|
{
|
|
var rulesetInstance = ruleset.CreateInstance();
|
|
|
|
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
|
{
|
|
Mod classicMod = rulesetInstance.CreateMod<ModClassic>();
|
|
|
|
var finalCombination = ModUtils.FlattenMod(combination);
|
|
if (classicMod != null)
|
|
finalCombination = finalCombination.Append(classicMod);
|
|
|
|
yield return Calculate(finalCombination.ToArray(), cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
|
|
/// </summary>
|
|
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, playableMods));
|
|
|
|
/// <summary>
|
|
/// Performs required tasks before every calculation.
|
|
/// </summary>
|
|
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
private void preProcess([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken)
|
|
{
|
|
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
|
Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts a given set of <see cref="DifficultyHitObject"/>s.
|
|
/// </summary>
|
|
/// <param name="input">The <see cref="DifficultyHitObject"/>s to sort.</param>
|
|
/// <returns>The sorted <see cref="DifficultyHitObject"/>s.</returns>
|
|
protected virtual IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input)
|
|
=> input.OrderBy(h => h.BaseObject.StartTime);
|
|
|
|
/// <summary>
|
|
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
|
/// </summary>
|
|
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
|
{
|
|
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
|
|
|
|
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
|
|
{
|
|
// Return the current set.
|
|
switch (currentSetCount)
|
|
{
|
|
case 0:
|
|
// Initial-case: Empty current set
|
|
yield return new ModNoMod();
|
|
|
|
break;
|
|
|
|
case 1:
|
|
yield return currentSet.Single();
|
|
|
|
break;
|
|
|
|
default:
|
|
yield return new MultiMod(currentSet.ToArray());
|
|
|
|
break;
|
|
}
|
|
|
|
// Apply the rest of the remaining mods recursively.
|
|
for (int i = 0; i < remainingMods.Length; i++)
|
|
{
|
|
(var nextSet, int nextCount) = flatten(remainingMods.Span[i]);
|
|
|
|
// Check if any mods in the next set are incompatible with any of the current set.
|
|
if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
|
|
continue;
|
|
|
|
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
|
|
if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
|
|
continue;
|
|
|
|
// If all's good, attach the next set to the current set and recurse further.
|
|
foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
|
|
yield return combo;
|
|
}
|
|
}
|
|
|
|
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
|
|
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
|
|
{
|
|
if (!(mod is MultiMod multi))
|
|
return (mod.Yield(), 1);
|
|
|
|
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
|
|
int count = 0;
|
|
|
|
foreach (var nested in multi.Mods)
|
|
{
|
|
(var nestedSet, int nestedCount) = flatten(nested);
|
|
set = set.Concat(nestedSet);
|
|
count += nestedCount;
|
|
}
|
|
|
|
return (set, count);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
|
/// </summary>
|
|
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
|
|
|
/// <summary>
|
|
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
|
/// </summary>
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
|
|
/// 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>
|
|
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="mods">Mods to create difficulty objects with.</param>
|
|
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
|
|
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"/>.
|
|
/// </summary>
|
|
/// <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>
|
|
/// <returns>The <see cref="Skill"/>s.</returns>
|
|
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.
|
|
/// </summary>
|
|
private class ProgressiveCalculationBeatmap : IBeatmap
|
|
{
|
|
private readonly IBeatmap baseBeatmap;
|
|
|
|
public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap)
|
|
{
|
|
this.baseBeatmap = baseBeatmap;
|
|
}
|
|
|
|
public readonly List<HitObject> HitObjects = new List<HitObject>();
|
|
|
|
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
|
|
|
|
#region Delegated IBeatmap implementation
|
|
|
|
public BeatmapInfo BeatmapInfo
|
|
{
|
|
get => baseBeatmap.BeatmapInfo;
|
|
set => baseBeatmap.BeatmapInfo = value;
|
|
}
|
|
|
|
public ControlPointInfo ControlPointInfo
|
|
{
|
|
get => baseBeatmap.ControlPointInfo;
|
|
set => baseBeatmap.ControlPointInfo = value;
|
|
}
|
|
|
|
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
|
|
|
|
public BeatmapDifficulty Difficulty
|
|
{
|
|
get => baseBeatmap.Difficulty;
|
|
set => baseBeatmap.Difficulty = value;
|
|
}
|
|
|
|
public SortedList<BreakPeriod> Breaks
|
|
{
|
|
get => baseBeatmap.Breaks;
|
|
set => baseBeatmap.Breaks = value;
|
|
}
|
|
|
|
public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines;
|
|
|
|
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
|
|
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
|
|
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
|
|
public int BeatmapVersion => baseBeatmap.BeatmapVersion;
|
|
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
|
|
|
|
public double AudioLeadIn
|
|
{
|
|
get => baseBeatmap.AudioLeadIn;
|
|
set => baseBeatmap.AudioLeadIn = value;
|
|
}
|
|
|
|
public float StackLeniency
|
|
{
|
|
get => baseBeatmap.StackLeniency;
|
|
set => baseBeatmap.StackLeniency = value;
|
|
}
|
|
|
|
public bool SpecialStyle
|
|
{
|
|
get => baseBeatmap.SpecialStyle;
|
|
set => baseBeatmap.SpecialStyle = value;
|
|
}
|
|
|
|
public bool LetterboxInBreaks
|
|
{
|
|
get => baseBeatmap.LetterboxInBreaks;
|
|
set => baseBeatmap.LetterboxInBreaks = value;
|
|
}
|
|
|
|
public bool WidescreenStoryboard
|
|
{
|
|
get => baseBeatmap.WidescreenStoryboard;
|
|
set => baseBeatmap.WidescreenStoryboard = value;
|
|
}
|
|
|
|
public bool EpilepsyWarning
|
|
{
|
|
get => baseBeatmap.EpilepsyWarning;
|
|
set => baseBeatmap.EpilepsyWarning = value;
|
|
}
|
|
|
|
public bool SamplesMatchPlaybackRate
|
|
{
|
|
get => baseBeatmap.SamplesMatchPlaybackRate;
|
|
set => baseBeatmap.SamplesMatchPlaybackRate = value;
|
|
}
|
|
|
|
public double DistanceSpacing
|
|
{
|
|
get => baseBeatmap.DistanceSpacing;
|
|
set => baseBeatmap.DistanceSpacing = value;
|
|
}
|
|
|
|
public int GridSize
|
|
{
|
|
get => baseBeatmap.GridSize;
|
|
set => baseBeatmap.GridSize = value;
|
|
}
|
|
|
|
public double TimelineZoom
|
|
{
|
|
get => baseBeatmap.TimelineZoom;
|
|
set => baseBeatmap.TimelineZoom = value;
|
|
}
|
|
|
|
public CountdownType Countdown
|
|
{
|
|
get => baseBeatmap.Countdown;
|
|
set => baseBeatmap.Countdown = value;
|
|
}
|
|
|
|
public int CountdownOffset
|
|
{
|
|
get => baseBeatmap.CountdownOffset;
|
|
set => baseBeatmap.CountdownOffset = value;
|
|
}
|
|
|
|
public int[] Bookmarks
|
|
{
|
|
get => baseBeatmap.Bookmarks;
|
|
set => baseBeatmap.Bookmarks = value;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
}
|