// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { private readonly Ruleset ruleset; private readonly WorkingBeatmap beatmap; protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { this.ruleset = ruleset; this.beatmap = beatmap; } /// /// Calculates the difficulty of the beatmap using a specific mod combination. /// /// The mods that should be applied to the beatmap. /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(params Mod[] mods) { mods = mods.Select(m => m.CreateCopy()).ToArray(); IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); var track = new TrackVirtual(10000); mods.OfType().ForEach(m => m.ApplyToTrack(track)); return calculate(playableBeatmap, mods, track.Rate); } /// /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// /// A collection of structures describing the difficulty of the beatmap for each mod combination. public IEnumerable CalculateAll() { foreach (var combination in CreateDifficultyAdjustmentModCombinations()) { if (combination is MultiMod multi) yield return Calculate(multi.Mods); else yield return Calculate(combination); } } private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) { var skills = CreateSkills(beatmap, mods); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList(); foreach (var hitObject in difficultyHitObjects) { foreach (var skill in skills) { skill.ProcessInternal(hitObject); } } return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); } /// /// 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, 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, 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. /// 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. /// Mods to calculate difficulty with. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); } }