mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 00:42:55 +08:00
Merge pull request #3564 from smoogipoo/tighten-diffcalc-tolerances
Make difficulty calculation closer match osu!stable
This commit is contained in:
commit
a1032a94bf
@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
{
|
{
|
||||||
public class OsuBeatmapProcessor : BeatmapProcessor
|
public class OsuBeatmapProcessor : BeatmapProcessor
|
||||||
{
|
{
|
||||||
|
private const int stack_distance = 3;
|
||||||
|
|
||||||
public OsuBeatmapProcessor(IBeatmap beatmap)
|
public OsuBeatmapProcessor(IBeatmap beatmap)
|
||||||
: base(beatmap)
|
: base(beatmap)
|
||||||
{
|
{
|
||||||
@ -18,17 +20,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
public override void PostProcess()
|
public override void PostProcess()
|
||||||
{
|
{
|
||||||
base.PostProcess();
|
base.PostProcess();
|
||||||
applyStacking((Beatmap<OsuHitObject>)Beatmap);
|
|
||||||
|
var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap;
|
||||||
|
|
||||||
|
// Reset stacking
|
||||||
|
foreach (var h in osuBeatmap.HitObjects)
|
||||||
|
h.StackHeight = 0;
|
||||||
|
|
||||||
|
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
||||||
|
applyStacking(osuBeatmap);
|
||||||
|
else
|
||||||
|
applyStackingOld(osuBeatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStacking(Beatmap<OsuHitObject> beatmap)
|
private void applyStacking(Beatmap<OsuHitObject> beatmap)
|
||||||
{
|
{
|
||||||
const int stack_distance = 3;
|
|
||||||
|
|
||||||
// Reset stacking
|
|
||||||
for (int i = 0; i <= beatmap.HitObjects.Count - 1; i++)
|
|
||||||
beatmap.HitObjects[i].StackHeight = 0;
|
|
||||||
|
|
||||||
// Extend the end index to include objects they are stacked on
|
// Extend the end index to include objects they are stacked on
|
||||||
int extendedEndIndex = beatmap.HitObjects.Count - 1;
|
int extendedEndIndex = beatmap.HitObjects.Count - 1;
|
||||||
for (int i = beatmap.HitObjects.Count - 1; i >= 0; i--)
|
for (int i = beatmap.HitObjects.Count - 1; i >= 0; i--)
|
||||||
@ -167,5 +173,40 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyStackingOld(Beatmap<OsuHitObject> beatmap)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < beatmap.HitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
OsuHitObject currHitObject = beatmap.HitObjects[i];
|
||||||
|
|
||||||
|
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
|
||||||
|
int sliderStack = 0;
|
||||||
|
|
||||||
|
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
|
||||||
|
{
|
||||||
|
double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency;
|
||||||
|
|
||||||
|
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||||
|
{
|
||||||
|
currHitObject.StackHeight++;
|
||||||
|
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
|
||||||
|
}
|
||||||
|
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
|
||||||
|
{
|
||||||
|
//Case for sliders - bump notes down and right, rather than up and left.
|
||||||
|
sliderStack++;
|
||||||
|
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
||||||
|
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double sectionLength = section_length * timeRate;
|
double sectionLength = section_length * timeRate;
|
||||||
|
|
||||||
// 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 = 2 * sectionLength;
|
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
|
||||||
|
|
||||||
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
|
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||||
@ -23,8 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
|
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
|
||||||
// This should probably happen before the objects reach the difficulty calculator.
|
// This should probably happen before the objects reach the difficulty calculator.
|
||||||
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
|
difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
|
||||||
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -21,15 +21,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
public OsuHitObject BaseObject { get; }
|
public OsuHitObject BaseObject { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalized distance from the <see cref="OsuHitObject.StackedPosition"/> of the previous <see cref="OsuDifficultyHitObject"/>.
|
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Distance { get; private set; }
|
public double JumpDistance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public double TravelDistance { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
|
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double DeltaTime { get; private set; }
|
public double DeltaTime { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
|
||||||
|
/// </summary>
|
||||||
|
public double StrainTime { get; private set; }
|
||||||
|
|
||||||
private readonly OsuHitObject lastObject;
|
private readonly OsuHitObject lastObject;
|
||||||
private readonly double timeRate;
|
private readonly double timeRate;
|
||||||
|
|
||||||
@ -51,31 +61,37 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
private void setDistances()
|
private void setDistances()
|
||||||
{
|
{
|
||||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
double scalingFactor = normalized_radius / BaseObject.Radius;
|
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
|
||||||
if (BaseObject.Radius < 30)
|
if (BaseObject.Radius < 30)
|
||||||
{
|
{
|
||||||
double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50;
|
float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50;
|
||||||
scalingFactor *= 1 + smallCircleBonus;
|
scalingFactor *= 1 + smallCircleBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector2 lastCursorPosition = lastObject.StackedPosition;
|
Vector2 lastCursorPosition = lastObject.StackedPosition;
|
||||||
float lastTravelDistance = 0;
|
|
||||||
|
|
||||||
var lastSlider = lastObject as Slider;
|
var lastSlider = lastObject as Slider;
|
||||||
if (lastSlider != null)
|
if (lastSlider != null)
|
||||||
{
|
{
|
||||||
computeSliderCursorPosition(lastSlider);
|
computeSliderCursorPosition(lastSlider);
|
||||||
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
|
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
|
||||||
lastTravelDistance = lastSlider.LazyTravelDistance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor;
|
// Don't need to jump to reach spinners
|
||||||
|
if (!(BaseObject is Spinner))
|
||||||
|
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||||
|
|
||||||
|
// Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also a slider!
|
||||||
|
if (BaseObject is Slider)
|
||||||
|
TravelDistance = (lastSlider?.LazyTravelDistance ?? 0) * scalingFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTimingValues()
|
private void setTimingValues()
|
||||||
{
|
{
|
||||||
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
|
DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
|
||||||
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
|
|
||||||
|
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||||
|
StrainTime = Math.Max(50, DeltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeSliderCursorPosition(Slider slider)
|
private void computeSliderCursorPosition(Slider slider)
|
||||||
@ -87,8 +103,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
float approxFollowCircleRadius = (float)(slider.Radius * 3);
|
float approxFollowCircleRadius = (float)(slider.Radius * 3);
|
||||||
var computeVertex = new Action<double>(t =>
|
var computeVertex = new Action<double>(t =>
|
||||||
{
|
{
|
||||||
|
double progress = ((int)t - (int)slider.StartTime) / (float)(int)slider.SpanDuration;
|
||||||
|
if (progress % 2 > 1)
|
||||||
|
progress = 1 - progress % 1;
|
||||||
|
else
|
||||||
|
progress = progress % 1;
|
||||||
|
|
||||||
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
|
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
|
||||||
var diff = slider.StackedPositionAt(t) - slider.LazyEndPosition.Value;
|
var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
|
||||||
float dist = diff.Length;
|
float dist = diff.Length;
|
||||||
|
|
||||||
if (dist > approxFollowCircleRadius)
|
if (dist > approxFollowCircleRadius)
|
||||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
protected override double SkillMultiplier => 26.25;
|
protected override double SkillMultiplier => 26.25;
|
||||||
protected override double StrainDecayBase => 0.15;
|
protected override double StrainDecayBase => 0.15;
|
||||||
|
|
||||||
protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime;
|
protected override double StrainValueOf(OsuDifficultyHitObject current)
|
||||||
|
=> (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override double StrainValueOf(OsuDifficultyHitObject current)
|
protected override double StrainValueOf(OsuDifficultyHitObject current)
|
||||||
{
|
{
|
||||||
double distance = current.Distance;
|
double distance = current.TravelDistance + current.JumpDistance;
|
||||||
|
|
||||||
double speedValue;
|
double speedValue;
|
||||||
if (distance > single_spacing_threshold)
|
if (distance > single_spacing_threshold)
|
||||||
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
else
|
else
|
||||||
speedValue = 0.95;
|
speedValue = 0.95;
|
||||||
|
|
||||||
return speedValue / current.DeltaTime;
|
return speedValue / current.StrainTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beat length at this control point.
|
/// The beat length at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double BeatLength
|
public virtual double BeatLength
|
||||||
{
|
{
|
||||||
get => beatLength;
|
get => beatLength;
|
||||||
set => beatLength = MathHelper.Clamp(value, 6, 60000);
|
set => beatLength = MathHelper.Clamp(value, 6, 60000);
|
||||||
|
@ -318,12 +318,12 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
if (timingChange)
|
if (timingChange)
|
||||||
{
|
{
|
||||||
handleTimingControlPoint(new TimingControlPoint
|
var controlPoint = CreateTimingControlPoint();
|
||||||
{
|
controlPoint.Time = time;
|
||||||
Time = time,
|
controlPoint.BeatLength = beatLength;
|
||||||
BeatLength = beatLength,
|
controlPoint.TimeSignature = timeSignature;
|
||||||
TimeSignature = timeSignature
|
|
||||||
});
|
handleTimingControlPoint(controlPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDifficultyControlPoint(new DifficultyControlPoint
|
handleDifficultyControlPoint(new DifficultyControlPoint
|
||||||
@ -418,6 +418,8 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
|
private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
|
||||||
|
|
||||||
|
protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint();
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
internal enum EffectFlags
|
internal enum EffectFlags
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps.Formats
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="LegacyBeatmapDecoder"/> built for difficulty calculation of legacy <see cref="Beatmap"/>s
|
||||||
|
/// <remarks>
|
||||||
|
/// To use this, the decoder must be registered by the application through <see cref="LegacyDifficultyCalculatorBeatmapDecoder.Register"/>.
|
||||||
|
/// Doing so will override any existing <see cref="Beatmap"/> decoders.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyDifficultyCalculatorBeatmapDecoder : LegacyBeatmapDecoder
|
||||||
|
{
|
||||||
|
public LegacyDifficultyCalculatorBeatmapDecoder(int version = LATEST_VERSION)
|
||||||
|
: base(version)
|
||||||
|
{
|
||||||
|
ApplyOffsets = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public new static void Register()
|
||||||
|
{
|
||||||
|
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last())));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TimingControlPoint CreateTimingControlPoint()
|
||||||
|
=> new LegacyDifficultyCalculatorControlPoint();
|
||||||
|
|
||||||
|
private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
|
||||||
|
{
|
||||||
|
public override double BeatLength { get; set; } = 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user