1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 10:42:55 +08:00
Files
osu-lazer/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
T
Bartłomiej Dach 91f3be5fea Move BeatmapVersion from BeatmapInfo to IBeatmap
Closes https://github.com/ppy/osu/issues/32420.

The failure cause here is that in editor the beatmap version for the
beatmap affected (or... any beatmap, really), is 0 (ZERO). That is
probably a regression from https://github.com/ppy/osu/pull/32315, but
like... can we universally agree that calling that change "a regression"
in any capacity is dumb? Like what was that code *doing* playing dumb
reference games and copying stuff into an arbitrary instance that could
get or not get used later on? And now you have a 50/50 chance of
accessing the *correct* model's field, depending on whether you go via
`BeatmapInfo` or `Beatmap.BeatmapInfo`?

Moving the field to `IBeatmap`, i.e. what is by now - by consensus,
since https://github.com/ppy/osu/pull/28473 - supposed to be the "decoded
and materialised" beatmap, fixes this issue.

I probably should have done this as part of
https://github.com/ppy/osu/pull/28473 but it slipped my mind. Probably
for the better too because this change has rather large chances of
breaking stuff so maybe better to examine it in isolation (via diffcalc
runs or whatever).

For added humour points, you'd say that the field on `BeatmapInfo` was
not `[Ignore]`d, so this is a realm schema change, right? No. As far as
I can tell, it's not. I opened realm studio and `BeatmapVersion` *is not
a listed column` on `Beatmap` models.

I'm also not gonna get into the fact that I think `EditorBeatmap` doing
dumb games with juggling two `BeatmapInfo` references since
https://github.com/ppy/osu/pull/15075 is bad, because I don't think I
have the mental capacity to hotfix this by going down that train of
thought.
2025-03-17 15:04:48 +01:00

432 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.Audio.Track;
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; }
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)
{
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);
/// <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();
preProcess(mods, cancellationToken);
var skills = CreateSkills(Beatmap, playableMods, clockRate);
if (!Beatmap.HitObjects.Any())
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
foreach (var hitObject in getDifficultyHitObjects())
{
foreach (var skill in skills)
{
cancellationToken.ThrowIfCancellationRequested();
skill.Process(hitObject);
}
}
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
}
/// <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();
preProcess(mods, cancellationToken);
var attribs = new List<TimedDifficultyAttributes>();
if (!Beatmap.HitObjects.Any())
return attribs;
var skills = CreateSkills(Beatmap, playableMods, clockRate);
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, clockRate)));
}
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, clockRate));
/// <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 = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
var track = new TrackVirtual(10000);
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
clockRate = track.Rate;
}
/// <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>
/// <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);
/// <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>
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
/// <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>
/// <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);
/// <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
}
}
}