2019-02-12 15:01:25 +08:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
2019-01-24 16:43:03 +08:00
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2018-06-06 15:20:17 +08:00
|
|
|
using System;
|
2018-04-13 17:19:50 +08:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
2019-12-09 16:34:04 +08:00
|
|
|
using osu.Framework.Audio.Track;
|
2019-02-21 12:12:37 +08:00
|
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
2018-05-15 16:38:04 +08:00
|
|
|
using osu.Game.Beatmaps;
|
2019-02-12 15:01:25 +08:00
|
|
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
|
|
using osu.Game.Rulesets.Difficulty.Skills;
|
2018-05-15 16:38:04 +08:00
|
|
|
using osu.Game.Rulesets.Mods;
|
2019-02-21 12:12:37 +08:00
|
|
|
using osu.Game.Rulesets.Objects;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2018-05-15 16:38:04 +08:00
|
|
|
namespace osu.Game.Rulesets.Difficulty
|
2018-04-13 17:19:50 +08:00
|
|
|
{
|
2019-02-21 12:12:37 +08:00
|
|
|
public abstract class DifficultyCalculator
|
2018-04-13 17:19:50 +08:00
|
|
|
{
|
2019-02-12 15:01:25 +08:00
|
|
|
/// <summary>
|
|
|
|
/// The length of each strain section.
|
|
|
|
/// </summary>
|
|
|
|
protected virtual int SectionLength => 400;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
2019-02-21 12:12:37 +08:00
|
|
|
private readonly Ruleset ruleset;
|
|
|
|
private readonly WorkingBeatmap beatmap;
|
|
|
|
|
2018-06-14 14:32:07 +08:00
|
|
|
protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
2018-04-13 17:19:50 +08:00
|
|
|
{
|
2019-02-21 12:12:37 +08:00
|
|
|
this.ruleset = ruleset;
|
|
|
|
this.beatmap = beatmap;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <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>
|
|
|
|
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
|
|
|
public DifficultyAttributes Calculate(params Mod[] mods)
|
|
|
|
{
|
2019-03-14 22:39:45 +08:00
|
|
|
mods = mods.Select(m => m.CreateCopy()).ToArray();
|
|
|
|
|
2019-04-08 17:32:05 +08:00
|
|
|
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
2019-02-21 12:12:37 +08:00
|
|
|
|
2019-12-09 16:34:04 +08:00
|
|
|
var track = new TrackVirtual(10000);
|
|
|
|
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
2019-02-21 12:12:37 +08:00
|
|
|
|
2019-12-09 16:34:04 +08:00
|
|
|
return calculate(playableBeatmap, mods, track.Rate);
|
2019-02-21 12:12:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
|
|
|
public IEnumerable<DifficultyAttributes> CalculateAll()
|
|
|
|
{
|
|
|
|
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
|
|
|
{
|
|
|
|
if (combination is MultiMod multi)
|
2019-03-20 15:46:16 +08:00
|
|
|
yield return Calculate(multi.Mods);
|
2019-02-21 12:12:37 +08:00
|
|
|
else
|
2019-03-20 15:46:16 +08:00
|
|
|
yield return Calculate(combination);
|
2019-02-21 12:12:37 +08:00
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
}
|
|
|
|
|
2019-02-21 12:12:37 +08:00
|
|
|
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
2018-04-13 17:19:50 +08:00
|
|
|
{
|
2019-02-19 16:52:59 +08:00
|
|
|
var skills = CreateSkills(beatmap);
|
2018-06-14 14:32:07 +08:00
|
|
|
|
2019-02-12 15:01:25 +08:00
|
|
|
if (!beatmap.HitObjects.Any())
|
2019-02-19 16:36:33 +08:00
|
|
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
2018-06-14 14:32:07 +08:00
|
|
|
|
2020-10-09 20:43:46 +08:00
|
|
|
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
|
2018-06-14 14:32:07 +08:00
|
|
|
|
2019-02-19 13:29:23 +08:00
|
|
|
double sectionLength = SectionLength * clockRate;
|
2019-02-12 15:01:25 +08:00
|
|
|
|
|
|
|
// 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)
|
2018-06-14 14:32:07 +08:00
|
|
|
{
|
2019-02-12 15:01:25 +08:00
|
|
|
while (h.BaseObject.StartTime > currentSectionEnd)
|
|
|
|
{
|
|
|
|
foreach (Skill s in skills)
|
|
|
|
{
|
|
|
|
s.SaveCurrentPeak();
|
2021-02-19 15:04:25 +08:00
|
|
|
s.StartNewSectionFrom(currentSectionEnd / clockRate);
|
2019-02-12 15:01:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
currentSectionEnd += sectionLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (Skill s in skills)
|
|
|
|
s.Process(h);
|
2018-06-14 14:32:07 +08:00
|
|
|
}
|
2019-02-12 15:01:25 +08:00
|
|
|
|
|
|
|
// The peak strain will not be saved for the last section in the above loop
|
|
|
|
foreach (Skill s in skills)
|
|
|
|
s.SaveCurrentPeak();
|
|
|
|
|
2019-02-19 16:36:33 +08:00
|
|
|
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
2018-04-13 17:19:50 +08:00
|
|
|
}
|
|
|
|
|
2020-10-09 20:43:46 +08: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 15:20:17 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
|
|
|
|
/// </summary>
|
|
|
|
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
|
|
|
{
|
2020-10-14 18:31:31 +08:00
|
|
|
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
|
2018-06-06 15:20:17 +08:00
|
|
|
|
2020-10-14 18:31:31 +08:00
|
|
|
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
|
2018-06-06 15:20:17 +08:00
|
|
|
{
|
2020-10-14 18:31:31 +08:00
|
|
|
// Return the current set.
|
2018-07-17 13:35:09 +08:00
|
|
|
switch (currentSetCount)
|
|
|
|
{
|
|
|
|
case 0:
|
|
|
|
// Initial-case: Empty current set
|
2018-11-30 16:35:13 +08:00
|
|
|
yield return new ModNoMod();
|
2019-02-28 12:31:40 +08:00
|
|
|
|
2018-07-17 13:35:09 +08:00
|
|
|
break;
|
2019-04-01 11:44:46 +08:00
|
|
|
|
2018-07-17 13:35:09 +08:00
|
|
|
case 1:
|
|
|
|
yield return currentSet.Single();
|
2019-02-28 12:31:40 +08:00
|
|
|
|
2018-07-17 13:35:09 +08:00
|
|
|
break;
|
2019-04-01 11:44:46 +08:00
|
|
|
|
2018-07-17 15:33:08 +08:00
|
|
|
default:
|
|
|
|
yield return new MultiMod(currentSet.ToArray());
|
2019-02-28 12:31:40 +08:00
|
|
|
|
2018-07-17 15:33:08 +08:00
|
|
|
break;
|
2018-07-17 13:35:09 +08:00
|
|
|
}
|
2018-06-06 15:20:17 +08:00
|
|
|
|
2020-10-14 18:31:31 +08:00
|
|
|
// Apply the rest of the remaining mods recursively.
|
|
|
|
for (int i = 0; i < remainingMods.Length; i++)
|
2018-06-06 15:20:17 +08:00
|
|
|
{
|
2020-10-14 18:53:37 +08:00
|
|
|
var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
|
2020-10-14 18:03:11 +08:00
|
|
|
|
2020-10-14 18:53:37 +08:00
|
|
|
// 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 15:20:17 +08:00
|
|
|
continue;
|
|
|
|
|
2020-10-14 18:53:37 +08:00
|
|
|
// 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 15:20:17 +08:00
|
|
|
continue;
|
|
|
|
|
2020-10-14 18:53:37 +08:00
|
|
|
// 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 15:20:17 +08:00
|
|
|
yield return combo;
|
|
|
|
}
|
|
|
|
}
|
2020-10-14 18:03:11 +08:00
|
|
|
|
2020-10-14 18:31:31 +08:00
|
|
|
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
|
|
|
|
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
|
2020-10-14 18:03:11 +08:00
|
|
|
{
|
2020-10-14 18:31:31 +08:00
|
|
|
if (!(mod is MultiMod multi))
|
|
|
|
return (mod.Yield(), 1);
|
|
|
|
|
|
|
|
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
|
|
|
|
int count = 0;
|
2020-10-14 18:03:11 +08:00
|
|
|
|
2020-10-14 18:31:31 +08:00
|
|
|
foreach (var nested in multi.Mods)
|
|
|
|
{
|
|
|
|
var (nestedSet, nestedCount) = flatten(nested);
|
|
|
|
set = set.Concat(nestedSet);
|
|
|
|
count += nestedCount;
|
2020-10-14 18:03:11 +08:00
|
|
|
}
|
|
|
|
|
2020-10-14 18:31:31 +08:00
|
|
|
return (set, count);
|
2020-10-14 18:03:11 +08:00
|
|
|
}
|
2018-06-06 15:20:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmap"/> difficulty.
|
|
|
|
/// </summary>
|
|
|
|
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
|
|
|
|
2018-06-14 14:32:07 +08:00
|
|
|
/// <summary>
|
2019-02-19 16:36:33 +08:00
|
|
|
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
2019-02-12 15:01:25 +08:00
|
|
|
/// </summary>
|
2019-02-19 16:52:59 +08:00
|
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.</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>
|
2019-02-19 13:29:23 +08:00
|
|
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
2019-02-19 16:36:33 +08:00
|
|
|
protected abstract DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate);
|
2019-02-12 15:01:25 +08:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
|
2018-06-14 14:32:07 +08:00
|
|
|
/// </summary>
|
2019-02-12 15:01:25 +08:00
|
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> providing the <see cref="HitObject"/>s to enumerate.</param>
|
2019-02-19 13:29:23 +08:00
|
|
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
2019-02-12 15:01:25 +08:00
|
|
|
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
|
2019-02-19 13:29:23 +08:00
|
|
|
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
|
2019-02-12 15:01:25 +08:00
|
|
|
|
|
|
|
/// <summary>
|
2019-02-19 16:52:59 +08:00
|
|
|
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
2019-02-12 15:01:25 +08:00
|
|
|
/// </summary>
|
2019-04-25 16:36:17 +08:00
|
|
|
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
|
2019-02-12 15:01:25 +08:00
|
|
|
/// <returns>The <see cref="Skill"/>s.</returns>
|
2019-02-19 16:52:59 +08:00
|
|
|
protected abstract Skill[] CreateSkills(IBeatmap beatmap);
|
2018-04-13 17:19:50 +08:00
|
|
|
}
|
|
|
|
}
|