Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring osu! difficulty calculation on par with osu!stable #2556

Merged
merged 8 commits into from May 15, 2018
12 changes: 8 additions & 4 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Expand Up @@ -36,18 +36,22 @@ public override double Calculate(Dictionary<string, double> categoryDifficulty =
new Speed()
};

double sectionEnd = section_length / TimeRate;
double sectionLength = section_length * TimeRate;

// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;

foreach (OsuDifficultyHitObject h in beatmap)
{
while (h.BaseObject.StartTime > sectionEnd)
while (h.BaseObject.StartTime > currentSectionEnd)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(sectionEnd);
s.StartNewSectionFrom(currentSectionEnd);
}

sectionEnd += section_length;
currentSectionEnd += sectionLength;
}

foreach (Skill s in skills)
Expand Down
Expand Up @@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
private readonly Queue<OsuDifficultyHitObject> onScreen = new Queue<OsuDifficultyHitObject>();

/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
Expand All @@ -30,65 +29,16 @@ public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)

/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// The inner loop adds objects that appear on screen into a queue until we need to hit the next object.
/// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen.
/// This means that we can loop through every object that is on screen at the time when a new one appears,
/// allowing us to determine a reading strain for the object that just appeared.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator()
{
while (true)
{
// Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued.
// This means there is always at least one object in the queue unless we reached the end of the map.
do
{
if (!difficultyObjects.MoveNext())
break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen.

OsuDifficultyHitObject latest = difficultyObjects.Current;
// Calculate flow values here

foreach (OsuDifficultyHitObject h in onScreen)
{
// ReSharper disable once PossibleNullReferenceException (resharper not smart enough to understand IEnumerator.MoveNext())
h.TimeUntilHit -= latest.DeltaTime;
// Calculate reading strain here
}

onScreen.Enqueue(latest);
}
while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one.

if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects.
yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared.
}
}

public IEnumerator<OsuDifficultyHitObject> GetEnumerator() => difficultyObjects;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
OsuHitObject[] triangle = new OsuHitObject[3];

// OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning.
if (objects.Count > 1)
{
triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle.
triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject.
}

// The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump.
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; ++i)
{
triangle[2] = triangle[1];
triangle[1] = triangle[0];
triangle[0] = objects[i];

yield return new OsuDifficultyHitObject(triangle, timeRate);
}
for (int i = 1; i < objects.Count; i++)
yield return new OsuDifficultyHitObject(objects[i], objects[i - 1], timeRate);
}
}
}
Expand Up @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public class OsuDifficultyHitObject
{
private const int normalized_radius = 52;

/// <summary>
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
/// </summary>
Expand All @@ -28,26 +30,19 @@ public class OsuDifficultyHitObject
/// </summary>
public double DeltaTime { get; private set; }

/// <summary>
/// Number of milliseconds until the <see cref="OsuDifficultyHitObject"/> has to be hit.
/// </summary>
public double TimeUntilHit { get; set; }

private const int normalized_radius = 52;

private readonly OsuHitObject lastObject;
private readonly double timeRate;

private readonly OsuHitObject[] t;

/// <summary>
/// Initializes the object calculating extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate)
public OsuDifficultyHitObject(OsuHitObject currentObject, OsuHitObject lastObject, double timeRate)
{
this.lastObject = lastObject;
this.timeRate = timeRate;

t = triangle;
BaseObject = t[0];
BaseObject = currentObject;

setDistances();
setTimingValues();
// Calculate angle here
Expand All @@ -63,10 +58,10 @@ private void setDistances()
scalingFactor *= 1 + smallCircleBonus;
}

Vector2 lastCursorPosition = t[1].StackedPosition;
Vector2 lastCursorPosition = lastObject.StackedPosition;
float lastTravelDistance = 0;

var lastSlider = t[1] as Slider;
var lastSlider = lastObject as Slider;
if (lastSlider != null)
{
computeSliderCursorPosition(lastSlider);
Expand All @@ -80,8 +75,7 @@ private void setDistances()
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate);
TimeUntilHit = 450; // BaseObject.PreEmpt;
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
}

private void computeSliderCursorPosition(Slider slider)
Expand All @@ -107,7 +101,8 @@ private void computeSliderCursorPosition(Slider slider)
}
});

var scoringTimes = slider.NestedHitObjects.Select(t => t.StartTime);
// Skip the head circle
var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime);
foreach (var time in scoringTimes)
computeVertex(time);
computeVertex(slider.EndTime);
Expand Down
4 changes: 2 additions & 2 deletions osu.Game/Rulesets/Mods/ModHardRock.cs
Expand Up @@ -21,8 +21,8 @@ public void ApplyToDifficulty(BeatmapDifficulty difficulty)
const float ratio = 1.4f;
difficulty.CircleSize *= 1.3f; // CS uses a custom 1.3 ratio.

This comment was marked as off-topic.

This comment was marked as off-topic.

difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ratio, 10.0f);
difficulty.DrainRate *= ratio;
difficulty.OverallDifficulty *= ratio;
difficulty.DrainRate = Math.Min(difficulty.DrainRate * ratio, 10.0f);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ratio, 10.0f);
}
}
}