diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index ef660b9ea8..61ee322ce2 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - [TestCase(2.2676066895468976, "diffcalc-test")] + [TestCase(2.3683365342338796d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); - protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(new ManiaRuleset(), beatmap); + protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap); protected override Ruleset CreateRuleset() => new ManiaRuleset(); } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index 2f614ea14b..3ff665d2c8 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -2,17 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { public double GreatHitWindow; - - public ManiaDifficultyAttributes(Mod[] mods, double starRating) - : base(mods, starRating) - { - } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs new file mode 100644 index 0000000000..59fed1031f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mania.Difficulty.Skills; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + public class ManiaDifficultyCalculator : DifficultyCalculator + { + private const double star_scaling_factor = 0.018; + + private readonly bool isForCurrentRuleset; + + public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + { + if (beatmap.HitObjects.Count == 0) + return new ManiaDifficultyAttributes { Mods = mods }; + + return new ManiaDifficultyAttributes + { + StarRating = difficultyValue(skills) * star_scaling_factor, + Mods = mods, + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future + GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate, + }; + } + + private double difficultyValue(Skill[] skills) + { + // Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section + var overall = skills.OfType().Single(); + var aggregatePeaks = new List(Enumerable.Repeat(0.0, overall.StrainPeaks.Count)); + + foreach (var individual in skills.OfType()) + { + for (int i = 0; i < individual.StrainPeaks.Count; i++) + { + double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i]; + + if (aggregate > aggregatePeaks[i]) + aggregatePeaks[i] = aggregate; + } + } + + aggregatePeaks.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 aggregatePeaks) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) + { + for (int i = 1; i < beatmap.HitObjects.Count; i++) + yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); + } + + protected override Skill[] CreateSkills(IBeatmap beatmap) + { + int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; + + var skills = new List { new Overall(columnCount) }; + + for (int i = 0; i < columnCount; i++) + skills.Add(new Individual(i, columnCount)); + + return skills.ToArray(); + } + + protected override Mod[] DifficultyAdjustmentMods + { + get + { + var mods = new Mod[] + { + new ManiaModDoubleTime(), + new ManiaModHalfTime(), + new ManiaModEasy(), + new ManiaModHardRock(), + }; + + if (isForCurrentRuleset) + return mods; + + // if we are a convert, we can be played in any key mod. + return mods.Concat(new Mod[] + { + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3(), + new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), + }).ToArray(); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs deleted file mode 100644 index 833454bf32..0000000000 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyDifficultyCalculator.cs +++ /dev/null @@ -1,173 +0,0 @@ -// 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 System.Collections.Generic; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Mania.Difficulty -{ - internal class ManiaLegacyDifficultyCalculator : LegacyDifficultyCalculator - { - private const double star_scaling_factor = 0.018; - - /// - /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size strain_step. - /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. - /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. - /// - private const double strain_step = 400; - - /// - /// The weighting of each strain value decays to this number * it's previous value - /// - private const double decay_weight = 0.9; - - private readonly bool isForCurrentRuleset; - - public ManiaLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) - : base(ruleset, beatmap) - { - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); - } - - protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate) - { - if (!beatmap.HitObjects.Any()) - return new ManiaDifficultyAttributes(mods, 0); - - var difficultyHitObjects = new List(); - - int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; - - // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. - // 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, clockRate)) - return new ManiaDifficultyAttributes(mods, 0); - - 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) / clockRate - }; - } - - private bool calculateStrainValues(List objects, double timeRate) - { - // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. - using (var hitObjectsEnumerator = objects.GetEnumerator()) - { - if (!hitObjectsEnumerator.MoveNext()) - return false; - - ManiaHitObjectDifficulty current = hitObjectsEnumerator.Current; - - // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject. - while (hitObjectsEnumerator.MoveNext()) - { - var next = hitObjectsEnumerator.Current; - next?.CalculateStrains(current, timeRate); - current = next; - } - - return true; - } - } - - private double calculateDifficulty(List objects, double timeRate) - { - double actualStrainStep = strain_step * timeRate; - - // Find the highest strain value within each strain step - List highestStrains = new List(); - double intervalEndTime = actualStrainStep; - double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval - - ManiaHitObjectDifficulty previousHitObject = null; - foreach (var hitObject in objects) - { - // While we are beyond the current interval push the currently available maximum to our strain list - while (hitObject.BaseHitObject.StartTime > intervalEndTime) - { - highestStrains.Add(maximumStrain); - - // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay - // until the beginning of the next interval. - if (previousHitObject == null) - { - maximumStrain = 0; - } - else - { - double individualDecay = Math.Pow(ManiaHitObjectDifficulty.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); - double overallDecay = Math.Pow(ManiaHitObjectDifficulty.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); - maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay; - } - - // Go to the next time interval - intervalEndTime += actualStrainStep; - } - - // Obtain maximum strain - double strain = hitObject.IndividualStrain + hitObject.OverallStrain; - maximumStrain = Math.Max(strain, maximumStrain); - - previousHitObject = hitObject; - } - - // Build the weighted sum over the highest strains for each interval - double difficulty = 0; - double weight = 1; - highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. - - foreach (double strain in highestStrains) - { - difficulty += weight * strain; - weight *= decay_weight; - } - - return difficulty; - } - - protected override Mod[] DifficultyAdjustmentMods - { - get - { - var mods = new Mod[] - { - new ManiaModDoubleTime(), - new ManiaModHalfTime(), - new ManiaModEasy(), - new ManiaModHardRock(), - }; - - if (isForCurrentRuleset) - return mods; - - // if we are a convert, we can be played in any key mod. - return mods.Concat(new Mod[] - { - new ManiaModKey1(), - new ManiaModKey2(), - new ManiaModKey3(), - new ManiaModKey4(), - new ManiaModKey5(), - new ManiaModKey6(), - new ManiaModKey7(), - new ManiaModKey8(), - new ManiaModKey9(), - }).ToArray(); - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs new file mode 100644 index 0000000000..29ba934e9f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing +{ + public class ManiaDifficultyHitObject : DifficultyHitObject + { + public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject; + + public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) + : base(hitObject, lastObject, clockRate) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs new file mode 100644 index 0000000000..059cd39641 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mania.Objects; + +namespace osu.Game.Rulesets.Mania.Difficulty.Skills +{ + public class Individual : Skill + { + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.125; + + private readonly double[] holdEndTimes; + + private readonly int column; + + public Individual(int column, int columnCount) + { + this.column = column; + + holdEndTimes = new double[columnCount]; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + var maniaCurrent = (ManiaDifficultyHitObject)current; + var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime; + + try + { + if (maniaCurrent.BaseObject.Column != column) + return 0; + + // We give a slight bonus if something is held meanwhile + return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2; + } + finally + { + holdEndTimes[maniaCurrent.BaseObject.Column] = endTime; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs new file mode 100644 index 0000000000..ed25173d38 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mania.Objects; + +namespace osu.Game.Rulesets.Mania.Difficulty.Skills +{ + public class Overall : Skill + { + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.3; + + private readonly double[] holdEndTimes; + + private readonly int columnCount; + + public Overall(int columnCount) + { + this.columnCount = columnCount; + + holdEndTimes = new double[columnCount]; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + var maniaCurrent = (ManiaDifficultyHitObject)current; + var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime; + + double holdFactor = 1.0; // Factor 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 + + for (int i = 0; i < columnCount; i++) + { + // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... + if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i]) + holdAddition = 1.0; + + // ... this addition only is valid if there is _no_ other note with the same ending. + // Releasing multiple notes at the same time is just as easy as releasing one + if (endTime == holdEndTimes[i]) + holdAddition = 0; + + // We give a slight bonus if something is held meanwhile + if (holdEndTimes[i] > endTime) + holdFactor = 1.25; + } + + holdEndTimes[maniaCurrent.BaseObject.Column] = endTime; + + return (1 + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 7a2a539a9d..d86ee19802 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaLegacyDifficultyCalculator(this, beatmap); + public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap); public override int? LegacyID => 3; diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs deleted file mode 100644 index b6ea8c8665..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Types; -using System; - -namespace osu.Game.Rulesets.Mania.Objects -{ - internal class ManiaHitObjectDifficulty - { - /// - /// Factor by how much individual / overall strain decays per second. - /// - /// - /// These values are results of tweaking a lot and taking into account general feedback. - /// - internal const double INDIVIDUAL_DECAY_BASE = 0.125; - internal const double OVERALL_DECAY_BASE = 0.30; - - internal ManiaHitObject BaseHitObject; - - private readonly int beatmapColumnCount; - - private readonly double endTime; - private readonly double[] heldUntil; - - /// - /// Measures jacks or more generally: repeated presses of the same button - /// - private readonly double[] individualStrains; - - internal double IndividualStrain - { - get - { - return individualStrains[BaseHitObject.Column]; - } - - set - { - individualStrains[BaseHitObject.Column] = value; - } - } - - /// - /// Measures note density in a way - /// - internal double OverallStrain = 1; - - public ManiaHitObjectDifficulty(ManiaHitObject baseHitObject, int columnCount) - { - BaseHitObject = baseHitObject; - - endTime = (baseHitObject as IHasEndTime)?.EndTime ?? baseHitObject.StartTime; - - beatmapColumnCount = columnCount; - heldUntil = new double[beatmapColumnCount]; - individualStrains = new double[beatmapColumnCount]; - - for (int i = 0; i < beatmapColumnCount; ++i) - { - individualStrains[i] = 0; - heldUntil[i] = 0; - } - } - - internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate) - { - // TODO: Factor in holds - double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; - double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000); - double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000); - - 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 - - // Fill up the heldUntil array - for (int i = 0; i < beatmapColumnCount; ++i) - { - heldUntil[i] = previousHitObject.heldUntil[i]; - - // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... - if (BaseHitObject.StartTime < heldUntil[i] && endTime > heldUntil[i]) - { - holdAddition = 1.0; - } - - // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 - if (endTime == heldUntil[i]) - { - holdAddition = 0; - } - - // We give a slight bonus to everything if something is held meanwhile - if (heldUntil[i] > endTime) - { - holdFactor = 1.25; - } - - // Decay individual strains - individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay; - } - - heldUntil[BaseHitObject.Column] = endTime; - - // Increase individual strain in own column - IndividualStrain += 2.0 * holdFactor; - - OverallStrain = previousHitObject.OverallStrain * overallDecay + (1.0 + holdAddition) * holdFactor; - } - } -} diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 72bb686260..e8020ed185 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -14,6 +14,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public abstract class Skill { + /// + /// The peak strain for each section of the beatmap. + /// + public IList StrainPeaks => strainPeaks; + /// /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. /// @@ -37,6 +42,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills 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 strainPeaks = new List(); ///