From c624712f2f413df06ee8e1670846d3210e5b5fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Mon, 5 Jun 2017 23:45:22 +0200 Subject: [PATCH 01/11] Refactor ppv2 to allow integration of pp+ features. --- .../Objects/OsuHitObjectDifficulty.cs | 201 ------------------ .../OsuDifficulty/OsuDifficultyCalculator.cs | 73 +++++++ .../Preprocessing/OsuDifficultyBeatmap.cs | 83 ++++++++ .../Preprocessing/OsuDifficultyHitObject.cs | 58 +++++ .../OsuDifficulty/Skills/Aim.cs | 15 ++ .../OsuDifficulty/Skills/Skill.cs | 85 ++++++++ .../OsuDifficulty/Skills/Speed.cs | 34 +++ .../OsuDifficulty/Utils/History.cs | 73 +++++++ .../OsuDifficultyCalculator.cs | 192 ----------------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + .../osu.Game.Rulesets.Osu.csproj | 9 +- 11 files changed, 429 insertions(+), 395 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs create mode 100644 osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs delete mode 100644 osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs deleted file mode 100644 index 1786771dca..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK; -using System; -using System.Diagnostics; -using System.Linq; - -namespace osu.Game.Rulesets.Osu.Objects -{ - internal class OsuHitObjectDifficulty - { - /// - /// Factor by how much speed / aim strain decays per second. - /// - /// - /// These values are results of tweaking a lot and taking into account general feedback. - /// Opinionated observation: Speed is easier to maintain than accurate jumps. - /// - internal static readonly double[] DECAY_BASE = { 0.3, 0.15 }; - - /// - /// Pseudo threshold values to distinguish between "singles" and "streams" - /// - /// - /// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values. - /// They also are based on tweaking and general feedback. - /// - private const double stream_spacing_threshold = 110, - single_spacing_threshold = 125; - - /// - /// Scaling values for weightings to keep aim and speed difficulty in balance. - /// - /// - /// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same. - /// - private static readonly double[] spacing_weight_scaling = { 1400, 26.25 }; - - /// - /// Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming. - /// - private const double almost_diameter = 90; - - internal OsuHitObject BaseHitObject; - internal double[] Strains = { 1, 1 }; - - internal int MaxCombo = 1; - - private readonly float scalingFactor; - private float lazySliderLength; - - private readonly Vector2 startPosition; - private readonly Vector2 endPosition; - - internal OsuHitObjectDifficulty(OsuHitObject baseHitObject) - { - BaseHitObject = baseHitObject; - float circleRadius = baseHitObject.Scale * 64; - - Slider slider = BaseHitObject as Slider; - if (slider != null) - MaxCombo += slider.Ticks.Count(); - - // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. - scalingFactor = 52.0f / circleRadius; - if (circleRadius < 30) - { - float smallCircleBonus = Math.Min(30.0f - circleRadius, 5.0f) / 50.0f; - scalingFactor *= 1.0f + smallCircleBonus; - } - - lazySliderLength = 0; - startPosition = baseHitObject.StackedPosition; - - // Calculate approximation of lazy movement on the slider - if (slider != null) - { - float sliderFollowCircleRadius = circleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests. - - // For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later - Vector2 cursorPos = startPosition; - - Action addSliderVertex = delegate (Vector2 pos) - { - Vector2 difference = pos - cursorPos; - float distance = difference.Length; - - // Did we move away too far? - if (distance > sliderFollowCircleRadius) - { - // Yep, we need to move the cursor - difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference - distance -= sliderFollowCircleRadius; - cursorPos += difference * distance; // We move the cursor just as far as needed to stay in the follow circle - lazySliderLength += distance; - } - }; - - // Actual computation of the first lazy curve - foreach (var tick in slider.Ticks) - addSliderVertex(tick.StackedPosition); - - addSliderVertex(baseHitObject.StackedEndPosition); - - lazySliderLength *= scalingFactor; - endPosition = cursorPos; - } - // We have a normal HitCircle or a spinner - else - endPosition = startPosition; - } - - internal void CalculateStrains(OsuHitObjectDifficulty previousHitObject, double timeRate) - { - calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Speed, timeRate); - calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Aim, timeRate); - } - - // Caution: The subjective values are strong with this one - private static double spacingWeight(double distance, OsuDifficultyCalculator.DifficultyType type) - { - switch (type) - { - case OsuDifficultyCalculator.DifficultyType.Speed: - if (distance > single_spacing_threshold) - return 2.5; - else if (distance > stream_spacing_threshold) - return 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); - else if (distance > almost_diameter) - return 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); - else if (distance > almost_diameter / 2) - return 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); - else - return 0.95; - - case OsuDifficultyCalculator.DifficultyType.Aim: - return Math.Pow(distance, 0.99); - } - - Debug.Assert(false, "Invalid osu difficulty hit object type."); - return 0; - } - - private void calculateSpecificStrain(OsuHitObjectDifficulty previousHitObject, OsuDifficultyCalculator.DifficultyType type, double timeRate) - { - double addition = 0; - double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; - double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000); - - if (BaseHitObject is Spinner) - { - // Do nothing for spinners - } - else if (BaseHitObject is Slider) - { - switch (type) - { - case OsuDifficultyCalculator.DifficultyType.Speed: - - // For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast. - // The spacing weight exists to differentiate between being able to easily alternate or having to single. - addition = - spacingWeight(previousHitObject.lazySliderLength + - DistanceTo(previousHitObject), type) * - spacing_weight_scaling[(int)type]; - - break; - case OsuDifficultyCalculator.DifficultyType.Aim: - - // For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference - // to multiple jumps. - addition = - ( - spacingWeight(previousHitObject.lazySliderLength, type) + - spacingWeight(DistanceTo(previousHitObject), type) - ) * - spacing_weight_scaling[(int)type]; - - break; - } - } - else if (BaseHitObject is HitCircle) - { - addition = spacingWeight(DistanceTo(previousHitObject), type) * spacing_weight_scaling[(int)type]; - } - - // Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero. - // You will never find maps that require this amongst ranked maps. - addition /= Math.Max(timeElapsed, 50); - - Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition; - } - - internal double DistanceTo(OsuHitObjectDifficulty other) - { - // Scale the distance by circle size. - return (startPosition - other.endPosition).Length * scalingFactor; - } - } -} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs new file mode 100644 index 0000000000..ee26109dd7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Beatmaps; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.OsuDifficulty.Skills; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty +{ + public class OsuDifficultyCalculator : DifficultyCalculator + { + private const int section_length = 400; + private const double difficulty_multiplier = 0.0675; + + public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap) + { + } + + protected override void PreprocessHitObjects() + { + foreach (OsuHitObject h in Objects) + (h as Slider)?.Curve?.Calculate(); + } + + protected override double CalculateInternal(Dictionary categoryDifficulty) + { + OsuDifficulyBeatmap beatmap = new OsuDifficulyBeatmap(Objects); + Skill[] skills = new Skill[2] + { + new Aim(), + new Speed() + }; + + double sectionEnd = section_length / TimeRate; + foreach (OsuDifficultyHitObject h in beatmap) + { + while (h.BaseObject.StartTime > sectionEnd) + { + foreach (Skill s in skills) + { + s.SaveCurrentPeak(); + s.StartNewSectionFrom(sectionEnd); + } + + sectionEnd += section_length; + } + + foreach (Skill s in skills) + s.Process(h); + } + + double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; + double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; + + double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; + + if (categoryDifficulty != null) + { + categoryDifficulty.Add("Aim", aimRating.ToString("0.00")); + categoryDifficulty.Add("Speed", speedRating.ToString("0.00")); + } + + return starRating; + } + + protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs new file mode 100644 index 0000000000..35a84a3018 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing +{ + public class OsuDifficulyBeatmap : IEnumerable + { + IEnumerator difficultyObjects; + private Queue onScreen = new Queue(); + + public OsuDifficulyBeatmap(List objects) + { + // Sort HitObjects by StartTime - they are not correctly ordered in some cases. + // This should probably happen before the objects reach the difficulty calculator. + objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); + difficultyObjects = createDifficultyObjectEnumerator(objects); + } + + public IEnumerator GetEnumerator() + { + do + { + // Add upcoming notes to the queue until we have at least one note that had been hit and can be dequeued. + // This means there is always at least one note in the queue unless we reached the end of the map. + bool hasNext; + do + { + hasNext = difficultyObjects.MoveNext(); + if (onScreen.Count == 0 && !hasNext) + yield break; // Stop if we have an empty enumerator. + + if (hasNext) + { + OsuDifficultyHitObject latest = difficultyObjects.Current; + // Calculate flow values here + + foreach (OsuDifficultyHitObject h in onScreen) + { + h.MSUntilHit -= latest.MS; + // Calculate reading strain here + } + + onScreen.Enqueue(latest); + } + } + while (onScreen.Peek().MSUntilHit > 0 && hasNext); // Keep adding new notes on screen while there is still time before we have to hit the next one. + + yield return onScreen.Dequeue(); // Remove and return notes one by one that had to be hit before the latest note appeared. + } + while (onScreen.Count > 0); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator createDifficultyObjectEnumerator(List objects) + { + // We will process HitObjects in groups of three to form a triangle, so we can calculate an angle for each note. + OsuHitObject[] triangle = new OsuHitObject[3]; + + // Difficulty object construction requires three components, an extra copy of the first object 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 is the real first note. + } + + // The final component of the first triangle will be the second note, which forms the first jump. + // If the beatmap has less than two HitObjects, 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); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs new file mode 100644 index 0000000000..ca10b19fc1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing +{ + public class OsuDifficultyHitObject + { + public OsuHitObject BaseObject { get; } + + /// + /// Normalized distance from the StartPosition of the previous note. + /// + public double Distance { get; private set; } + + /// + /// Milliseconds elapsed since the StartTime of the previous note. + /// + public double MS { get; private set; } + + public double MSUntilHit { get; set; } + + private const int normalized_radius = 52; + + private OsuHitObject[] t; + + public OsuDifficultyHitObject(OsuHitObject[] triangle) + { + t = triangle; + BaseObject = t[0]; + setDistances(); + setTimingValues(); + // Calculate angle here + } + + private void setDistances() + { + // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. + double scalingFactor = normalized_radius / BaseObject.Radius; + if (BaseObject.Radius < 30) + { + double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50; + scalingFactor *= 1 + smallCircleBonus; + } + + Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor; + } + + private void setTimingValues() + { + // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. + MS = Math.Max(40, t[0].StartTime - t[1].StartTime); + MSUntilHit = 450; // BaseObject.PreEmpt; + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs new file mode 100644 index 0000000000..2880098627 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + public class Aim : Skill + { + protected override double skillMultiplier => 26.25; + protected override double strainDecayBase => 0.15; + + protected override double strainValue() => Math.Pow(current.Distance, 0.99) / current.MS; + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs new file mode 100644 index 0000000000..3ac23ccd34 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.OsuDifficulty.Utils; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + public abstract class Skill + { + protected abstract double skillMultiplier { get; } + protected abstract double strainDecayBase { get; } + + protected OsuDifficultyHitObject current; + protected History previous = new History(2); // Contained objects not used yet + + 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 List strainPeaks = new List(); + + /// + /// Process a HitObject and update current strain values accordingly. + /// + public void Process(OsuDifficultyHitObject h) + { + current = h; + + currentStrain *= strainDecay(current.MS); + if (!(current.BaseObject is Spinner)) + currentStrain += strainValue() * skillMultiplier; + + currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); + + previous.Push(current); + } + + /// + /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. + /// + public void SaveCurrentPeak() + { + if (previous.Count > 0) + strainPeaks.Add(currentSectionPeak); + } + + /// + /// Sets the initial strain level for a new section. + /// + /// The beginning of the new section in milliseconds + public void StartNewSectionFrom(double offset) + { + // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. + // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. + if (previous.Count > 0) + currentSectionPeak = currentStrain * strainDecay(offset - previous[0].BaseObject.StartTime); + } + + /// + /// Returns the calculated difficulty value representing all currently processed HitObjects. + /// + public double DifficultyValue() + { + strainPeaks.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 strainPeaks) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + + protected abstract double strainValue(); + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs new file mode 100644 index 0000000000..42da6b8ed2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + public class Speed : Skill + { + protected override double skillMultiplier => 1400; + protected override double strainDecayBase => 0.3; + + private const double single_spacing_threshold = 125; + private const double stream_spacing_threshold = 110; + private const double almost_diameter = 90; + + protected override double strainValue() + { + double distance = current.Distance; + + double speedValue; + if (distance > single_spacing_threshold) + speedValue = 2.5; + else if (distance > stream_spacing_threshold) + speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); + else if (distance > almost_diameter) + speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); + else if (distance > almost_diameter / 2) + speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); + else + speedValue = 0.95; + + return speedValue / current.MS; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs new file mode 100644 index 0000000000..4bf20b1830 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils +{ + /// + /// An indexed stack with Push() only, which disposes items at the bottom once the size limit has been reached. + /// Indexing starts at the top of the stack. + /// + public class History : IEnumerable + { + public int Count { get; private set; } = 0; + + private T[] array; + private int size; + private int marker; // Marks the position of the most recently added item. + + public History(int size) + { + this.size = size; + array = new T[size]; + marker = size; // Set marker to the end of the array, outside of the indexed range by one. + } + + public T this[int i] // Index 0 returns the most recently added item. + { + get + { + if (i > Count - 1) + throw new IndexOutOfRangeException(); + + i += marker; + if (i > size - 1) + i -= size; + + return array[i]; + } + } + + /// + /// Adds the element as the most recent one in the history. + /// The oldest element is disposed if the history is full. + /// + public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition. + { + if (marker == 0) + marker = size - 1; + else + --marker; + + array[marker] = item; + + if (Count < size) + ++Count; + } + + public IEnumerator GetEnumerator() + { + for (int i = marker; i < size; ++i) + yield return array[i]; + + if (Count == size) + for (int i = 0; i < marker; ++i) + yield return array[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs deleted file mode 100644 index 5669993e67..0000000000 --- a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Beatmaps; -using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; -using System; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Osu -{ - public class OsuDifficultyCalculator : DifficultyCalculator - { - private const double star_scaling_factor = 0.0675; - private const double extreme_scaling_factor = 0.5; - - /// - /// HitObjects are stored as a member variable. - /// - internal List DifficultyHitObjects = new List(); - - public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap) - { - } - - protected override void PreprocessHitObjects() - { - foreach (var h in Objects) - (h as Slider)?.Curve?.Calculate(); - } - - protected override double CalculateInternal(Dictionary categoryDifficulty) - { - // Fill our custom DifficultyHitObject class, that carries additional information - DifficultyHitObjects.Clear(); - - foreach (var hitObject in Objects) - DifficultyHitObjects.Add(new OsuHitObjectDifficulty(hitObject)); - - // 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 speedDifficulty = CalculateDifficulty(DifficultyType.Speed); - double aimDifficulty = CalculateDifficulty(DifficultyType.Aim); - - // OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is - // to play the map, assuming, that too much of an error will not lead to a death. - // It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance - // and is superfluous in the beatmap difficulty rating. - // If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length - // into account as well. - - // The difficulty can be scaled by any desired metric. - // In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.) - // It would not be suitable for a star rating, therefore: - - // The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier - // 5-star maps would end up with one star. - double speedStars = Math.Sqrt(speedDifficulty) * star_scaling_factor; - double aimStars = Math.Sqrt(aimDifficulty) * star_scaling_factor; - - if (categoryDifficulty != null) - { - categoryDifficulty.Add("Aim", aimStars.ToString("0.00")); - categoryDifficulty.Add("Speed", speedStars.ToString("0.00")); - - double hitWindow300 = 30/*HitObjectManager.HitWindow300*/ / TimeRate; - double preEmpt = 450/*HitObjectManager.PreEmpt*/ / TimeRate; - - categoryDifficulty.Add("OD", (-(hitWindow300 - 80.0) / 6.0).ToString("0.00")); - categoryDifficulty.Add("AR", (preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0).ToString("0.00")); - - int maxCombo = 0; - foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects) - maxCombo += hitObject.MaxCombo; - - categoryDifficulty.Add("Max combo", maxCombo.ToString()); - } - - // Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder, - // than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes. - double starRating = speedStars + aimStars + Math.Abs(speedStars - aimStars) * extreme_scaling_factor; - // Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if - // the hit window size is to be considered as well. - - // Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars. - // Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star. - // Tune by yourself as you please. ;) - - return starRating; - } - - protected 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; - - OsuHitObjectDifficulty 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; - } - } - - /// - /// 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; - - protected double CalculateDifficulty(DifficultyType type) - { - 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 - - OsuHitObjectDifficulty previousHitObject = null; - foreach (OsuHitObjectDifficulty 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 decay = Math.Pow(OsuHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); - maximumStrain = previousHitObject.Strains[(int)type] * decay; - } - - // Go to the next time interval - intervalEndTime += actualStrainStep; - } - - // Obtain maximum strain - maximumStrain = Math.Max(hitObject.Strains[(int)type], 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() => new OsuBeatmapConverter(); - - // Those values are used as array indices. Be careful when changing them! - public enum DifficultyType - { - Speed = 0, - Aim, - }; - } -} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index bfed889b36..63fe6aaa59 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -7,6 +7,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index b91bdc6a78..7219cf8769 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -68,9 +68,14 @@ - - + + + + + + + From f9441a7419b5312bc7b2c855028ae37ecbda28e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 00:07:00 +0200 Subject: [PATCH 02/11] Fix typo and whitespace. --- .../OsuDifficulty/OsuDifficultyCalculator.cs | 2 +- .../OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs | 4 ++-- osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs | 2 +- osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index ee26109dd7..5da3613e8d 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty protected override double CalculateInternal(Dictionary categoryDifficulty) { - OsuDifficulyBeatmap beatmap = new OsuDifficulyBeatmap(Objects); + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects); Skill[] skills = new Skill[2] { new Aim(), diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index 35a84a3018..a831320f4b 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -7,12 +7,12 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing { - public class OsuDifficulyBeatmap : IEnumerable + public class OsuDifficultyBeatmap : IEnumerable { IEnumerator difficultyObjects; private Queue onScreen = new Queue(); - public OsuDifficulyBeatmap(List objects) + public OsuDifficultyBeatmap(List objects) { // Sort HitObjects by StartTime - they are not correctly ordered in some cases. // This should probably happen before the objects reach the difficulty calculator. diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs index 2880098627..274e50ab2d 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -12,4 +12,4 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills protected override double strainValue() => Math.Pow(current.Distance, 0.99) / current.MS; } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs index 3ac23ccd34..2146ffc4e6 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills currentStrain += strainValue() * skillMultiplier; currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); - + previous.Push(current); } From afb4443763fa445212b1dc83c6ad1c143651851d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 01:08:34 +0200 Subject: [PATCH 03/11] Capitalised protected members, added readonly modifiers. --- .../OsuDifficulty/OsuDifficultyCalculator.cs | 2 +- .../Preprocessing/OsuDifficultyBeatmap.cs | 10 +++---- .../Preprocessing/OsuDifficultyHitObject.cs | 10 +++---- .../OsuDifficulty/Skills/Aim.cs | 6 ++-- .../OsuDifficulty/Skills/Skill.cs | 30 +++++++++---------- .../OsuDifficulty/Skills/Speed.cs | 10 +++---- .../OsuDifficulty/Utils/History.cs | 6 ++-- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index 5da3613e8d..d8d48f8734 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty protected override double CalculateInternal(Dictionary categoryDifficulty) { OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects); - Skill[] skills = new Skill[2] + Skill[] skills = new Skill[] { new Aim(), new Speed() diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index a831320f4b..c2446409c5 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing { public class OsuDifficultyBeatmap : IEnumerable { - IEnumerator difficultyObjects; - private Queue onScreen = new Queue(); + private readonly IEnumerator difficultyObjects; + private readonly Queue onScreen = new Queue(); public OsuDifficultyBeatmap(List objects) { @@ -40,14 +40,14 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing foreach (OsuDifficultyHitObject h in onScreen) { - h.MSUntilHit -= latest.MS; + h.MsUntilHit -= latest.Ms; // Calculate reading strain here } onScreen.Enqueue(latest); } } - while (onScreen.Peek().MSUntilHit > 0 && hasNext); // Keep adding new notes on screen while there is still time before we have to hit the next one. + while (onScreen.Peek().MsUntilHit > 0 && hasNext); // Keep adding new notes on screen while there is still time before we have to hit the next one. yield return onScreen.Dequeue(); // Remove and return notes one by one that had to be hit before the latest note appeared. } @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator createDifficultyObjectEnumerator(List objects) + private IEnumerator createDifficultyObjectEnumerator(List objects) { // We will process HitObjects in groups of three to form a triangle, so we can calculate an angle for each note. OsuHitObject[] triangle = new OsuHitObject[3]; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index ca10b19fc1..4497f96c89 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -18,13 +18,13 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing /// /// Milliseconds elapsed since the StartTime of the previous note. /// - public double MS { get; private set; } + public double Ms { get; private set; } - public double MSUntilHit { get; set; } + public double MsUntilHit { get; set; } private const int normalized_radius = 52; - private OsuHitObject[] t; + private readonly OsuHitObject[] t; public OsuDifficultyHitObject(OsuHitObject[] triangle) { @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private void setTimingValues() { // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. - MS = Math.Max(40, t[0].StartTime - t[1].StartTime); - MSUntilHit = 450; // BaseObject.PreEmpt; + Ms = Math.Max(40, t[0].StartTime - t[1].StartTime); + MsUntilHit = 450; // BaseObject.PreEmpt; } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs index 274e50ab2d..57fcff965a 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public class Aim : Skill { - protected override double skillMultiplier => 26.25; - protected override double strainDecayBase => 0.15; + protected override double SkillMultiplier => 26.25; + protected override double StrainDecayBase => 0.15; - protected override double strainValue() => Math.Pow(current.Distance, 0.99) / current.MS; + protected override double StrainValue() => Math.Pow(Current.Distance, 0.99) / Current.Ms; } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs index 2146ffc4e6..c2aa55d650 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -11,30 +11,30 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public abstract class Skill { - protected abstract double skillMultiplier { get; } - protected abstract double strainDecayBase { get; } + protected abstract double SkillMultiplier { get; } + protected abstract double StrainDecayBase { get; } - protected OsuDifficultyHitObject current; - protected History previous = new History(2); // Contained objects not used yet + protected OsuDifficultyHitObject Current; + protected History Previous = new History(2); // Contained objects not used yet 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 List strainPeaks = new List(); + private readonly List strainPeaks = new List(); /// /// Process a HitObject and update current strain values accordingly. /// public void Process(OsuDifficultyHitObject h) { - current = h; + Current = h; - currentStrain *= strainDecay(current.MS); - if (!(current.BaseObject is Spinner)) - currentStrain += strainValue() * skillMultiplier; + currentStrain *= strainDecay(Current.Ms); + if (!(Current.BaseObject is Spinner)) + currentStrain += StrainValue() * SkillMultiplier; currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); - previous.Push(current); + Previous.Push(Current); } /// @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills /// public void SaveCurrentPeak() { - if (previous.Count > 0) + if (Previous.Count > 0) strainPeaks.Add(currentSectionPeak); } @@ -54,8 +54,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. - if (previous.Count > 0) - currentSectionPeak = currentStrain * strainDecay(offset - previous[0].BaseObject.StartTime); + if (Previous.Count > 0) + currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime); } /// @@ -78,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills return difficulty; } - protected abstract double strainValue(); + protected abstract double StrainValue(); - private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs index 42da6b8ed2..33e8fec829 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -5,16 +5,16 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public class Speed : Skill { - protected override double skillMultiplier => 1400; - protected override double strainDecayBase => 0.3; + protected override double SkillMultiplier => 1400; + protected override double StrainDecayBase => 0.3; private const double single_spacing_threshold = 125; private const double stream_spacing_threshold = 110; private const double almost_diameter = 90; - protected override double strainValue() + protected override double StrainValue() { - double distance = current.Distance; + double distance = Current.Distance; double speedValue; if (distance > single_spacing_threshold) @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills else speedValue = 0.95; - return speedValue / current.MS; + return speedValue / Current.Ms; } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs index 4bf20b1830..38727707d9 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -13,10 +13,10 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils /// public class History : IEnumerable { - public int Count { get; private set; } = 0; + public int Count { get; private set; } - private T[] array; - private int size; + private readonly T[] array; + private readonly int size; private int marker; // Marks the position of the most recently added item. public History(int size) From 01585027b1f869a4e1c26389c569ad4bfed74ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 15:39:37 +0200 Subject: [PATCH 04/11] OsuDifficultyBeatmap enumeration logic made clearer, more documentation added. --- .../OsuDifficulty/OsuDifficultyCalculator.cs | 2 +- .../Preprocessing/OsuDifficultyBeatmap.cs | 43 +++++++++++-------- .../Preprocessing/OsuDifficultyHitObject.cs | 17 ++++++-- .../OsuDifficulty/Skills/Aim.cs | 2 +- .../OsuDifficulty/Skills/Skill.cs | 20 ++++++++- .../OsuDifficulty/Skills/Speed.cs | 4 +- .../OsuDifficulty/Utils/History.cs | 40 +++++++++++------ 7 files changed, 87 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index d8d48f8734..a164566263 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty protected override double CalculateInternal(Dictionary categoryDifficulty) { OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects); - Skill[] skills = new Skill[] + Skill[] skills = { new Aim(), new Speed() diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index c2446409c5..a96dd31078 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private readonly IEnumerator difficultyObjects; private readonly Queue onScreen = new Queue(); + /// + /// Creates an enumerable, which handles the preprocessing of HitObjects, getting them ready to be used in difficulty calculation. + /// public OsuDifficultyBeatmap(List objects) { // Sort HitObjects by StartTime - they are not correctly ordered in some cases. @@ -20,38 +23,40 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing difficultyObjects = createDifficultyObjectEnumerator(objects); } + /// + /// Returns an enumerator that enumerates all difficulty objects in the beatmap. + /// The inner loop adds notes that appear on screen into a queue until we need to hit the next note, + /// the outer loop returns notes 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 note that just appeared. + /// public IEnumerator GetEnumerator() { - do + while (true) { // Add upcoming notes to the queue until we have at least one note that had been hit and can be dequeued. // This means there is always at least one note in the queue unless we reached the end of the map. - bool hasNext; do { - hasNext = difficultyObjects.MoveNext(); - if (onScreen.Count == 0 && !hasNext) - yield break; // Stop if we have an empty enumerator. + if (!difficultyObjects.MoveNext()) + break; // New notes can't be added anymore, but we still need to dequeue and return the ones already on screen. - if (hasNext) + OsuDifficultyHitObject latest = difficultyObjects.Current; + // Calculate flow values here + + foreach (OsuDifficultyHitObject h in onScreen) { - OsuDifficultyHitObject latest = difficultyObjects.Current; - // Calculate flow values here - - foreach (OsuDifficultyHitObject h in onScreen) - { - h.MsUntilHit -= latest.Ms; - // Calculate reading strain here - } - - onScreen.Enqueue(latest); + h.TimeUntilHit -= latest.DeltaTime; + // Calculate reading strain here } - } - while (onScreen.Peek().MsUntilHit > 0 && hasNext); // Keep adding new notes on screen while there is still time before we have to hit the next one. + onScreen.Enqueue(latest); + } + while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new notes 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 notes. yield return onScreen.Dequeue(); // Remove and return notes one by one that had to be hit before the latest note appeared. } - while (onScreen.Count > 0); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index 4497f96c89..a83ae6ee7e 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing { public class OsuDifficultyHitObject { + /// + /// The current note. Attributes are calculated relative to previous ones. + /// public OsuHitObject BaseObject { get; } /// @@ -18,14 +21,20 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing /// /// Milliseconds elapsed since the StartTime of the previous note. /// - public double Ms { get; private set; } + public double DeltaTime { get; private set; } - public double MsUntilHit { get; set; } + /// + /// Number of milliseconds until the note has to be hit. + /// + public double TimeUntilHit { get; set; } private const int normalized_radius = 52; private readonly OsuHitObject[] t; + /// + /// Constructs a wrapper around a HitObject calculating additional data required for difficulty calculation. + /// public OsuDifficultyHitObject(OsuHitObject[] triangle) { t = triangle; @@ -51,8 +60,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private void setTimingValues() { // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. - Ms = Math.Max(40, t[0].StartTime - t[1].StartTime); - MsUntilHit = 450; // BaseObject.PreEmpt; + DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime); + TimeUntilHit = 450; // BaseObject.PreEmpt; } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs index 57fcff965a..e17679161b 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills protected override double SkillMultiplier => 26.25; protected override double StrainDecayBase => 0.15; - protected override double StrainValue() => Math.Pow(Current.Distance, 0.99) / Current.Ms; + protected override double StrainValue() => Math.Pow(Current.Distance, 0.99) / Current.DeltaTime; } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs index c2aa55d650..498374df8d 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -11,10 +11,25 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public abstract class Skill { + /// + /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. + /// protected abstract double SkillMultiplier { get; } + + /// + /// Determines how quickly strain decays for the given skill. + /// For example a value of 0.15 indicates that strain decays to 15% of it's original value in one second. + /// protected abstract double StrainDecayBase { get; } + /// + /// The note that will be processed. + /// protected OsuDifficultyHitObject Current; + + /// + /// Notes that were processed previously. They can affect the strain value of the current note. + /// protected History Previous = new History(2); // Contained objects not used yet private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap. @@ -28,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { Current = h; - currentStrain *= strainDecay(Current.Ms); + currentStrain *= strainDecay(Current.DeltaTime); if (!(Current.BaseObject is Spinner)) currentStrain += StrainValue() * SkillMultiplier; @@ -78,6 +93,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills return difficulty; } + /// + /// Calculates the strain value of the current note. This value is affected by previous notes. + /// protected abstract double StrainValue(); private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs index 33e8fec829..934df4dd8a 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills else speedValue = 0.95; - return speedValue / Current.Ms; + return speedValue / Current.DeltaTime; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs index 38727707d9..f1f7b189be 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils { /// - /// An indexed stack with Push() only, which disposes items at the bottom once the size limit has been reached. + /// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full. /// Indexing starts at the top of the stack. /// public class History : IEnumerable @@ -16,17 +16,28 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils public int Count { get; private set; } private readonly T[] array; - private readonly int size; + private readonly int capacity; private int marker; // Marks the position of the most recently added item. - public History(int size) + // + /// + /// Initializes a new instance of the History class that is empty and has the specified capacity. + /// + /// The number of items the History can hold. + public History(int capacity) { - this.size = size; - array = new T[size]; - marker = size; // Set marker to the end of the array, outside of the indexed range by one. + if (capacity < 0) + throw new ArgumentOutOfRangeException(); + + this.capacity = capacity; + array = new T[capacity]; + marker = capacity; // Set marker to the end of the array, outside of the indexed range by one. } - public T this[int i] // Index 0 returns the most recently added item. + /// + /// The most recently added item is returned at index 0. + /// + public T this[int i] { get { @@ -34,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils throw new IndexOutOfRangeException(); i += marker; - if (i > size - 1) - i -= size; + if (i > capacity - 1) + i -= capacity; return array[i]; } @@ -48,22 +59,25 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition. { if (marker == 0) - marker = size - 1; + marker = capacity - 1; else --marker; array[marker] = item; - if (Count < size) + if (Count < capacity) ++Count; } + /// + /// Returns an enumerator which enumerates items in the history starting from the most recently added item. + /// public IEnumerator GetEnumerator() { - for (int i = marker; i < size; ++i) + for (int i = marker; i < capacity; ++i) yield return array[i]; - if (Count == size) + if (Count == capacity) for (int i = 0; i < marker; ++i) yield return array[i]; } From ce0d70d6513adc79cc9f77ecbf32adfc7c613539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 15:52:33 +0200 Subject: [PATCH 05/11] Whitespace fix. --- osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs index f1f7b189be..21b58cd69d 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils private readonly int capacity; private int marker; // Marks the position of the most recently added item. - // /// /// Initializes a new instance of the History class that is empty and has the specified capacity. /// From 93f654a5397c4721fb519cfad6a3e547b859aae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 18:59:46 +0200 Subject: [PATCH 06/11] More docs, better docs. --- .../Preprocessing/OsuDifficultyBeatmap.cs | 39 +++++++++++-------- .../Preprocessing/OsuDifficultyHitObject.cs | 13 ++++--- .../OsuDifficulty/Skills/Aim.cs | 3 +- .../OsuDifficulty/Skills/Skill.cs | 35 ++++++++--------- .../OsuDifficulty/Skills/Speed.cs | 8 ++-- .../OsuDifficulty/Utils/History.cs | 6 +-- 6 files changed, 56 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index a96dd31078..72ba421344 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -7,39 +7,44 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing { + /// + /// An enumerable container wrapping input as + /// which contains extra data required for difficulty calculation. + /// public class OsuDifficultyBeatmap : IEnumerable { private readonly IEnumerator difficultyObjects; private readonly Queue onScreen = new Queue(); /// - /// Creates an enumerable, which handles the preprocessing of HitObjects, getting them ready to be used in difficulty calculation. + /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as + /// which contains extra data required for difficulty calculation. /// public OsuDifficultyBeatmap(List objects) { - // Sort HitObjects 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. objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); difficultyObjects = createDifficultyObjectEnumerator(objects); } /// - /// Returns an enumerator that enumerates all difficulty objects in the beatmap. - /// The inner loop adds notes that appear on screen into a queue until we need to hit the next note, - /// the outer loop returns notes from this queue one at a time, only after they had to be hit, and should no longer be on screen. + /// Returns an enumerator that enumerates all s in the . + /// 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 note that just appeared. + /// allowing us to determine a reading strain for the object that just appeared. /// public IEnumerator GetEnumerator() { while (true) { - // Add upcoming notes to the queue until we have at least one note that had been hit and can be dequeued. - // This means there is always at least one note in the queue unless we reached the end of the map. + // 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 notes can't be added anymore, but we still need to dequeue and return the ones already on screen. + 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 @@ -52,10 +57,10 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing onScreen.Enqueue(latest); } - while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new notes on screen while there is still time before we have to hit the next one. + 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 notes. - yield return onScreen.Dequeue(); // Remove and return notes one by one that had to be hit before the latest note appeared. + 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. } } @@ -63,18 +68,18 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private IEnumerator createDifficultyObjectEnumerator(List objects) { - // We will process HitObjects in groups of three to form a triangle, so we can calculate an angle for each note. + // 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]; - // Difficulty object construction requires three components, an extra copy of the first object is used at the beginning. + // 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 is the real first note. + triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject. } - // The final component of the first triangle will be the second note, which forms the first jump. - // If the beatmap has less than two HitObjects, the enumerator will not return anything. + // The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump. + // 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]; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index a83ae6ee7e..69399fb02f 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -6,25 +6,28 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing { + /// + /// A wrapper around extending it with additional data required for difficulty calculation. + /// public class OsuDifficultyHitObject { /// - /// The current note. Attributes are calculated relative to previous ones. + /// The this refers to. /// public OsuHitObject BaseObject { get; } /// - /// Normalized distance from the StartPosition of the previous note. + /// Normalized distance from the of the previous . /// public double Distance { get; private set; } /// - /// Milliseconds elapsed since the StartTime of the previous note. + /// Milliseconds elapsed since the StartTime of the previous . /// public double DeltaTime { get; private set; } /// - /// Number of milliseconds until the note has to be hit. + /// Number of milliseconds until the has to be hit. /// public double TimeUntilHit { get; set; } @@ -33,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private readonly OsuHitObject[] t; /// - /// Constructs a wrapper around a HitObject calculating additional data required for difficulty calculation. + /// Initializes the object calculating extra data required for difficulty calculation. /// public OsuDifficultyHitObject(OsuHitObject[] triangle) { diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs index e17679161b..99d0e761ad 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { @@ -10,6 +11,6 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills protected override double SkillMultiplier => 26.25; protected override double StrainDecayBase => 0.15; - protected override double StrainValue() => Math.Pow(Current.Distance, 0.99) / Current.DeltaTime; + protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime; } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs index 498374df8d..b9632e18e2 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -9,6 +9,10 @@ using osu.Game.Rulesets.Osu.OsuDifficulty.Utils; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { + /// + /// Used to processes strain values of s, keep track of strain levels caused by the processed objects + /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. + /// public abstract class Skill { /// @@ -18,38 +22,31 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills /// /// Determines how quickly strain decays for the given skill. - /// For example a value of 0.15 indicates that strain decays to 15% of it's original value in one second. + /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. /// protected abstract double StrainDecayBase { get; } /// - /// The note that will be processed. + /// s that were processed previously. They can affect the strain values of the following objects. /// - protected OsuDifficultyHitObject Current; - - /// - /// Notes that were processed previously. They can affect the strain value of the current note. - /// - protected History Previous = new History(2); // Contained objects not used yet + protected readonly History Previous = new History(2); // Contained objects not used yet 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(); /// - /// Process a HitObject and update current strain values accordingly. + /// Process an and update current strain values accordingly. /// - public void Process(OsuDifficultyHitObject h) + public void Process(OsuDifficultyHitObject current) { - Current = h; - - currentStrain *= strainDecay(Current.DeltaTime); - if (!(Current.BaseObject is Spinner)) - currentStrain += StrainValue() * SkillMultiplier; + currentStrain *= strainDecay(current.DeltaTime); + if (!(current.BaseObject is Spinner)) + currentStrain += StrainValueOf(current) * SkillMultiplier; currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); - Previous.Push(Current); + Previous.Push(current); } /// @@ -74,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills } /// - /// Returns the calculated difficulty value representing all currently processed HitObjects. + /// Returns the calculated difficulty value representing all processed s. /// public double DifficultyValue() { @@ -94,9 +91,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills } /// - /// Calculates the strain value of the current note. This value is affected by previous notes. + /// Calculates the strain value of an . This value is affected by previously processed objects. /// - protected abstract double StrainValue(); + protected abstract double StrainValueOf(OsuDifficultyHitObject current); private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs index 934df4dd8a..826d62adcc 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; + namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public class Speed : Skill @@ -12,9 +14,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills private const double stream_spacing_threshold = 110; private const double almost_diameter = 90; - protected override double StrainValue() + protected override double StrainValueOf(OsuDifficultyHitObject current) { - double distance = Current.Distance; + double distance = current.Distance; double speedValue; if (distance > single_spacing_threshold) @@ -28,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills else speedValue = 0.95; - return speedValue / Current.DeltaTime; + return speedValue / current.DeltaTime; } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs index 21b58cd69d..92b206ccf8 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils } /// - /// Adds the element as the most recent one in the history. - /// The oldest element is disposed if the history is full. + /// Adds the item as the most recent one in the history. + /// The oldest item is disposed if the history is full. /// public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition. { @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils } /// - /// Returns an enumerator which enumerates items in the history starting from the most recently added item. + /// Returns an enumerator which enumerates items in the history starting from the most recently added one. /// public IEnumerator GetEnumerator() { From 1f311cca0659da77843c7142d7b2f4fe5216a78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Tue, 6 Jun 2017 19:09:26 +0200 Subject: [PATCH 07/11] Fix cref in comment. --- .../OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index 69399fb02f..bdeb62df3e 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing public double DeltaTime { get; private set; } /// - /// Number of milliseconds until the has to be hit. + /// Number of milliseconds until the has to be hit. /// public double TimeUntilHit { get; set; } From a0bdab9f0de894b1c06f28bb720f73b2d0f03584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Nemes?= Date: Wed, 7 Jun 2017 20:29:03 +0200 Subject: [PATCH 08/11] Aaand docs again. --- osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs | 3 +++ osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs | 3 +++ osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs index 99d0e761ad..aad53f6fe8 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -6,6 +6,9 @@ using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { + /// + /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. + /// public class Aim : Skill { protected override double SkillMultiplier => 26.25; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs index 826d62adcc..b06063fca4 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -3,6 +3,9 @@ using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +/// +/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. +/// namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { public class Speed : Skill diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs index 92b206ccf8..d2c2e1d774 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils { get { - if (i > Count - 1) + if (i < 0 || i > Count - 1) throw new IndexOutOfRangeException(); i += marker; From 9d915a691dd6c8273f21d9112b733cc2e7944f19 Mon Sep 17 00:00:00 2001 From: Drezi126 Date: Thu, 8 Jun 2017 00:17:58 +0200 Subject: [PATCH 09/11] Fix comment placement. --- osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs index b06063fca4..6c43c53e35 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -3,11 +3,11 @@ using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; -/// -/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. -/// namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills { + /// + /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. + /// public class Speed : Skill { protected override double SkillMultiplier => 1400; From 5f7270ee4b1109178ffd7a4f8bb4959068e5470c Mon Sep 17 00:00:00 2001 From: Tom94 Date: Thu, 8 Jun 2017 09:53:45 +0200 Subject: [PATCH 10/11] Use new invalidation rules --- osu-framework | 2 +- osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs | 2 +- osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs | 2 +- osu.Game/Screens/Play/SquareGraph.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu-framework b/osu-framework index 3ad1dd52ae..68111ddd8d 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 3ad1dd52ae511b816fb928f70ef811ec605c5c18 +Subproject commit 68111ddd8dae9d57874e96812e2b83920b9b8155 diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs index 2619ce150c..26d5146aae 100644 --- a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs +++ b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Timing public override void InvalidateFromChild(Invalidation invalidation) { // We only want to re-compute our size when a child's size or position has changed - if ((invalidation & Invalidation.Geometry) == 0) + if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0) { base.InvalidateFromChild(invalidation); return; diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 11a964d179..ec94dced06 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.General set { bounding = value; - Invalidate(Invalidation.Geometry); + Invalidate(Invalidation.MiscGeometry); } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 43a8253b53..0b3bcc55a2 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Play public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { - if ((invalidation & Invalidation.SizeInParentSpace) > 0) + if ((invalidation & Invalidation.DrawSize) > 0) layout.Invalidate(); return base.Invalidate(invalidation, source, shallPropagate); } From 64682a741cdfdbce8424c855b3336e02a2f97b64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Jun 2017 14:51:22 +0900 Subject: [PATCH 11/11] Update framework --- osu-framework | 2 +- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- osu.Game/Overlays/Mods/ModButton.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- osu.Game/Screens/Multiplayer/Match.cs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu-framework b/osu-framework index 3ad1dd52ae..63a4ff0593 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 3ad1dd52ae511b816fb928f70ef811ec605c5c18 +Subproject commit 63a4ff05939bcb511a9f18d52dba1ceff99a1cd3 diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 0cd6b8dd3a..e8bde11a3c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -86,6 +86,6 @@ namespace osu.Game.Overlays.Chat } } - private void scrollToEnd() => Scheduler.AddDelayed(() => scroll.ScrollToEnd(), 50); + private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index dd135e43ef..516f65951c 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Mods backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); backgroundIcon.Icon = modAfter.Icon; - using (iconsContainer.BeginDelayedSequence(mod_switch_duration, true)) + using (BeginDelayedSequence(mod_switch_duration, true)) { foregroundIcon.RotateTo(-rotate_angle * direction); foregroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Mods backgroundIcon.RotateTo(rotate_angle * direction); backgroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); - iconsContainer.Schedule(() => displayMod(modAfter)); + Schedule(() => displayMod(modAfter)); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 850c640770..2582c68296 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -25,13 +25,13 @@ namespace osu.Game.Screens.Edit protected override void OnEntering(Screen last) { base.OnEntering(last); - Background.Schedule(() => Background.FadeColour(Color4.DarkGray, 500)); + Background.FadeColour(Color4.DarkGray, 500); Beatmap?.Track?.Stop(); } protected override bool OnExiting(Screen next) { - Background.Schedule(() => Background.FadeColour(Color4.White, 500)); + Background.FadeColour(Color4.White, 500); Beatmap?.Track?.Start(); return base.OnExiting(next); } diff --git a/osu.Game/Screens/Multiplayer/Match.cs b/osu.Game/Screens/Multiplayer/Match.cs index 7da6ef800e..ec6a66062d 100644 --- a/osu.Game/Screens/Multiplayer/Match.cs +++ b/osu.Game/Screens/Multiplayer/Match.cs @@ -24,12 +24,12 @@ namespace osu.Game.Screens.Multiplayer { base.OnEntering(last); - Background.Schedule(() => Background.FadeColour(Color4.DarkGray, 500)); + Background.FadeColour(Color4.DarkGray, 500); } protected override bool OnExiting(Screen next) { - Background.Schedule(() => Background.FadeColour(Color4.White, 500)); + Background.FadeColour(Color4.White, 500); return base.OnExiting(next); } }