// Copyright (c) ppy Pty Ltd . 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.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 { /// /// The beatmap for which difficulty will be calculated. /// protected IBeatmap Beatmap { get; private set; } private Mod[] playableMods; private double clockRate; private readonly IRulesetInfo ruleset; private readonly IWorkingBeatmap beatmap; /// /// A yymmdd version which is used to discern when reprocessing is required. /// public virtual int Version => 0; protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { this.ruleset = ruleset; this.beatmap = beatmap; } /// /// Calculates the difficulty of the beatmap with no mods applied. /// /// The cancellation token. /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(CancellationToken cancellationToken = default) => Calculate(Array.Empty(), cancellationToken); /// /// Calculates the difficulty of the beatmap using a specific mod combination. /// /// The mods that should be applied to the beatmap. /// The cancellation token. /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { 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); } /// /// Calculates the difficulty of the beatmap with no mods applied and returns a set of representing the difficulty at every relevant time value in the beatmap. /// /// The cancellation token. /// The set of . public List CalculateTimed(CancellationToken cancellationToken = default) => CalculateTimed(Array.Empty(), cancellationToken); /// /// Calculates the difficulty of the beatmap using a specific mod combination and returns a set of representing the difficulty at every relevant time value in the beatmap. /// /// The mods that should be applied to the beatmap. /// The cancellation token. /// The set of . public List CalculateTimed([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); var attribs = new List(); if (!Beatmap.HitObjects.Any()) return attribs; var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); var difficultyObjects = getDifficultyHitObjects().ToArray(); foreach (var obj in difficultyObjects) { // Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap. // At the same time, we also need to consider the possibility DHOs may not be generated for any given object, // so we'll add all remaining objects up to the current point in time to the progressive beatmap. for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++) { if (obj != difficultyObjects[^1] && Beatmap.HitObjects[i].StartTime > obj.BaseObject.StartTime) break; progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]); } foreach (var skill in skills) { cancellationToken.ThrowIfCancellationRequested(); skill.Process(obj); } attribs.Add(new TimedDifficultyAttributes(obj.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } return attribs; } /// /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// /// /// This can only be used to compute difficulties for legacy mod combinations. /// /// A collection of structures describing the difficulty of the beatmap for each mod combination. public IEnumerable CalculateAllLegacyCombinations(CancellationToken cancellationToken = default) { var rulesetInstance = ruleset.CreateInstance(); foreach (var combination in CreateDifficultyAdjustmentModCombinations()) { Mod classicMod = rulesetInstance.CreateMod(); var finalCombination = ModUtils.FlattenMod(combination); if (classicMod != null) finalCombination = finalCombination.Append(classicMod); yield return Calculate(finalCombination.ToArray(), cancellationToken); } } /// /// Retrieves the s to calculate against. /// private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate)); /// /// Performs required tasks before every calculation. /// /// The original list of s. /// The cancellation token. private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { 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); var track = new TrackVirtual(10000); playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); clockRate = track.Rate; } /// /// Sorts a given set of s. /// /// The s to sort. /// The sorted s. protected virtual IEnumerable SortObjects(IEnumerable input) => input.OrderBy(h => h.BaseObject.StartTime); /// /// Creates all combinations which adjust the difficulty. /// public Mod[] CreateDifficultyAdjustmentModCombinations() { return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty()).ToArray(); static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory remainingMods, IEnumerable 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 static (IEnumerable set, int count) flatten(Mod mod) { if (!(mod is MultiMod multi)) return (mod.Yield(), 1); IEnumerable set = Enumerable.Empty(); int count = 0; foreach (var nested in multi.Mods) { (var nestedSet, int nestedCount) = flatten(nested); set = set.Concat(nestedSet); count += nestedCount; } return (set, count); } } /// /// Retrieves all s which adjust the difficulty. /// protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. /// /// The whose difficulty was calculated. /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. /// The rate at which the gameplay clock is run at. protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate); /// /// Enumerates s to be processed from s in the . /// /// The providing the s to enumerate. /// The rate at which the gameplay clock is run at. /// The enumerated s. protected abstract IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate); /// /// Creates the s to calculate the difficulty of an . /// /// The whose difficulty will be calculated. /// This may differ from in the case of timed calculation. /// Mods to calculate difficulty with. /// Clockrate to calculate difficulty with. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); /// /// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time. /// private class ProgressiveCalculationBeatmap : IBeatmap { private readonly IBeatmap baseBeatmap; public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap) { this.baseBeatmap = baseBeatmap; } public readonly List HitObjects = new List(); IReadOnlyList 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 List Breaks => baseBeatmap.Breaks; public List UnhandledEventLines => baseBeatmap.UnhandledEventLines; public double TotalBreakTime => baseBeatmap.TotalBreakTime; public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); 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; } #endregion } } }