1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 22:22:54 +08:00

Merge branch 'diffcalc-merging-2' into new-diffcalc-osu

# Conflicts:
#	osu.Game.Rulesets.Osu/Difficulty/OsuLegacyDifficultyCalculator.cs
This commit is contained in:
smoogipoo 2019-02-19 14:33:34 +09:00
commit 0001b6204b
11 changed files with 176 additions and 49 deletions

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
}
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{
if (!beatmap.HitObjects.Any())
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));
if (!calculateStrainValues(difficultyHitObjects, timeRate))
if (!calculateStrainValues(difficultyHitObjects, clockRate))
return new CatchDifficultyAttributes(mods, 0);
// 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 starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor;
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, clockRate)) * star_scaling_factor;
return new CatchDifficultyAttributes(mods, starRating)
{

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
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())
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
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);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
double starRating = calculateDifficulty(difficultyHitObjects, clockRate) * star_scaling_factor;
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
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate
};
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{
if (!beatmap.HitObjects.Any())
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.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate))
if (!calculateStrainValues(difficultyHitObjects, clockRate))
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)
{
// 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)
};
}

View File

@ -146,7 +146,7 @@ namespace osu.Game.Tests.NonVisual
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

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

View File

@ -23,17 +23,18 @@ namespace osu.Game.Rulesets.Difficulty
{
}
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{
var attributes = CreateDifficultyAttributes(mods);
var attributes = CreateDifficultyAttributes();
attributes.Mods = mods;
if (!beatmap.HitObjects.Any())
return attributes;
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, timeRate).OrderBy(h => h.BaseObject.StartTime).ToList();
var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
var skills = CreateSkills();
double sectionLength = SectionLength * timeRate;
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;
@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Difficulty
foreach (Skill s in skills)
s.SaveCurrentPeak();
PopulateAttributes(attributes, beatmap, skills, timeRate);
PopulateAttributes(attributes, beatmap, skills, clockRate);
return attributes;
}
@ -112,16 +113,16 @@ namespace osu.Game.Rulesets.Difficulty
/// <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>
protected abstract void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double timeRate);
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
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="timeRate">The rate of time in <paramref name="beatmap"/>.</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 timeRate);
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.
@ -132,8 +133,7 @@ namespace osu.Game.Rulesets.Difficulty
/// <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);
protected abstract DifficultyAttributes CreateDifficultyAttributes();
}
}

View File

@ -100,8 +100,8 @@ namespace osu.Game.Rulesets.Difficulty
/// </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>
/// <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 timeRate);
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate);
}
}

View File

@ -5,28 +5,37 @@ 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>
/// 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.
/// The <see cref="HitObject"/> this <see cref="DifficultyHitObject"/> wraps.
/// </summary>
public readonly HitObject BaseObject;
/// <summary>
/// The previous <see cref="HitObject"/> to <see cref="BaseObject"/>.
/// The last <see cref="HitObject"/> which occurs before <see cref="BaseObject"/>.
/// </summary>
public readonly HitObject LastObject;
public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double timeRate)
/// <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) / timeRate;
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
}
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <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
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.

View File

@ -8,11 +8,13 @@ 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.
/// An indexed stack with limited depth. Indexing starts at the top of the stack.
/// </summary>
public class History<T> : IEnumerable<T>
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;
@ -20,10 +22,10 @@ namespace osu.Game.Rulesets.Difficulty.Utils
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.
/// Constructs a new <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="capacity">The number of items the History can hold.</param>
public History(int capacity)
/// <param name="capacity">The number of items the stack can hold.</param>
public LimitedCapacityStack(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException();
@ -34,8 +36,9 @@ namespace osu.Game.Rulesets.Difficulty.Utils
}
/// <summary>
/// The most recently added item is returned at index 0.
/// 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
@ -52,11 +55,12 @@ namespace osu.Game.Rulesets.Difficulty.Utils
}
/// <summary>
/// Adds the item as the most recent one in the history.
/// The oldest item is disposed if the history is full.
/// Pushes an item to this <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition.
/// <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