osu/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs

332 lines
15 KiB
C#
Raw Normal View History

// 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.
2018-04-13 09:19:50 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
2018-06-06 07:20:17 +00:00
using System;
2018-04-13 09:19:50 +00:00
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Audio.Track;
2019-02-21 04:12:37 +00:00
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
2021-09-30 08:00:15 +00:00
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;
2019-02-21 04:12:37 +00:00
using osu.Game.Rulesets.Objects;
2022-02-17 12:14:49 +00:00
using osu.Game.Utils;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Rulesets.Difficulty
2018-04-13 09:19:50 +00:00
{
2019-02-21 04:12:37 +00:00
public abstract class DifficultyCalculator
2018-04-13 09:19:50 +00:00
{
/// <summary>
/// The beatmap for which difficulty will be calculated.
/// </summary>
protected IBeatmap Beatmap { get; private set; }
2019-02-21 04:12:37 +00:00
2021-09-30 08:00:15 +00:00
private Mod[] playableMods;
private double clockRate;
private readonly IRulesetInfo ruleset;
private readonly IWorkingBeatmap beatmap;
/// <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)
2018-04-13 09:19:50 +00:00
{
2019-02-21 04:12:37 +00:00
this.ruleset = ruleset;
this.beatmap = 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);
2019-02-21 04:12:37 +00:00
/// <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>
2019-02-21 04:12:37 +00:00
/// <returns>A structure describing the difficulty of the beatmap.</returns>
public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
2019-02-21 04:12:37 +00:00
{
cancellationToken.ThrowIfCancellationRequested();
preProcess(mods, cancellationToken);
var skills = CreateSkills(Beatmap, playableMods, clockRate);
2019-02-21 04:12:37 +00:00
if (!Beatmap.HitObjects.Any())
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
2021-09-30 08:00:15 +00:00
foreach (var hitObject in getDifficultyHitObjects())
{
foreach (var skill in skills)
{
cancellationToken.ThrowIfCancellationRequested();
2022-05-22 20:45:27 +00:00
skill.Process(hitObject);
}
2021-09-30 08:00:15 +00:00
}
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
2021-09-30 08:00:15 +00:00
}
2021-11-02 08:55:00 +00:00
/// <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.
2021-11-02 08:55:00 +00:00
/// </summary>
/// <param name="mods">The mods that should be applied to the beatmap.</param>
/// <param name="cancellationToken">The cancellation token.</param>
2021-11-02 08:55:00 +00:00
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
public List<TimedDifficultyAttributes> CalculateTimed([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
2021-09-30 08:00:15 +00:00
{
cancellationToken.ThrowIfCancellationRequested();
preProcess(mods, cancellationToken);
2021-09-30 08:00:15 +00:00
2021-10-05 07:59:54 +00:00
var attribs = new List<TimedDifficultyAttributes>();
if (!Beatmap.HitObjects.Any())
2021-10-05 07:59:54 +00:00
return attribs;
2021-09-30 08:00:15 +00:00
var skills = CreateSkills(Beatmap, playableMods, clockRate);
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
2021-09-30 08:00:15 +00:00
foreach (var hitObject in getDifficultyHitObjects())
{
progressiveBeatmap.HitObjects.Add(hitObject.BaseObject);
2019-02-21 04:12:37 +00:00
2021-09-30 08:00:15 +00:00
foreach (var skill in skills)
{
cancellationToken.ThrowIfCancellationRequested();
2022-05-22 20:45:27 +00:00
skill.Process(hitObject);
}
2021-09-30 08:00:15 +00:00
attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
2021-09-30 08:00:15 +00:00
}
2021-10-05 07:59:54 +00:00
return attribs;
2019-02-21 04:12:37 +00:00
}
/// <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>
2019-02-21 04:12:37 +00:00
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
2019-02-21 04:12:37 +00:00
{
2022-02-17 12:14:49 +00:00
var rulesetInstance = ruleset.CreateInstance();
2019-02-21 04:12:37 +00:00
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
2022-02-17 12:14:49 +00:00
Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
var finalCombination = ModUtils.FlattenMod(combination);
if (classicMod != null)
finalCombination = finalCombination.Append(classicMod);
yield return Calculate(finalCombination.ToArray(), cancellationToken);
2019-02-21 04:12:37 +00:00
}
2018-04-13 09:19:50 +00:00
}
2021-09-30 08:00:15 +00:00
/// <summary>
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
/// </summary>
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate));
2021-09-30 08:00:15 +00:00
/// <summary>
/// Performs required tasks before every calculation.
/// </summary>
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
2021-11-07 12:38:00 +00:00
/// <param name="cancellationToken">The cancellation token.</param>
private void preProcess([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
2021-09-30 08:00:15 +00:00
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
// Only pass through the cancellation token if it's non-default.
// This allows for the default timeout to be applied for playable beatmap construction.
Beatmap = cancellationToken == default
? beatmap.GetPlayableBeatmap(ruleset, playableMods)
: beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
2021-09-30 08:00:15 +00:00
var track = new TrackVirtual(10000);
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
2021-09-30 08:00:15 +00:00
clockRate = track.Rate;
2018-04-13 09:19:50 +00:00
}
2020-10-09 12:43:46 +00:00
/// <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);
2018-06-06 07:20:17 +00:00
/// <summary>
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
2018-06-06 07:20:17 +00:00
/// </summary>
public Mod[] CreateDifficultyAdjustmentModCombinations()
{
2020-10-14 10:31:31 +00:00
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
2018-06-06 07:20:17 +00:00
2020-10-14 10:31:31 +00:00
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
2018-06-06 07:20:17 +00:00
{
2020-10-14 10:31:31 +00:00
// Return the current set.
switch (currentSetCount)
{
case 0:
// Initial-case: Empty current set
2018-11-30 08:35:13 +00:00
yield return new ModNoMod();
2019-02-28 04:31:40 +00:00
break;
2019-04-01 03:44:46 +00:00
case 1:
yield return currentSet.Single();
2019-02-28 04:31:40 +00:00
break;
2019-04-01 03:44:46 +00:00
2018-07-17 07:33:08 +00:00
default:
yield return new MultiMod(currentSet.ToArray());
2019-02-28 04:31:40 +00:00
2018-07-17 07:33:08 +00:00
break;
}
2018-06-06 07:20:17 +00:00
2020-10-14 10:31:31 +00:00
// Apply the rest of the remaining mods recursively.
for (int i = 0; i < remainingMods.Length; i++)
2018-06-06 07:20:17 +00:00
{
(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)))
2018-06-06 07:20:17 +00:00
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())))
2018-06-06 07:20:17 +00:00
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))
2018-06-06 07:20:17 +00:00
yield return combo;
}
}
2020-10-14 10:31:31 +00:00
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
{
2020-10-14 10:31:31 +00:00
if (!(mod is MultiMod multi))
return (mod.Yield(), 1);
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
int count = 0;
2020-10-14 10:31:31 +00:00
foreach (var nested in multi.Mods)
{
(var nestedSet, int nestedCount) = flatten(nested);
2020-10-14 10:31:31 +00:00
set = set.Concat(nestedSet);
count += nestedCount;
}
2020-10-14 10:31:31 +00:00
return (set, count);
}
2018-06-06 07:20:17 +00:00
}
/// <summary>
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
2018-06-06 07:20:17 +00:00
/// </summary>
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
/// <summary>
2019-02-19 08:36:33 +00:00
/// 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>
2019-02-19 08:52:59 +00:00
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
/// <param name="skills">The skills which processed the beatmap.</param>
2019-02-19 05:29:23 +00:00
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
2019-02-19 08:36:33 +00:00
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
/// <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>
2019-02-19 05:29:23 +00:00
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
2019-02-19 05:29:23 +00:00
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
/// <summary>
2019-02-19 08:52:59 +00:00
/// 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>
/// <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);
2021-09-30 08:00:15 +00:00
2021-10-01 11:56:03 +00:00
/// <summary>
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
/// </summary>
2021-09-30 08:00:15 +00:00
private class ProgressiveCalculationBeatmap : IBeatmap
{
private readonly IBeatmap baseBeatmap;
public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap)
{
this.baseBeatmap = baseBeatmap;
}
2021-10-05 06:10:56 +00:00
public readonly List<HitObject> HitObjects = new List<HitObject>();
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
#region Delegated IBeatmap implementation
2021-09-30 08:00:15 +00:00
public BeatmapInfo BeatmapInfo
{
get => baseBeatmap.BeatmapInfo;
set => baseBeatmap.BeatmapInfo = value;
}
public ControlPointInfo ControlPointInfo
{
get => baseBeatmap.ControlPointInfo;
set => baseBeatmap.ControlPointInfo = value;
}
2021-10-05 06:10:56 +00:00
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
public BeatmapDifficulty Difficulty
{
get => baseBeatmap.Difficulty;
set => baseBeatmap.Difficulty = value;
}
2021-09-30 08:00:15 +00:00
public List<BreakPeriod> Breaks => baseBeatmap.Breaks;
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
2021-10-05 06:10:56 +00:00
#endregion
2021-09-30 08:00:15 +00:00
}
2018-04-13 09:19:50 +00:00
}
}