1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-06 07:02:54 +08:00

Merge pull request #4277 from smoogipoo/diffcalc-merging-2

Implement new difficulty calculator structure
This commit is contained in:
Dean Herbert 2019-02-19 15:55:29 +09:00 committed by GitHub
commit b9c9c9ea21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 577 additions and 84 deletions

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, 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(); 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 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; public override int? LegacyID => 2;

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchDifficultyCalculator : DifficultyCalculator public class CatchLegacyDifficultyCalculator : LegacyDifficultyCalculator
{ {
/// <summary> /// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP. /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
@ -28,12 +28,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private const double star_scaling_factor = 0.145; private const double star_scaling_factor = 0.145;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public CatchLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return new CatchDifficultyAttributes(mods, 0); return new CatchDifficultyAttributes(mods, 0);
@ -59,12 +59,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate)) if (!calculateStrainValues(difficultyHitObjects, clockRate))
return new CatchDifficultyAttributes(mods, 0); return new CatchDifficultyAttributes(mods, 0);
// this is the same as osu!, so there's potential to share the implementation... maybe // this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor; double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, clockRate)) * star_scaling_factor;
return new CatchDifficultyAttributes(mods, starRating) return new CatchDifficultyAttributes(mods, starRating)
{ {

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, 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(); protected override Ruleset CreateRuleset() => new ManiaRuleset();
} }

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
{ {
internal class ManiaDifficultyCalculator : DifficultyCalculator internal class ManiaLegacyDifficultyCalculator : LegacyDifficultyCalculator
{ {
private const double star_scaling_factor = 0.018; private const double star_scaling_factor = 0.018;
@ -31,13 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public ManiaLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return new ManiaDifficultyAttributes(mods, 0); return new ManiaDifficultyAttributes(mods, 0);
@ -50,15 +50,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change // Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime)); difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate)) if (!calculateStrainValues(difficultyHitObjects, clockRate))
return new ManiaDifficultyAttributes(mods, 0); return new ManiaDifficultyAttributes(mods, 0);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; double starRating = calculateDifficulty(difficultyHitObjects, clockRate) * star_scaling_factor;
return new ManiaDifficultyAttributes(mods, starRating) return new ManiaDifficultyAttributes(mods, starRating)
{ {
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate
}; };
} }

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 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; public override int? LegacyID => 3;

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, 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(); protected override Ruleset CreateRuleset() => new OsuRuleset();
} }

View File

@ -13,29 +13,29 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuDifficultyCalculator : DifficultyCalculator public class OsuLegacyDifficultyCalculator : LegacyDifficultyCalculator
{ {
private const int section_length = 400; private const int section_length = 400;
private const double difficulty_multiplier = 0.0675; private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public OsuLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return new OsuDifficultyAttributes(mods, 0); return new OsuDifficultyAttributes(mods, 0);
OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), timeRate); OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), clockRate);
Skill[] skills = Skill[] skills =
{ {
new Aim(), new Aim(),
new Speed() new Speed()
}; };
double sectionLength = section_length * timeRate; double sectionLength = section_length * clockRate;
// The first object doesn't generate a strain, so we begin with an incremented section end // 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; double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate; double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
int maxCombo = beatmap.HitObjects.Count; int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)

View File

@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o }; 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); 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) public void Test(double expected, string name)
=> base.Test(expected, 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(); protected override Ruleset CreateRuleset() => new TaikoRuleset();
} }

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
internal class TaikoDifficultyCalculator : DifficultyCalculator internal class TaikoLegacyDifficultyCalculator : LegacyDifficultyCalculator
{ {
private const double star_scaling_factor = 0.04125; private const double star_scaling_factor = 0.04125;
@ -28,12 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
/// </summary> /// </summary>
private const double decay_weight = 0.9; private const double decay_weight = 0.9;
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public TaikoLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return new TaikoDifficultyAttributes(mods, 0); return new TaikoDifficultyAttributes(mods, 0);
@ -46,15 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate)) if (!calculateStrainValues(difficultyHitObjects, clockRate))
return new TaikoDifficultyAttributes(mods, 0); return new TaikoDifficultyAttributes(mods, 0);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; double starRating = calculateDifficulty(difficultyHitObjects, clockRate) * star_scaling_factor;
return new TaikoDifficultyAttributes(mods, starRating) return new TaikoDifficultyAttributes(mods, starRating)
{ {
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate, GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit) MaxCombo = beatmap.HitObjects.Count(h => h is Hit)
}; };
} }

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 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); 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] [Test]
public void TestNoMods() public void TestNoMods()
{ {
var combinations = new TestDifficultyCalculator().CreateDifficultyAdjustmentModCombinations(); var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(1, combinations.Length); Assert.AreEqual(1, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -24,7 +24,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestSingleMod() public void TestSingleMod()
{ {
var combinations = new TestDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations(); var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(2, combinations.Length); Assert.AreEqual(2, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestDoubleMod() 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.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestIncompatibleMods() 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.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestDoubleIncompatibleMods() 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.AreEqual(8, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -83,7 +83,7 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestIncompatibleThroughBaseType() 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.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod); Assert.IsTrue(combinations[0] is ModNoMod);
@ -136,9 +136,9 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) }; 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) : base(null, null)
{ {
DifficultyAdjustmentMods = mods; DifficultyAdjustmentMods = mods;
@ -146,7 +146,7 @@ namespace osu.Game.Tests.NonVisual
protected override Mod[] DifficultyAdjustmentMods { get; } protected override Mod[] DifficultyAdjustmentMods { get; }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => throw new NotImplementedException(); protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate) => throw new NotImplementedException();
} }
} }
} }

View File

@ -0,0 +1,115 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class LimitedCapacityStackTest
{
private const int capacity = 3;
private LimitedCapacityStack<int> stack;
[SetUp]
public void Setup()
{
stack = new LimitedCapacityStack<int>(capacity);
}
[Test]
public void TestEmptyStack()
{
Assert.AreEqual(0, stack.Count);
Assert.Throws<IndexOutOfRangeException>(() =>
{
int unused = stack[0];
});
int count = 0;
foreach (var unused in stack)
count++;
Assert.AreEqual(0, count);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void TestInRangeElements(int count)
{
// e.g. 0 -> 1 -> 2
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(count, stack.Count);
// e.g. 2 -> 1 -> 0 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<IndexOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestOverflowElements(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(capacity, stack.Count);
// e.g. 3 -> 2 -> 1 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<IndexOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestEnumerator(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
int enumeratorCount = 0;
int expectedValue = count - 1;
foreach (var item in stack)
{
Assert.AreEqual(expectedValue, item);
enumeratorCount++;
expectedValue--;
}
Assert.AreEqual(stack.Count, enumeratorCount);
}
}
}

View File

@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap }; 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"; public override string Description => "dummy";

View File

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

View File

@ -1,56 +1,68 @@
// 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Timing;
using osu.Game.Beatmaps; 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.Mods;
namespace osu.Game.Rulesets.Difficulty namespace osu.Game.Rulesets.Difficulty
{ {
public abstract class DifficultyCalculator public abstract class DifficultyCalculator : LegacyDifficultyCalculator
{ {
private readonly Ruleset ruleset; /// <summary>
private readonly WorkingBeatmap beatmap; /// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{ {
this.ruleset = ruleset;
this.beatmap = beatmap;
} }
/// <summary> protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
/// 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; var attributes = CreateDifficultyAttributes();
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo); attributes.Mods = mods;
var clock = new StopwatchClock(); if (!beatmap.HitObjects.Any())
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock)); return attributes;
return Calculate(playableBeatmap, mods, clock.Rate); var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
} var skills = CreateSkills();
/// <summary> double sectionLength = SectionLength * clockRate;
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary> // The first object doesn't generate a strain, so we begin with an incremented section end
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
public IEnumerable<DifficultyAttributes> CalculateAll()
{ foreach (DifficultyHitObject h in difficultyHitObjects)
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{ {
if (combination is MultiMod multi) while (h.BaseObject.StartTime > currentSectionEnd)
yield return Calculate(multi.Mods); {
else foreach (Skill s in skills)
yield return Calculate(combination); {
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, clockRate);
return attributes;
} }
/// <summary> /// <summary>
@ -96,12 +108,32 @@ namespace osu.Game.Rulesets.Difficulty
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>(); protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
/// <summary> /// <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> /// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to compute the difficulty for.</param> /// <param name="attributes">The <see cref="DifficultyAttributes"/> to populate with information about the difficulty of <paramref name="beatmap"/>.</param>
/// <param name="mods">The <see cref="Mod"/>s that should be applied.</param> /// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was processed.</param>
/// <param name="timeRate">The rate of time in <paramref name="beatmap"/>.</param> /// <param name="skills">The skills which processed the difficulty.</param>
/// <returns>A structure containing the difficulty attributes.</returns> /// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate); protected abstract void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double clockRate);
/// <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="clockRate">The rate at which the gameplay clock is run at.</param>
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
protected abstract IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate);
/// <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>
/// <returns>The empty <see cref="DifficultyAttributes"/>.</returns>
protected abstract DifficultyAttributes CreateDifficultyAttributes();
} }
} }

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="clockRate">The rate at which the gameplay clock is run at.</param>
/// <returns>A structure containing the difficulty attributes.</returns>
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate);
}
}

View File

@ -0,0 +1,41 @@
// 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
{
/// <summary>
/// Wraps a <see cref="HitObject"/> and provides additional information to be used for difficulty calculation.
/// </summary>
public class DifficultyHitObject
{
/// <summary>
/// The <see cref="HitObject"/> this <see cref="DifficultyHitObject"/> wraps.
/// </summary>
public readonly HitObject BaseObject;
/// <summary>
/// The last <see cref="HitObject"/> which occurs before <see cref="BaseObject"/>.
/// </summary>
public readonly HitObject LastObject;
/// <summary>
/// Amount of time elapsed between <see cref="BaseObject"/> and <see cref="LastObject"/>.
/// </summary>
public readonly double DeltaTime;
/// <summary>
/// Creates a new <see cref="DifficultyHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> which this <see cref="DifficultyHitObject"/> wraps.</param>
/// <param name="lastObject">The last <see cref="HitObject"/> which occurs before <paramref name="hitObject"/> in the beatmap.</param>
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
{
BaseObject = hitObject;
LastObject = lastObject;
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
}
}
}

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 LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<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,90 @@
// 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 limited depth. Indexing starts at the top of the stack.
/// </summary>
public class LimitedCapacityStack<T> : IEnumerable<T>
{
/// <summary>
/// The number of elements in the stack.
/// </summary>
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>
/// Constructs a new <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="capacity">The number of items the stack can hold.</param>
public LimitedCapacityStack(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>
/// Retrieves the item at an index in the stack.
/// </summary>
/// <param name="i">The index of the item to retrieve. The top of the stack is returned at index 0.</param>
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>
/// Pushes an item to this <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="item">The item to push.</param>
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> /// <returns>The <see cref="IBeatmapProcessor"/>.</returns>
public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null; 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; 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}"); 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(); protected abstract Ruleset CreateRuleset();
} }