diff --git a/osu.Game.Rulesets.Mania/Difficulty/Evaluators/IndividualStrainEvaluator.cs b/osu.Game.Rulesets.Mania/Difficulty/Evaluators/IndividualStrainEvaluator.cs new file mode 100644 index 0000000000..297beb2840 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Evaluators/IndividualStrainEvaluator.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Utils; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators +{ + public class IndividualStrainEvaluator + { + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + var maniaCurrent = (ManiaDifficultyHitObject)current; + double startTime = maniaCurrent.StartTime; + double endTime = maniaCurrent.EndTime; + + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + + // We award a bonus if this note starts and ends before the end of another hold note. + foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects) + { + if (maniaPrevious is null) + continue; + + if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) && + Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1)) + { + holdFactor = 1.25; + break; + } + } + + return 2.0 * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Evaluators/OverallStrainEvaluator.cs b/osu.Game.Rulesets.Mania/Difficulty/Evaluators/OverallStrainEvaluator.cs new file mode 100644 index 0000000000..97782f7644 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Evaluators/OverallStrainEvaluator.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Utils; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators +{ + public class OverallStrainEvaluator + { + private const double release_threshold = 30; + + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + var maniaCurrent = (ManiaDifficultyHitObject)current; + double startTime = maniaCurrent.StartTime; + double endTime = maniaCurrent.EndTime; + bool isOverlapping = false; + + double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + + foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects) + { + if (maniaPrevious is null) + continue; + + // The current note is overlapped if a previous note or end is overlapping the current note body + isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) && + Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) && + Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1); + + // We give a slight bonus to everything if something is held meanwhile + if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) && + Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1)) + holdFactor = 1.25; + + closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime)); + } + + // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. + // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. + // holdAddition + // ^ + // 1.0 + - - - - - -+----------- + // | / + // 0.5 + - - - - -/ Sigmoid Curve + // | /| + // 0.0 +--------+-+---------------> Release Difference / ms + // release_threshold + if (isOverlapping) + holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); + + return (1 + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 06b8018f2b..bcf16e6808 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { var sortedObjects = beatmap.HitObjects.ToArray(); + int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns; LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime))); List objects = new List(); + List[] perColumnObjects = new List[totalColumns]; + + for (int column = 0; column < totalColumns; column++) + perColumnObjects[column] = new List(); for (int i = 1; i < sortedObjects.Length; i++) - objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count)); + { + var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count); + objects.Add(currentObject); + perColumnObjects[currentObject.Column].Add(currentObject); + } return objects; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs index a67d38b29f..91b6a2b861 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs @@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing { public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject; - public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, int index) + private readonly List[] perColumnObjects; + + private readonly int columnIndex; + + public readonly int Column; + + // The hit object earlier in time than this note in each column + public readonly ManiaDifficultyHitObject?[] PreviousHitObjects; + + public readonly double ColumnStrainTime; + + public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List[] perColumnObjects, int index) : base(hitObject, lastObject, clockRate, objects, index) { + int totalColumns = perColumnObjects.Length; + this.perColumnObjects = perColumnObjects; + Column = BaseObject.Column; + columnIndex = perColumnObjects[Column].Count; + PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns]; + ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime; + + if (index > 0) + { + ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1]; + + for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++) + PreviousHitObjects[i] = prevNote.PreviousHitObjects[i]; + + // intentionally depends on processing order to match live. + PreviousHitObjects[prevNote.Column] = prevNote; + } + } + + /// + /// The previous object in the same column as this , exclusive of Long Note tails. + /// + /// The number of notes to go back. + /// The object in this column notes back, or null if this is the first note in the column. + public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex) + { + int index = columnIndex - (backwardsIndex + 1); + return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null; + } + + /// + /// The next object in the same column as this , exclusive of Long Note tails. + /// + /// The number of notes to go forward. + /// The object in this column notes forward, or null if this is the last note in the column. + public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex) + { + int index = columnIndex + (forwardsIndex + 1); + return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index bb4261ea13..037b7e3511 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mania.Difficulty.Evaluators; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; @@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; - private const double release_threshold = 30; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; - private readonly double[] startTimes; - private readonly double[] endTimes; private readonly double[] individualStrains; - - private double individualStrain; + private double highestIndividualStrain; private double overallStrain; public Strain(Mod[] mods, int totalColumns) : base(mods) { - startTimes = new double[totalColumns]; - endTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; overallStrain = 1; } @@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var maniaCurrent = (ManiaDifficultyHitObject)current; - double startTime = maniaCurrent.StartTime; - double endTime = maniaCurrent.EndTime; - int column = maniaCurrent.BaseObject.Column; - bool isOverlapping = false; - double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information - double holdFactor = 1.0; // Factor to all additional strains in case something else is held - double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base); + individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current); - for (int i = 0; i < endTimes.Length; ++i) - { - // The current note is overlapped if a previous note or end is overlapping the current note body - isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && - Precision.DefinitelyBigger(endTime, endTimes[i], 1) && - Precision.DefinitelyBigger(startTime, startTimes[i], 1); + // Take the hardest individualStrain for notes that happen at the same time (in a chord). + // This is to ensure the order in which the notes are processed does not affect the resultant total strain. + highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column]; - // We give a slight bonus to everything if something is held meanwhile - if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) && - Precision.DefinitelyBigger(startTime, startTimes[i], 1)) - holdFactor = 1.25; - - closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i])); - } - - // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. - // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. - // holdAddition - // ^ - // 1.0 + - - - - - -+----------- - // | / - // 0.5 + - - - - -/ Sigmoid Curve - // | /| - // 0.0 +--------+-+---------------> Release Difference / ms - // release_threshold - if (isOverlapping) - holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); - - // Decay and increase individualStrains in own column - individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); - individualStrains[column] += 2.0 * holdFactor; - - // For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns - individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column]; - - // Decay and increase overallStrain - overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base); - overallStrain += (1 + holdAddition) * holdFactor; - - // Update startTimes and endTimes arrays - startTimes[column] = startTime; - endTimes[column] = endTime; + overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base); + overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current); // By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section. - return individualStrain + overallStrain - CurrentStrain; + return highestIndividualStrain + overallStrain - CurrentStrain; } - protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) - => applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base) - + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base); + protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) => + applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base) + + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000);