diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 2c36e81190..a25551f854 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3683365342338796d, "diffcalc-test")]
+ [TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index d1d5adea75..93a9ce3dbd 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
public int TotalColumns => Stages.Sum(g => g.Columns);
+ ///
+ /// The total number of columns that were present in this before any user adjustments.
+ ///
+ public readonly int OriginalTotalColumns;
+
///
/// Creates a new .
///
/// The initial stages.
- public ManiaBeatmap(StageDefinition defaultStage)
+ /// The total number of columns present before any user adjustments. Defaults to the total columns in .
+ public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
+ OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable GetStatistics()
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index b17ab3f375..7a0e3b2b76 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public bool Dual;
public readonly bool IsForCurrentRuleset;
+ private readonly int originalTargetColumns;
+
// Internal for testing purposes
internal FastRandom Random { get; private set; }
@@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
+
+ originalTargetColumns = TargetColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
@@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap CreateBeatmap()
{
- beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
+ beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 3ff665d2c8..0b58d1efc6 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
+ public double ScoreMultiplier;
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index b08c520c54..ade830764d 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
+using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
@@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset;
+ private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
+ originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes
{
- StarRating = difficultyValue(skills) * star_scaling_factor,
+ StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
+ GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
+ ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
- private double difficultyValue(Skill[] skills)
- {
- // Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
- var overall = skills.OfType().Single();
- var aggregatePeaks = new List(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
-
- foreach (var individual in skills.OfType())
- {
- for (int i = 0; i < individual.StrainPeaks.Count; i++)
- {
- double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
-
- if (aggregate > aggregatePeaks[i])
- aggregatePeaks[i] = aggregate;
- }
- }
-
- aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
-
- double difficulty = 0;
- double weight = 1;
-
- // Difficulty is the weighted sum of the highest strains from every section.
- foreach (double strain in aggregatePeaks)
- {
- difficulty += strain * weight;
- weight *= 0.9;
- }
-
- return difficulty;
- }
-
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- for (int i = 1; i < beatmap.HitObjects.Count; i++)
- yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
+ var sortedObjects = beatmap.HitObjects.ToArray();
+
+ LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
+
+ for (int i = 1; i < sortedObjects.Length; i++)
+ yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
}
- protected override Skill[] CreateSkills(IBeatmap beatmap)
+ // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
+ protected override IEnumerable SortObjects(IEnumerable input) => input;
+
+ protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
- int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
-
- var skills = new List { new Overall(columnCount) };
-
- for (int i = 0; i < columnCount; i++)
- skills.Add(new Individual(i, columnCount));
-
- return skills.ToArray();
- }
+ new Strain(((ManiaBeatmap)beatmap).TotalColumns)
+ };
protected override Mod[] DifficultyAdjustmentMods
{
@@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
+ new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(),
+ new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(),
+ new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(),
+ new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(),
+ new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray();
}
}
+
+ private int getHitWindow300(Mod[] mods)
+ {
+ if (isForCurrentRuleset)
+ {
+ double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
+ return applyModAdjustments(34 + 3 * od, mods);
+ }
+
+ if (Math.Round(originalOverallDifficulty) > 4)
+ return applyModAdjustments(34, mods);
+
+ return applyModAdjustments(47, mods);
+
+ static int applyModAdjustments(double value, Mod[] mods)
+ {
+ if (mods.Any(m => m is ManiaModHardRock))
+ value /= 1.4;
+ else if (mods.Any(m => m is ManiaModEasy))
+ value *= 1.4;
+
+ if (mods.Any(m => m is ManiaModDoubleTime))
+ value *= 1.5;
+ else if (mods.Any(m => m is ManiaModHalfTime))
+ value *= 0.75;
+
+ return (int)value;
+ }
+ }
+
+ private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
+ {
+ double scoreMultiplier = 1;
+
+ foreach (var m in mods)
+ {
+ switch (m)
+ {
+ case ManiaModNoFail _:
+ case ManiaModEasy _:
+ case ManiaModHalfTime _:
+ scoreMultiplier *= 0.5;
+ break;
+ }
+ }
+
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+ int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
+
+ if (diff > 0)
+ scoreMultiplier *= 0.9;
+ else if (diff < 0)
+ scoreMultiplier *= 0.9 + 0.04 * diff;
+
+ return scoreMultiplier;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
deleted file mode 100644
index 4f7ab87fad..0000000000
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Objects;
-
-namespace osu.Game.Rulesets.Mania.Difficulty.Skills
-{
- public class Individual : Skill
- {
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.125;
-
- private readonly double[] holdEndTimes;
-
- private readonly int column;
-
- public Individual(int column, int columnCount)
- {
- this.column = column;
-
- holdEndTimes = new double[columnCount];
- }
-
- protected override double StrainValueOf(DifficultyHitObject current)
- {
- var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = maniaCurrent.BaseObject.GetEndTime();
-
- try
- {
- if (maniaCurrent.BaseObject.Column != column)
- return 0;
-
- // We give a slight bonus if something is held meanwhile
- return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
- }
- finally
- {
- holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
deleted file mode 100644
index bbbb93fd8b..0000000000
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Objects;
-
-namespace osu.Game.Rulesets.Mania.Difficulty.Skills
-{
- public class Overall : Skill
- {
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.3;
-
- private readonly double[] holdEndTimes;
-
- private readonly int columnCount;
-
- public Overall(int columnCount)
- {
- this.columnCount = columnCount;
-
- holdEndTimes = new double[columnCount];
- }
-
- protected override double StrainValueOf(DifficultyHitObject current)
- {
- var maniaCurrent = (ManiaDifficultyHitObject)current;
- var endTime = maniaCurrent.BaseObject.GetEndTime();
-
- double holdFactor = 1.0; // Factor in case something else is held
- double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
-
- for (int i = 0; i < columnCount; i++)
- {
- // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
- if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
- holdAddition = 1.0;
-
- // ... this addition only is valid if there is _no_ other note with the same ending.
- // Releasing multiple notes at the same time is just as easy as releasing one
- if (endTime == holdEndTimes[i])
- holdAddition = 0;
-
- // We give a slight bonus if something is held meanwhile
- if (holdEndTimes[i] > endTime)
- holdFactor = 1.25;
- }
-
- holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
-
- return (1 + holdAddition) * holdFactor;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
new file mode 100644
index 0000000000..7ebc1ff752
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mania.Difficulty.Skills
+{
+ public class Strain : Skill
+ {
+ private const double individual_decay_base = 0.125;
+ private const double overall_decay_base = 0.30;
+
+ protected override double SkillMultiplier => 1;
+ protected override double StrainDecayBase => 1;
+
+ private readonly double[] holdEndTimes;
+ private readonly double[] individualStrains;
+
+ private double individualStrain;
+ private double overallStrain;
+
+ public Strain(int totalColumns)
+ {
+ holdEndTimes = new double[totalColumns];
+ individualStrains = new double[totalColumns];
+ overallStrain = 1;
+ }
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ var maniaCurrent = (ManiaDifficultyHitObject)current;
+ var endTime = maniaCurrent.BaseObject.GetEndTime();
+ var column = maniaCurrent.BaseObject.Column;
+
+ double holdFactor = 1.0; // Factor to all additional strains in case something else is held
+ double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
+
+ // Fill up the holdEndTimes array
+ for (int i = 0; i < holdEndTimes.Length; ++i)
+ {
+ // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
+ if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
+ holdAddition = 1.0;
+
+ // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
+ if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
+ holdAddition = 0;
+
+ // We give a slight bonus to everything if something is held meanwhile
+ if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
+ holdFactor = 1.25;
+
+ // Decay individual strains
+ individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
+ }
+
+ holdEndTimes[column] = endTime;
+
+ // Increase individual strain in own column
+ individualStrains[column] += 2.0 * holdFactor;
+ individualStrain = individualStrains[column];
+
+ overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
+
+ return individualStrain + overallStrain - CurrentStrain;
+ }
+
+ protected override double GetPeakStrain(double offset)
+ => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
+ + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
+
+ private double applyDecay(double value, double deltaTime, double decayBase)
+ => value * Math.Pow(decayBase, deltaTime / 1000);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
new file mode 100644
index 0000000000..0f4829028f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
@@ -0,0 +1,165 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+
+namespace osu.Game.Rulesets.Mania.MathUtils
+{
+ ///
+ /// Provides access to .NET4.0 unstable sorting methods.
+ ///
+ ///
+ /// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
+ /// Copyright (c) Microsoft Corporation. All rights reserved.
+ ///
+ internal static class LegacySortHelper
+ {
+ private const int quick_sort_depth_threshold = 32;
+
+ public static void Sort(T[] keys, IComparer comparer)
+ {
+ if (keys == null)
+ throw new ArgumentNullException(nameof(keys));
+
+ if (keys.Length == 0)
+ return;
+
+ comparer ??= Comparer.Default;
+ depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
+ }
+
+ private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer comparer, int depthLimit)
+ {
+ do
+ {
+ if (depthLimit == 0)
+ {
+ heapsort(keys, left, right, comparer);
+ return;
+ }
+
+ int i = left;
+ int j = right;
+
+ // pre-sort the low, middle (pivot), and high values in place.
+ // this improves performance in the face of already sorted data, or
+ // data that is made up of multiple sorted runs appended together.
+ int middle = i + ((j - i) >> 1);
+ swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
+ swapIfGreater(keys, comparer, i, j); // swap the low with the high
+ swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
+
+ T x = keys[middle];
+
+ do
+ {
+ while (comparer.Compare(keys[i], x) < 0) i++;
+ while (comparer.Compare(x, keys[j]) < 0) j--;
+ Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
+ if (i > j) break;
+
+ if (i < j)
+ {
+ T key = keys[i];
+ keys[i] = keys[j];
+ keys[j] = key;
+ }
+
+ i++;
+ j--;
+ } while (i <= j);
+
+ // The next iteration of the while loop is to "recursively" sort the larger half of the array and the
+ // following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
+ // both sorts see the new value.
+ depthLimit--;
+
+ if (j - left <= right - i)
+ {
+ if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
+ left = i;
+ }
+ else
+ {
+ if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
+ right = j;
+ }
+ } while (left < right);
+ }
+
+ private static void heapsort(T[] keys, int lo, int hi, IComparer comparer)
+ {
+ Contract.Requires(keys != null);
+ Contract.Requires(comparer != null);
+ Contract.Requires(lo >= 0);
+ Contract.Requires(hi > lo);
+ Contract.Requires(hi < keys.Length);
+
+ int n = hi - lo + 1;
+
+ for (int i = n / 2; i >= 1; i = i - 1)
+ {
+ downHeap(keys, i, n, lo, comparer);
+ }
+
+ for (int i = n; i > 1; i = i - 1)
+ {
+ swap(keys, lo, lo + i - 1);
+ downHeap(keys, 1, i - 1, lo, comparer);
+ }
+ }
+
+ private static void downHeap(T[] keys, int i, int n, int lo, IComparer comparer)
+ {
+ Contract.Requires(keys != null);
+ Contract.Requires(comparer != null);
+ Contract.Requires(lo >= 0);
+ Contract.Requires(lo < keys.Length);
+
+ T d = keys[lo + i - 1];
+
+ while (i <= n / 2)
+ {
+ var child = 2 * i;
+
+ if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
+ {
+ child++;
+ }
+
+ if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
+ break;
+
+ keys[lo + i - 1] = keys[lo + child - 1];
+ i = child;
+ }
+
+ keys[lo + i - 1] = d;
+ }
+
+ private static void swap(T[] a, int i, int j)
+ {
+ if (i != j)
+ {
+ T t = a[i];
+ a[i] = a[j];
+ a[j] = t;
+ }
+ }
+
+ private static void swapIfGreater(T[] keys, IComparer comparer, int a, int b)
+ {
+ if (a != b)
+ {
+ if (comparer.Compare(keys[a], keys[b]) > 0)
+ {
+ T key = keys[a];
+ keys[a] = keys[b];
+ keys[b] = key;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index 13fdd74113..8fd5950dfb 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModKey7),
typeof(ManiaModKey8),
typeof(ManiaModKey9),
+ typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray();
}
}
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 760a033aff..5c7adb3f49 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
}
+ [Test]
+ public void TestMultiModFlattening()
+ {
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
+
+ Assert.AreEqual(4, combinations.Length);
+ Assert.IsTrue(combinations[0] is ModNoMod);
+ Assert.IsTrue(combinations[1] is ModA);
+ Assert.IsTrue(combinations[2] is MultiMod);
+ Assert.IsTrue(combinations[3] is MultiMod);
+
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
+ Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
+ Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
+ }
+
+ [Test]
+ public void TestIncompatibleThroughMultiMod()
+ {
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
+
+ Assert.AreEqual(3, combinations.Length);
+ Assert.IsTrue(combinations[0] is ModNoMod);
+ Assert.IsTrue(combinations[1] is ModA);
+ Assert.IsTrue(combinations[2] is MultiMod);
+
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
+ }
+
+ [Test]
+ public void TestIncompatibleWithSameInstanceViaMultiMod()
+ {
+ var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
+
+ Assert.AreEqual(3, combinations.Length);
+ Assert.IsTrue(combinations[0] is ModNoMod);
+ Assert.IsTrue(combinations[1] is ModA);
+ Assert.IsTrue(combinations[2] is MultiMod);
+
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
+ Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ }
+
private class ModA : Mod
{
public override string Name => nameof(ModA);
@@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
}
+ private class ModC : Mod
+ {
+ public override string Name => nameof(ModC);
+ public override string Acronym => nameof(ModC);
+ public override double ScoreMultiplier => 1;
+ }
+
private class ModIncompatibleWithA : Mod
{
public override string Name => $"Incompatible With {nameof(ModA)}";
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
index c5ce3751ef..645b83758c 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneModSettings : OsuTestScene
+ public class TestSceneModSettings : OsuManualInputManagerTestScene
{
private TestModSelectOverlay modSelect;
@@ -95,6 +97,41 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
}
+ [Test]
+ public void TestMultiModSettingsUnboundWhenCopied()
+ {
+ MultiMod original = null;
+ MultiMod copy = null;
+
+ AddStep("create mods", () =>
+ {
+ original = new MultiMod(new OsuModDoubleTime());
+ copy = (MultiMod)original.CreateCopy();
+ });
+
+ AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
+
+ AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
+ AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
+ }
+
+ [Test]
+ public void TestCustomisationMenuNoClickthrough()
+ {
+ createModSelect();
+ openModSelect();
+
+ AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
+ AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
+ AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1);
+ AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
+ AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
+ AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
+ AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
+ AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
+ AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
+ }
+
private void createModSelect()
{
AddStep("create mod select", () =>
@@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
+ public ModButton GetModButton(Mod mod)
+ {
+ return ModSectionsContainer.ChildrenOfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
+ }
+
public void SelectMod(Mod mod) =>
- ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
- .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
+ GetModButton(mod).SelectNext(1);
+
+ public void SetModSettingsWidth(float newWidth) =>
+ ModSettingsContainer.Width = newWidth;
}
public class TestRulesetInfo : RulesetInfo
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 4eb4fc6501..31adf47456 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
@@ -45,9 +44,7 @@ namespace osu.Game.Overlays.Mods
protected readonly FillFlowContainer ModSectionsContainer;
- protected readonly FillFlowContainer ModSettingsContent;
-
- protected readonly Container ModSettingsContainer;
+ protected readonly ModSettingsContainer ModSettingsContainer;
public readonly Bindable> SelectedMods = new Bindable>(Array.Empty());
@@ -284,7 +281,7 @@ namespace osu.Game.Overlays.Mods
},
},
},
- ModSettingsContainer = new Container
+ ModSettingsContainer = new ModSettingsContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
@@ -292,29 +289,11 @@ namespace osu.Game.Overlays.Mods
Width = 0.25f,
Alpha = 0,
X = -100,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = new Color4(0, 0, 0, 192)
- },
- new OsuScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- Child = ModSettingsContent = new FillFlowContainer
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0f, 10f),
- Padding = new MarginPadding(20),
- }
- }
- }
+ SelectedMods = { BindTarget = SelectedMods },
}
};
+
+ ((IBindable)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection);
}
[BackgroundDependencyLoader(true)]
@@ -423,8 +402,6 @@ namespace osu.Game.Overlays.Mods
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
updateMods();
-
- updateModSettings(mods);
}
private void updateMods()
@@ -445,25 +422,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(Color4.White, 200);
}
- private void updateModSettings(ValueChangedEvent> selectedMods)
- {
- ModSettingsContent.Clear();
-
- foreach (var mod in selectedMods.NewValue)
- {
- var settings = mod.CreateSettingsControls().ToList();
- if (settings.Count > 0)
- ModSettingsContent.Add(new ModControlSection(mod, settings));
- }
-
- bool hasSettings = ModSettingsContent.Count > 0;
-
- CustomiseButton.Enabled.Value = hasSettings;
-
- if (!hasSettings)
- ModSettingsContainer.Hide();
- }
-
private void modButtonPressed(Mod selectedMod)
{
if (selectedMod != null)
diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs
new file mode 100644
index 0000000000..b185b56ecd
--- /dev/null
+++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Mods;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Mods
+{
+ public class ModSettingsContainer : Container
+ {
+ public readonly IBindable> SelectedMods = new Bindable>(Array.Empty());
+
+ public IBindable HasSettingsForSelection => hasSettingsForSelection;
+
+ private readonly Bindable hasSettingsForSelection = new Bindable();
+
+ private readonly FillFlowContainer modSettingsContent;
+
+ public ModSettingsContainer()
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(0, 0, 0, 192)
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = modSettingsContent = new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0f, 10f),
+ Padding = new MarginPadding(20),
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ SelectedMods.BindValueChanged(modsChanged, true);
+ }
+
+ private void modsChanged(ValueChangedEvent> mods)
+ {
+ modSettingsContent.Clear();
+
+ foreach (var mod in mods.NewValue)
+ {
+ var settings = mod.CreateSettingsControls().ToList();
+ if (settings.Count > 0)
+ modSettingsContent.Add(new ModControlSection(mod, settings));
+ }
+
+ bool hasSettings = modSettingsContent.Count > 0;
+
+ if (!hasSettings)
+ Hide();
+
+ hasSettingsForSelection.Value = hasSettings;
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e) => true;
+ protected override bool OnHover(HoverEvent e) => true;
+ }
+}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 1902de5bda..f15e5e1df0 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Difficulty
if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
- var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList();
+ var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
double sectionLength = SectionLength * clockRate;
@@ -100,15 +100,24 @@ namespace osu.Game.Rulesets.Difficulty
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
}
+ ///
+ /// Sorts a given set of s.
+ ///
+ /// The s to sort.
+ /// The sorted s.
+ protected virtual IEnumerable SortObjects(IEnumerable input)
+ => input.OrderBy(h => h.BaseObject.StartTime);
+
///
/// Creates all combinations which adjust the difficulty.
///
public Mod[] CreateDifficultyAdjustmentModCombinations()
{
- return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray();
+ return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty()).ToArray();
- IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0)
+ static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory remainingMods, IEnumerable currentSet, int currentSetCount = 0)
{
+ // Return the current set.
switch (currentSetCount)
{
case 0:
@@ -128,18 +137,43 @@ namespace osu.Game.Rulesets.Difficulty
break;
}
- // Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod
- // combinations in further recursions, so a moving subset is used to eliminate this effect
- for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
+ // Apply the rest of the remaining mods recursively.
+ for (int i = 0; i < remainingMods.Length; i++)
{
- var adjustmentMod = adjustmentSet[i];
- if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
+ var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
+
+ // Check if any mods in the next set are incompatible with any of the current set.
+ if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
continue;
- foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1))
+ // Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
+ if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
+ continue;
+
+ // If all's good, attach the next set to the current set and recurse further.
+ foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
yield return combo;
}
}
+
+ // Flattens a mod hierarchy (through MultiMod) as an IEnumerable
+ static (IEnumerable set, int count) flatten(Mod mod)
+ {
+ if (!(mod is MultiMod multi))
+ return (mod.Yield(), 1);
+
+ IEnumerable set = Enumerable.Empty();
+ int count = 0;
+
+ foreach (var nested in multi.Mods)
+ {
+ var (nestedSet, nestedCount) = flatten(nested);
+ set = set.Concat(nestedSet);
+ count += nestedCount;
+ }
+
+ return (set, count);
+ }
}
///
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
index 227f2f4018..1063a24b27 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -41,7 +41,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
protected readonly LimitedCapacityStack Previous = new LimitedCapacityStack(2); // Contained objects not used yet
- private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
+ ///
+ /// The current strain level.
+ ///
+ protected double CurrentStrain { get; private set; } = 1;
+
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List strainPeaks = new List();
@@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
public void Process(DifficultyHitObject current)
{
- currentStrain *= strainDecay(current.DeltaTime);
- currentStrain += StrainValueOf(current) * SkillMultiplier;
+ CurrentStrain *= strainDecay(current.DeltaTime);
+ CurrentStrain += StrainValueOf(current) * SkillMultiplier;
- currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
+ currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Previous.Push(current);
}
@@ -71,15 +75,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
///
/// Sets the initial strain level for a new section.
///
- /// The beginning of the new section in milliseconds.
- public void StartNewSectionFrom(double offset)
+ /// The beginning of the new section in milliseconds.
+ public void StartNewSectionFrom(double time)
{
// 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);
+ currentSectionPeak = GetPeakStrain(time);
}
+ ///
+ /// Retrieves the peak strain at a point in time.
+ ///
+ /// The time to retrieve the peak strain at.
+ /// The peak strain.
+ protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime);
+
///
/// Returns the calculated difficulty value representing all processed s.
///
diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs
index f7d574d3c7..2107009dbb 100644
--- a/osu.Game/Rulesets/Mods/MultiMod.cs
+++ b/osu.Game/Rulesets/Mods/MultiMod.cs
@@ -6,7 +6,7 @@ using System.Linq;
namespace osu.Game.Rulesets.Mods
{
- public class MultiMod : Mod
+ public sealed class MultiMod : Mod
{
public override string Name => string.Empty;
public override string Acronym => string.Empty;
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods
Mods = mods;
}
+ public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
+
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 26aefa138b..14ceadac81 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -63,6 +63,8 @@ namespace osu.Game.Screens.Play
private readonly Container topScoreContainer;
+ private readonly FillFlowContainer bottomRightElements;
+
private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods)
@@ -80,32 +82,55 @@ namespace osu.Game.Screens.Play
visibilityContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Child = new GridContainer
{
- HealthDisplay = CreateHealthDisplay(),
- topScoreContainer = new Container
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
+ new Drawable[]
{
- AccuracyCounter = CreateAccuracyCounter(),
- ScoreCounter = CreateScoreCounter(),
- ComboCounter = CreateComboCounter(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ HealthDisplay = CreateHealthDisplay(),
+ topScoreContainer = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ AccuracyCounter = CreateAccuracyCounter(),
+ ScoreCounter = CreateScoreCounter(),
+ ComboCounter = CreateComboCounter(),
+ },
+ },
+ ComboCounter = CreateComboCounter(),
+ ModDisplay = CreateModsContainer(),
+ HitErrorDisplay = CreateHitErrorDisplayOverlay(),
+ PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
+ }
+ },
},
+ new Drawable[]
+ {
+ Progress = CreateProgress(),
+ }
},
- Progress = CreateProgress(),
- ModDisplay = CreateModsContainer(),
- HitErrorDisplay = CreateHitErrorDisplayOverlay(),
- PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
- }
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ }
+ },
},
- new FillFlowContainer
+ bottomRightElements = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
- Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y),
+ X = -5,
AutoSizeAxes = Axes.Both,
LayoutDuration = fade_duration / 2,
LayoutEasing = fade_easing,
@@ -186,6 +211,12 @@ namespace osu.Game.Screens.Play
replayLoaded.BindValueChanged(replayLoadedValueChanged, true);
}
+ protected override void Update()
+ {
+ base.Update();
+ bottomRightElements.Y = -Progress.Height;
+ }
+
private void replayLoadedValueChanged(ValueChangedEvent e)
{
PlayerSettingsOverlay.ReplayLoaded = e.NewValue;
diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs
index aa745f5ba2..acf4640aa4 100644
--- a/osu.Game/Screens/Play/SongProgress.cs
+++ b/osu.Game/Screens/Play/SongProgress.cs
@@ -70,7 +70,6 @@ namespace osu.Game.Screens.Play
public SongProgress()
{
Masking = true;
- Height = bottom_bar_height + graph_height + handle_size.Y + info_height;
Children = new Drawable[]
{
@@ -148,6 +147,8 @@ namespace osu.Game.Screens.Play
bar.CurrentTime = gameplayTime;
graph.Progress = (int)(graph.ColumnCount * progress);
+
+ Height = bottom_bar_height + graph_height + handle_size.Y + info_height - graph.Y;
}
private void updateBarVisibility()