// 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.Extensions.IEnumerableExtensions; using osu.Framework.Timing; 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 { /// /// The length of each strain section. /// protected virtual int SectionLength => 400; 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 clock = new StopwatchClock(); mods.OfType().ForEach(m => m.ApplyToClock(clock)); return calculate(playableBeatmap, mods, clock.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); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList(); double sectionLength = SectionLength * clockRate; // The first object doesn't generate a strain, so we begin with an incremented section end double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength; foreach (DifficultyHitObject h in difficultyHitObjects) { while (h.BaseObject.StartTime > currentSectionEnd) { foreach (Skill s in skills) { s.SaveCurrentPeak(); s.StartNewSectionFrom(currentSectionEnd); } currentSectionEnd += sectionLength; } foreach (Skill s in skills) s.Process(h); } // The peak strain will not be saved for the last section in the above loop foreach (Skill s in skills) s.SaveCurrentPeak(); return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); } /// /// Creates all combinations which adjust the difficulty. /// public Mod[] CreateDifficultyAdjustmentModCombinations() { return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray(); IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) { 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 mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod // combinations in further recursions, so a moving subset is used to eliminate this effect for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) { var adjustmentMod = adjustmentSet[i]; if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)))) continue; foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1)) yield return combo; } } } /// /// 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. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap); } }