diff --git a/osu-framework b/osu-framework index 3ad1dd52ae..7131c54ea0 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 3ad1dd52ae511b816fb928f70ef811ec605c5c18 +Subproject commit 7131c54ea0a76ea03565ca93680487930c1c158a 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.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..a164566263 --- /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) + { + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects); + Skill[] skills = + { + 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..72ba421344 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -0,0 +1,93 @@ +// 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 +{ + /// + /// 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 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 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 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 object that just appeared. + /// + public IEnumerator GetEnumerator() + { + while (true) + { + // 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 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 + + foreach (OsuDifficultyHitObject h in onScreen) + { + h.TimeUntilHit -= latest.DeltaTime; + // Calculate reading strain here + } + + onScreen.Enqueue(latest); + } + 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 objects. + yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared. + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerator createDifficultyObjectEnumerator(List objects) + { + // 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]; + + // 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 component corresponds to the real first OsuHitOject. + } + + // 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]; + 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..bdeb62df3e --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -0,0 +1,70 @@ +// 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 +{ + /// + /// A wrapper around extending it with additional data required for difficulty calculation. + /// + public class OsuDifficultyHitObject + { + /// + /// The this refers to. + /// + public OsuHitObject BaseObject { get; } + + /// + /// Normalized distance from the of the previous . + /// + public double Distance { get; private set; } + + /// + /// Milliseconds elapsed since the StartTime of the previous . + /// + public double DeltaTime { get; private set; } + + /// + /// Number of milliseconds until the has to be hit. + /// + public double TimeUntilHit { get; set; } + + private const int normalized_radius = 52; + + private readonly OsuHitObject[] t; + + /// + /// Initializes the object calculating extra data required for difficulty calculation. + /// + 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. + 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 new file mode 100644 index 0000000000..aad53f6fe8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -0,0 +1,19 @@ +// 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.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; + protected override double StrainDecayBase => 0.15; + + 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 new file mode 100644 index 0000000000..b9632e18e2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -0,0 +1,100 @@ +// 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 +{ + /// + /// 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 + { + /// + /// 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 its original value in one second. + /// + protected abstract double StrainDecayBase { get; } + + /// + /// s that were processed previously. They can affect the strain values of the following objects. + /// + 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 an and update current strain values accordingly. + /// + public void Process(OsuDifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + if (!(current.BaseObject is Spinner)) + currentStrain += StrainValueOf(current) * 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 processed s. + /// + 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; + } + + /// + /// Calculates the strain value of an . This value is affected by previously processed objects. + /// + 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 new file mode 100644 index 0000000000..6c43c53e35 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -0,0 +1,39 @@ +// 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 +{ + /// + /// 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; + 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 StrainValueOf(OsuDifficultyHitObject current) + { + 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.DeltaTime; + } + } +} 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..d2c2e1d774 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -0,0 +1,86 @@ +// 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 after the capacity is full. + /// Indexing starts at the top of the stack. + /// + public class History : IEnumerable + { + public int Count { get; private set; } + + private readonly T[] array; + 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. + /// + /// The number of items the History can hold. + public History(int capacity) + { + 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. + } + + /// + /// The most recently added item is returned at index 0. + /// + public T this[int i] + { + get + { + if (i < 0 || i > Count - 1) + throw new IndexOutOfRangeException(); + + i += marker; + if (i > capacity - 1) + i -= capacity; + + return array[i]; + } + } + + /// + /// 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. + { + if (marker == 0) + marker = capacity - 1; + else + --marker; + + array[marker] = item; + + if (Count < capacity) + ++Count; + } + + /// + /// Returns an enumerator which enumerates items in the history starting from the most recently added one. + /// + public IEnumerator GetEnumerator() + { + for (int i = marker; i < capacity; ++i) + yield return array[i]; + + if (Count == capacity) + 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 21beaf4ccd..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 @@ - - + + + + + + + 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 d7b5247296..8a04d91cfb 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -79,7 +79,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); @@ -87,7 +87,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/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/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); } } 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); }