1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 13:27:25 +08:00

Implement new difficulty calculator structure

This commit is contained in:
smoogipoo 2019-02-12 16:01:25 +09:00
parent 66a5abe9bb
commit a8faa942a6
22 changed files with 430 additions and 64 deletions

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}

View File

@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(this, beatmap);
public override int? LegacyID => 2;

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
public class CatchLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private const double star_scaling_factor = 0.145;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
public CatchLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty
{
internal class ManiaDifficultyCalculator : DifficultyCalculator
internal class ManiaLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const double star_scaling_factor = 0.018;
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
public ManiaLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);

View File

@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(this, beatmap);
public override int? LegacyID => 3;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();
}

View File

@ -13,12 +13,12 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator
public class OsuLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
public OsuLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}

View File

@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuLegacyDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoLegacyDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
internal class TaikoDifficultyCalculator : DifficultyCalculator
internal class TaikoLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
private const double star_scaling_factor = 0.04125;
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
/// </summary>
private const double decay_weight = 0.9;
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
public TaikoLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}

View File

@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoLegacyDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestNoMods()
{
var combinations = new TestDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(1, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -24,7 +24,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestSingleMod()
{
var combinations = new TestDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(2, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestDoubleMod()
{
var combinations = new TestDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIncompatibleMods()
{
var combinations = new TestDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestDoubleIncompatibleMods()
{
var combinations = new TestDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(8, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -83,7 +83,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIncompatibleThroughBaseType()
{
var combinations = new TestDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
@ -136,9 +136,9 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
}
private class TestDifficultyCalculator : DifficultyCalculator
private class TestLegacyDifficultyCalculator : LegacyDifficultyCalculator
{
public TestDifficultyCalculator(params Mod[] mods)
public TestLegacyDifficultyCalculator(params Mod[] mods)
: base(null, null)
{
DifficultyAdjustmentMods = mods;

View File

@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
public override string Description => "dummy";

View File

@ -8,7 +8,13 @@ namespace osu.Game.Rulesets.Difficulty
public class DifficultyAttributes
{
public readonly Mod[] Mods;
public readonly double StarRating;
public double StarRating;
public DifficultyAttributes(Mod[] mods)
{
Mods = mods;
}
public DifficultyAttributes(Mod[] mods, double starRating)
{

View File

@ -1,56 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.
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;
namespace osu.Game.Rulesets.Difficulty
{
public abstract class DifficultyCalculator
public abstract class DifficultyCalculator : LegacyDifficultyCalculator
{
private readonly Ruleset ruleset;
private readonly WorkingBeatmap beatmap;
/// <summary>
/// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
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)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
beatmap.Mods.Value = mods;
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
var attributes = CreateDifficultyAttributes(mods);
var clock = new StopwatchClock();
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
if (!beatmap.HitObjects.Any())
return attributes;
return Calculate(playableBeatmap, mods, clock.Rate);
}
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, timeRate).OrderBy(h => h.BaseObject.StartTime).ToList();
var skills = CreateSkills();
/// <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())
double sectionLength = SectionLength * timeRate;
// 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)
{
if (combination is MultiMod multi)
yield return Calculate(multi.Mods);
else
yield return Calculate(combination);
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();
PopulateAttributes(attributes, beatmap, skills, timeRate);
return attributes;
}
/// <summary>
@ -96,12 +107,33 @@ namespace osu.Game.Rulesets.Difficulty
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
/// <summary>
/// Calculates the difficulty of a <see cref="Beatmap"/> using a specific <see cref="Mod"/> combination.
/// Populates <see cref="DifficultyAttributes"/> after difficulty has been processed.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to compute the difficulty for.</param>
/// <param name="mods">The <see cref="Mod"/>s that should be applied.</param>
/// <param name="attributes">The <see cref="DifficultyAttributes"/> to populate with information about the difficulty of <paramref name="beatmap"/>.</param>
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was processed.</param>
/// <param name="skills">The skills which processed the difficulty.</param>
/// <param name="timeRate">The rate of time in <paramref name="beatmap"/>.</param>
/// <returns>A structure containing the difficulty attributes.</returns>
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate);
protected abstract void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double timeRate);
/// <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="timeRate">The rate of time in <paramref name="beatmap"/>.</param>
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double timeRate);
/// <summary>
/// Creates the <see cref="Skill"/>s to calculate the difficulty of <see cref="DifficultyHitObject"/>s.
/// </summary>
/// <returns>The <see cref="Skill"/>s.</returns>
protected abstract Skill[] CreateSkills();
/// <summary>
/// Creates an empty <see cref="DifficultyAttributes"/>.
/// </summary>
/// <param name="mods">The <see cref="Mod"/>s which difficulty is being processed with.</param>
/// <returns>The empty <see cref="DifficultyAttributes"/>.</returns>
protected abstract DifficultyAttributes CreateDifficultyAttributes(Mod[] mods);
}
}

View File

@ -0,0 +1,107 @@
// 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.
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.Mods;
namespace osu.Game.Rulesets.Difficulty
{
public abstract class LegacyDifficultyCalculator
{
private readonly Ruleset ruleset;
private readonly WorkingBeatmap beatmap;
protected LegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
{
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)
{
beatmap.Mods.Value = mods;
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
var clock = new StopwatchClock();
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
return Calculate(playableBeatmap, mods, clock.Rate);
}
/// <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)
yield return Calculate(multi.Mods);
else
yield return Calculate(combination);
}
}
/// <summary>
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
/// </summary>
public Mod[] CreateDifficultyAdjustmentModCombinations()
{
return createDifficultyAdjustmentModCombinations(Enumerable.Empty<Mod>(), DifficultyAdjustmentMods).ToArray();
IEnumerable<Mod> createDifficultyAdjustmentModCombinations(IEnumerable<Mod> 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;
}
}
}
/// <summary>
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmap"/> difficulty.
/// </summary>
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
/// <summary>
/// Calculates the difficulty of a <see cref="Beatmap"/> using a specific <see cref="Mod"/> combination.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to compute the difficulty for.</param>
/// <param name="mods">The <see cref="Mod"/>s that should be applied.</param>
/// <param name="timeRate">The rate of time in <paramref name="beatmap"/>.</param>
/// <returns>A structure containing the difficulty attributes.</returns>
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate);
}
}

View File

@ -0,0 +1,32 @@
// 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.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Difficulty.Preprocessing
{
public class DifficultyHitObject
{
/// <summary>
/// Milliseconds elapsed since the <see cref="HitObject.StartTime"/> of the previous <see cref="DifficultyHitObject"/>.
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// The <see cref="HitObject"/> this <see cref="DifficultyHitObject"/> refers to.
/// </summary>
public readonly HitObject BaseObject;
/// <summary>
/// The previous <see cref="HitObject"/> to <see cref="BaseObject"/>.
/// </summary>
public readonly HitObject LastObject;
public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double timeRate)
{
BaseObject = hitObject;
LastObject = lastObject;
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / timeRate;
}
}
}

View File

@ -0,0 +1,103 @@
// 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.
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Rulesets.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// </summary>
public abstract class Skill
{
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// The weight by which each strain value decays.
/// </summary>
protected virtual double DecayWeight => 0.9;
/// <summary>
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
/// </summary>
protected readonly History<DifficultyHitObject> Previous = new History<DifficultyHitObject>(2); // Contained objects not used yet
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>();
/// <summary>
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
public void Process(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
Previous.Push(current);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// </summary>
public void SaveCurrentPeak()
{
if (Previous.Count > 0)
strainPeaks.Add(currentSectionPeak);
}
/// <summary>
/// Sets the initial strain level for a new section.
/// </summary>
/// <param name="offset">The beginning of the new section in milliseconds.</param>
public void StartNewSectionFrom(double offset)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
if (Previous.Count > 0)
currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
}
/// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
/// </summary>
public double DifficultyValue()
{
strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in strainPeaks)
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(DifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
}
}

View File

@ -0,0 +1,86 @@
// 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.
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
{
/// <summary>
/// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.
/// Indexing starts at the top of the stack.
/// </summary>
public class History<T> : IEnumerable<T>
{
public int Count { get; private set; }
private readonly T[] array;
private readonly int capacity;
private int marker; // Marks the position of the most recently added item.
/// <summary>
/// Initializes a new instance of the History class that is empty and has the specified capacity.
/// </summary>
/// <param name="capacity">The number of items the History can hold.</param>
public History(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException();
this.capacity = capacity;
array = new T[capacity];
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
}
/// <summary>
/// The most recently added item is returned at index 0.
/// </summary>
public T this[int i]
{
get
{
if (i < 0 || i > Count - 1)
throw new IndexOutOfRangeException();
i += marker;
if (i > capacity - 1)
i -= capacity;
return array[i];
}
}
/// <summary>
/// Adds the item as the most recent one in the history.
/// The oldest item is disposed if the history is full.
/// </summary>
public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition.
{
if (marker == 0)
marker = capacity - 1;
else
--marker;
array[marker] = item;
if (Count < capacity)
++Count;
}
/// <summary>
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
for (int i = marker; i < capacity; ++i)
yield return array[i];
if (Count == capacity)
for (int i = 0; i < marker; ++i)
yield return array[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets
/// <returns>The <see cref="IBeatmapProcessor"/>.</returns>
public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null;
public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
public abstract LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => null;

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Beatmaps
return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}");
}
protected abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
protected abstract LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
protected abstract Ruleset CreateRuleset();
}