From 31cf00e3b83f04aee8c2d28325239f0b1060431d Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 25 Feb 2018 23:52:38 -0800 Subject: [PATCH 1/6] Implement mania star difficulty calculation --- .../ManiaDifficultyCalculator.cs | 124 +++++++++++++++++- .../Objects/ManiaHitObjectDifficulty.cs | 114 ++++++++++++++++ .../osu.Game.Rulesets.Mania.csproj | 1 + 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 75a8543548..62d2929f27 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -4,18 +4,140 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using System; using System.Collections.Generic; namespace osu.Game.Rulesets.Mania { public class ManiaDifficultyCalculator : DifficultyCalculator { + 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. + /// + protected const double strain_step = 400; + + /// + /// The weighting of each strain value decays to this number * it's previous value + /// + protected const double decay_weight = 0.9; + + /// + /// HitObjects are stored as a member variable. + /// + private readonly List difficultyHitObjects = new List(); + public ManiaDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) + { + // Fill our custom DifficultyHitObject class, that carries additional information + difficultyHitObjects.Clear(); + + int columnCount = (Beatmap as ManiaBeatmap).TotalColumns; + + foreach (var hitObject in Beatmap.HitObjects) + difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount)); + + // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. + difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); + + if (!calculateStrainValues()) + return 0; + + double starRating = calculateDifficulty() * star_scaling_factor; + + if (categoryDifficulty != null) + { + categoryDifficulty.Add("Strain", starRating); + // categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); + } + + return starRating; + } + + private bool calculateStrainValues() + { + // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. + using (List.Enumerator hitObjectsEnumerator = difficultyHitObjects.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() + { + 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 difficultyHitObjects) + { + // 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 BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap); } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs new file mode 100644 index 0000000000..e8b47092f6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using System; + +namespace osu.Game.Rulesets.Mania.Objects +{ + 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 int beatmapColumnCount; + + + private double endTime; + private double[] heldUntil; + + /// + /// Measures jacks or more generally: repeated presses of the same button + /// + private 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 addition = 1.0; + 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/* + (double)SpeedMania.Column / 8.0*/) * holdFactor; + + OverallStrain = previousHitObject.OverallStrain * overallDecay + (addition + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index a2e21e2053..b9c62cf40b 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -114,6 +114,7 @@ + From 96f416fef382bad61da66454de0f8f688ae75d27 Mon Sep 17 00:00:00 2001 From: Poyo Date: Mon, 26 Feb 2018 00:18:54 -0800 Subject: [PATCH 2/6] Update code style Sorry, bot overlords. --- osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs | 8 ++++---- .../Objects/ManiaHitObjectDifficulty.cs | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 62d2929f27..e1d3b6212f 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Mania { - public class ManiaDifficultyCalculator : DifficultyCalculator + internal class ManiaDifficultyCalculator : DifficultyCalculator { private const double star_scaling_factor = 0.018; @@ -18,12 +18,12 @@ namespace osu.Game.Rulesets.Mania /// 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. /// - protected const double strain_step = 400; + private const double strain_step = 400; /// /// The weighting of each strain value decays to this number * it's previous value /// - protected const double decay_weight = 0.9; + private const double decay_weight = 0.9; /// /// HitObjects are stored as a member variable. @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania // Fill our custom DifficultyHitObject class, that carries additional information difficultyHitObjects.Clear(); - int columnCount = (Beatmap as ManiaBeatmap).TotalColumns; + int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7; foreach (var hitObject in Beatmap.HitObjects) difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount)); diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs index e8b47092f6..0b5e7d7e4c 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs @@ -6,7 +6,7 @@ using System; namespace osu.Game.Rulesets.Mania.Objects { - class ManiaHitObjectDifficulty + internal class ManiaHitObjectDifficulty { /// /// Factor by how much individual / overall strain decays per second. @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Objects internal ManiaHitObject BaseHitObject; - private int beatmapColumnCount; + private readonly int beatmapColumnCount; private double endTime; @@ -68,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.Objects internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate) { // TODO: Factor in holds - double addition = 1.0; 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); @@ -106,9 +105,9 @@ namespace osu.Game.Rulesets.Mania.Objects heldUntil[BaseHitObject.Column] = endTime; // Increase individual strain in own column - IndividualStrain += (2.0/* + (double)SpeedMania.Column / 8.0*/) * holdFactor; + IndividualStrain += 2.0 * holdFactor; - OverallStrain = previousHitObject.OverallStrain * overallDecay + (addition + holdAddition) * holdFactor; + OverallStrain = previousHitObject.OverallStrain * overallDecay + (1.0 + holdAddition) * holdFactor; } } } From e187c6453d841aa393dda610938e858b786951bf Mon Sep 17 00:00:00 2001 From: Poyo Date: Mon, 5 Mar 2018 18:19:06 -0800 Subject: [PATCH 3/6] Added mania-difficulty mod support --- .../ManiaDifficultyCalculator.cs | 15 ++++++++------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Objects/ManiaHitObjectDifficulty.cs | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index e1d3b6212f..02560c8141 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -4,6 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; @@ -31,8 +32,11 @@ namespace osu.Game.Rulesets.Mania private readonly List difficultyHitObjects = new List(); public ManiaDifficultyCalculator(Beatmap beatmap) - : base(beatmap) - { + : base(beatmap) { + } + + public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods) + : base(beatmap, mods) { } public override double Calculate(Dictionary categoryDifficulty = null) @@ -53,11 +57,8 @@ namespace osu.Game.Rulesets.Mania double starRating = calculateDifficulty() * star_scaling_factor; - if (categoryDifficulty != null) - { - categoryDifficulty.Add("Strain", starRating); - // categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); - } + categoryDifficulty?.Add("Strain", starRating); + // categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); return starRating; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 3bfb4d3e44..ac815e0e2f 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); public override int LegacyID => 3; diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs index 0b5e7d7e4c..2b59279972 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs @@ -22,13 +22,13 @@ namespace osu.Game.Rulesets.Mania.Objects private readonly int beatmapColumnCount; - private double endTime; - private double[] heldUntil; + private readonly double endTime; + private readonly double[] heldUntil; /// /// Measures jacks or more generally: repeated presses of the same button /// - private double[] individualStrains; + private readonly double[] individualStrains; internal double IndividualStrain { From fbb80edde1888523ff730d31d186a5d1466e3849 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Mar 2018 13:01:29 +0900 Subject: [PATCH 4/6] Minor cleanups --- osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 02560c8141..f4e3d54a3d 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -32,11 +32,13 @@ namespace osu.Game.Rulesets.Mania private readonly List difficultyHitObjects = new List(); public ManiaDifficultyCalculator(Beatmap beatmap) - : base(beatmap) { + : base(beatmap) + { } public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods) - : base(beatmap, mods) { + : base(beatmap, mods) + { } public override double Calculate(Dictionary categoryDifficulty = null) @@ -58,7 +60,6 @@ namespace osu.Game.Rulesets.Mania double starRating = calculateDifficulty() * star_scaling_factor; categoryDifficulty?.Add("Strain", starRating); - // categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); return starRating; } From 81186f8423198b76a97a2d9846281d4a612cbe56 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Mar 2018 13:06:42 +0900 Subject: [PATCH 5/6] Apply beatmap converter mods in DifficultyCalculator --- osu.Game/Beatmaps/DifficultyCalculator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index 798268d05f..d61c62a30b 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -24,9 +24,15 @@ namespace osu.Game.Beatmaps protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null) { - Beatmap = CreateBeatmapConverter(beatmap).Convert(beatmap); Mods = mods ?? new Mod[0]; + var converter = CreateBeatmapConverter(beatmap); + + foreach (var mod in Mods.OfType>()) + mod.ApplyToBeatmapConverter(converter); + + Beatmap = converter.Convert(beatmap); + ApplyMods(Mods); PreprocessHitObjects(); From 3cd203699b485067d79eed122e27fd822895e695 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Mar 2018 13:09:19 +0900 Subject: [PATCH 6/6] Apply beatmap converter mods in PerformanceCalculator --- osu.Game/Rulesets/Scoring/PerformanceCalculator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs index ba16d78b37..c047a421fd 100644 --- a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs @@ -2,7 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -23,9 +25,15 @@ namespace osu.Game.Rulesets.Scoring protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) { - Beatmap = CreateBeatmapConverter().Convert(beatmap); Score = score; + var converter = CreateBeatmapConverter(); + + foreach (var mod in score.Mods.OfType>()) + mod.ApplyToBeatmapConverter(converter); + + Beatmap = converter.Convert(beatmap); + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); }