diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index fe63f5faf3..680312ad27 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index c3c755ecd7..6c36ad052e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index 47224bd195..22db147e32 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
public class CatchLegacySkinTransformer : LegacySkinTransformer
{
+ ///
+ /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
+ ///
+ private bool providesComboCounter => this.HasFont(GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score");
+
public CatchLegacySkinTransformer(ISkinSource source)
: base(source)
{
@@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component)
{
+ if (component is HUDSkinComponent hudComponent)
+ {
+ switch (hudComponent.Component)
+ {
+ case HUDSkinComponents.ComboCounter:
+ // catch may provide its own combo counter; hide the default.
+ return providesComboCounter ? Drawable.Empty() : null;
+ }
+ }
+
if (!(component is CatchSkinComponent catchSkinComponent))
return null;
@@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
- var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
- // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
- if (this.HasFont(comboFont))
- return new LegacyComboCounter(Source);
+ if (providesComboCounter)
+ return new LegacyCatchComboCounter(Source);
break;
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
similarity index 96%
rename from osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
rename to osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
index c8abc9e832..34608b07ff 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
///
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
///
- public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
+ public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
- public LegacyComboCounter(ISkin skin)
+ public LegacyCatchComboCounter(ISkin skin)
{
var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f;
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/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 8b22309033..0784109158 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
- private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
+ private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name)
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/Resources/old-skin/score-0.png b/osu.Game.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000..8304617d8c
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-0.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-1.png b/osu.Game.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000..c3b85eb873
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-1.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-2.png b/osu.Game.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000..7f65eb7ca7
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-2.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-3.png b/osu.Game.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000..82bec3babe
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-3.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-4.png b/osu.Game.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000..5e38c75a9d
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-4.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-5.png b/osu.Game.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000..a562d9f2ac
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-5.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-6.png b/osu.Game.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000..b4cf81f26e
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-6.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-7.png b/osu.Game.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000..a23f5379b2
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-7.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-8.png b/osu.Game.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000..430b18509d
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-8.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-9.png b/osu.Game.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000..add1202c31
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-9.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-comma.png b/osu.Game.Tests/Resources/old-skin/score-comma.png
new file mode 100644
index 0000000000..f68d32957f
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-comma.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-dot.png b/osu.Game.Tests/Resources/old-skin/score-dot.png
new file mode 100644
index 0000000000..80c39b8745
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-dot.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-percent.png b/osu.Game.Tests/Resources/old-skin/score-percent.png
new file mode 100644
index 0000000000..fc750abc7e
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-percent.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/score-x.png b/osu.Game.Tests/Resources/old-skin/score-x.png
new file mode 100644
index 0000000000..779773f8bd
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/score-x.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-bg.png b/osu.Game.Tests/Resources/old-skin/scorebar-bg.png
new file mode 100644
index 0000000000..1e94f464ca
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-bg.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png
new file mode 100644
index 0000000000..1119ce289e
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png
new file mode 100644
index 0000000000..7669474d8b
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png
new file mode 100644
index 0000000000..70fdb4b146
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png
new file mode 100644
index 0000000000..18ac6976c9
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-ki.png b/osu.Game.Tests/Resources/old-skin/scorebar-ki.png
new file mode 100644
index 0000000000..a030c5801e
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-ki.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png
new file mode 100644
index 0000000000..ac5a2c5893
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png
new file mode 100644
index 0000000000..507be0463f
Binary files /dev/null and b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png differ
diff --git a/osu.Game.Tests/Resources/old-skin/skin.ini b/osu.Game.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..5369de24e9
--- /dev/null
+++ b/osu.Game.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,2 @@
+[General]
+Version: 1.0
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs
new file mode 100644
index 0000000000..d0c2fb5064
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneComboCounter : SkinnableTestScene
+ {
+ private IEnumerable comboCounters => CreatedDrawables.OfType();
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create combo counters", () => SetContents(() =>
+ {
+ var comboCounter = new SkinnableComboCounter();
+ comboCounter.Current.Value = 1;
+ return comboCounter;
+ }));
+ }
+
+ [Test]
+ public void TestComboCounterIncrementing()
+ {
+ AddRepeatStep("increase combo", () =>
+ {
+ foreach (var counter in comboCounters)
+ counter.Current.Value++;
+ }, 10);
+
+ AddStep("reset combo", () =>
+ {
+ foreach (var counter in comboCounters)
+ counter.Current.Value = 0;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
index 5bb3851264..6e505b16c2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("get variables", () =>
{
- gameplayClock = Player.ChildrenOfType().First().GameplayClock;
+ gameplayClock = Player.ChildrenOfType().First();
slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType().ToArray();
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index c192a7b0e0..603b5d4956 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -2,23 +2,29 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
- public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
+ public class TestSceneHUDOverlay : SkinnableTestScene
{
private HUDOverlay hudOverlay;
+ private IEnumerable hudOverlays => CreatedDrawables.OfType();
+
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
@@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private OsuConfigManager config { get; set; }
+ [Test]
+ public void TestComboCounterIncrementing()
+ {
+ createNew();
+
+ AddRepeatStep("increase combo", () =>
+ {
+ foreach (var hud in hudOverlays)
+ hud.ComboCounter.Current.Value++;
+ }, 10);
+
+ AddStep("reset combo", () =>
+ {
+ foreach (var hud in hudOverlays)
+ hud.ComboCounter.Current.Value = 0;
+ });
+ }
+
[Test]
public void TestShownByDefault()
{
@@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
AddUntilStep("wait for load", () => hudOverlay.IsAlive);
- AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1);
+ AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
}
[Test]
@@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
createNew();
- AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set keycounter visible false", () =>
{
config.Set(OsuSetting.KeyOverlay, false);
- hudOverlay.KeyCounter.AlwaysVisible.Value = false;
+ hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
});
- AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
- AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
+ AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
@@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create overlay", () =>
{
- Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
+ SetContents(() =>
+ {
+ hudOverlay = new HUDOverlay(null, null, null, Array.Empty());
- // Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ // Add any key just to display the key counter visually.
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
- action?.Invoke(hudOverlay);
+ hudOverlay.ComboCounter.Current.Value = 1;
+
+ action?.Invoke(hudOverlay);
+
+ return hudOverlay;
+ });
});
}
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 377f305d63..1021ac3760 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private BarHitErrorMeter barMeter;
private BarHitErrorMeter barMeter2;
+ private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2;
+ private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows;
public TestSceneHitErrorMeter()
@@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft,
});
+ Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.CentreLeft,
+ Rotation = 270,
+ });
+
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.CentreRight,
@@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 }
});
+
+ Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.CentreLeft,
+ Rotation = 270,
+ Margin = new MarginPadding { Left = 50 }
+ });
}
private void newJudgement(double offset = 0)
@@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement);
+ barMeter3.OnNewJudgement(judgement);
colourMeter.OnNewJudgement(judgement);
colourMeter2.OnNewJudgement(judgement);
+ colourMeter3.OnNewJudgement(judgement);
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
deleted file mode 100644
index 09b4f9b761..0000000000
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs
+++ /dev/null
@@ -1,68 +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 NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Screens.Play.HUD;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.Gameplay
-{
- [TestFixture]
- public class TestSceneScoreCounter : OsuTestScene
- {
- public TestSceneScoreCounter()
- {
- int numerator = 0, denominator = 0;
-
- ScoreCounter score = new ScoreCounter(7)
- {
- Origin = Anchor.TopRight,
- Anchor = Anchor.TopRight,
- Margin = new MarginPadding(20),
- };
- Add(score);
-
- ComboCounter comboCounter = new StandardComboCounter
- {
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Margin = new MarginPadding(10),
- };
- Add(comboCounter);
-
- PercentageCounter accuracyCounter = new PercentageCounter
- {
- Origin = Anchor.TopRight,
- Anchor = Anchor.TopRight,
- Position = new Vector2(-20, 60),
- };
- Add(accuracyCounter);
-
- AddStep(@"Reset all", delegate
- {
- score.Current.Value = 0;
- comboCounter.Current.Value = 0;
- numerator = denominator = 0;
- accuracyCounter.SetFraction(0, 0);
- });
-
- AddStep(@"Hit! :D", delegate
- {
- score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0);
- comboCounter.Increment();
- numerator++;
- denominator++;
- accuracyCounter.SetFraction(numerator, denominator);
- });
-
- AddStep(@"miss...", delegate
- {
- comboCounter.Current.Value = 0;
- denominator++;
- accuracyCounter.SetFraction(numerator, denominator);
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs
new file mode 100644
index 0000000000..709929dcb0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene
+ {
+ private IEnumerable accuracyCounters => CreatedDrawables.OfType();
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create combo counters", () => SetContents(() =>
+ {
+ var accuracyCounter = new SkinnableAccuracyCounter();
+
+ accuracyCounter.Current.Value = 1;
+
+ return accuracyCounter;
+ }));
+ }
+
+ [Test]
+ public void TestChangingAccuracy()
+ {
+ AddStep(@"Reset all", delegate
+ {
+ foreach (var s in accuracyCounters)
+ s.Current.Value = 1;
+ });
+
+ AddStep(@"Hit! :D", delegate
+ {
+ foreach (var s in accuracyCounters)
+ s.Current.Value -= 0.023f;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs
new file mode 100644
index 0000000000..e1b0820662
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
+ {
+ private IEnumerable healthDisplays => CreatedDrawables.OfType();
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create health displays", () =>
+ {
+ SetContents(() => new SkinnableHealthDisplay());
+ });
+ AddStep(@"Reset all", delegate
+ {
+ foreach (var s in healthDisplays)
+ s.Current.Value = 1;
+ });
+ }
+
+ [Test]
+ public void TestHealthDisplayIncrementing()
+ {
+ AddRepeatStep(@"decrease hp", delegate
+ {
+ foreach (var healthDisplay in healthDisplays)
+ healthDisplay.Current.Value -= 0.08f;
+ }, 10);
+
+ AddRepeatStep(@"increase hp without flash", delegate
+ {
+ foreach (var healthDisplay in healthDisplays)
+ healthDisplay.Current.Value += 0.1f;
+ }, 3);
+
+ AddRepeatStep(@"increase hp with flash", delegate
+ {
+ foreach (var healthDisplay in healthDisplays)
+ {
+ healthDisplay.Current.Value += 0.1f;
+ healthDisplay.Flash(new JudgementResult(null, new OsuJudgement()));
+ }
+ }, 3);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
new file mode 100644
index 0000000000..2d5003d1da
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableScoreCounter : SkinnableTestScene
+ {
+ private IEnumerable scoreCounters => CreatedDrawables.OfType();
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Create combo counters", () => SetContents(() =>
+ {
+ var comboCounter = new SkinnableScoreCounter();
+ comboCounter.Current.Value = 1;
+ return comboCounter;
+ }));
+ }
+
+ [Test]
+ public void TestScoreCounterIncrementing()
+ {
+ AddStep(@"Reset all", delegate
+ {
+ foreach (var s in scoreCounters)
+ s.Current.Value = 0;
+ });
+
+ AddStep(@"Hit! :D", delegate
+ {
+ foreach (var s in scoreCounters)
+ s.Current.Value += 300;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index 8f2011e5dd..864e88d023 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
-using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
- [Cached(typeof(ISamplePlaybackDisabler))]
- private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
-
private TestSkinSourceContainer skinSource;
private PausableSkinnableSound skinnableSound;
[SetUp]
- public void SetUp() => Schedule(() =>
+ public void SetUpSteps()
{
- gameplayClock.IsPaused.Value = false;
-
- Children = new Drawable[]
+ AddStep("setup hierarchy", () =>
{
- skinSource = new TestSkinSourceContainer
+ Children = new Drawable[]
{
- Clock = gameplayClock,
- RelativeSizeAxes = Axes.Both,
- Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
- },
- };
- });
+ skinSource = new TestSkinSourceContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
+ },
+ };
+ });
+ }
[Test]
public void TestStoppedSoundDoesntResumeAfterPause()
@@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
- AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
- AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+ AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
+
+ AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("sample not playing", () => !sample.Playing);
@@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for sample to start playing", () => sample.Playing);
- AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
+ AddUntilStep("wait for sample to start playing", () => sample.Playing);
}
[Test]
@@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing);
- AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
- AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
- AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+ AddUntilStep("sample not playing", () => !sample.Playing);
+
+ AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
@@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playing", () => sample.Playing);
- AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
@@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddAssert("new sample stopped", () => !sample.Playing);
- AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+ AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("new sample not played", () => !sample.Playing);
}
[Cached(typeof(ISkinSource))]
- private class TestSkinSourceContainer : Container, ISkinSource
+ [Cached(typeof(ISamplePlaybackDisabler))]
+ private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{
[Resolved]
private ISkinSource source { get; set; }
public event Action SourceChanged;
+ public Bindable SamplePlaybackDisabled { get; } = new Bindable();
+
+ IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
+
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 0ba8cfeb28..4699784327 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
- AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
+ AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
}
[Test]
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.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index 7dc5ce1d7f..f9613d9e25 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.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.IO;
using System.Linq;
using osu.Framework.Audio;
@@ -59,7 +60,7 @@ namespace osu.Game.Tests
get
{
using (var reader = getZipReader())
- return reader.Filenames.First(f => f.EndsWith(".mp3"));
+ return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal));
}
}
@@ -73,7 +74,7 @@ namespace osu.Game.Tests
protected override Beatmap CreateBeatmap()
{
using (var reader = getZipReader())
- using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))))
+ using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal))))
using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader);
}
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
index e10154b722..4c3adeae76 100644
--- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings
if (string.IsNullOrEmpty(line))
continue;
- if (line.ToUpperInvariant().StartsWith("GROUP"))
+ if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal))
continue;
// ReSharper disable once AccessToModifiedClosure
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index acab525821..8d1f0e59bf 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public string StoredBookmarks
{
- get => string.Join(",", Bookmarks);
+ get => string.Join(',', Bookmarks);
set
{
if (string.IsNullOrEmpty(value))
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 4c75069f08..33e024fa28 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
@@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps
///
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
///
+ [ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable
{
///
@@ -389,7 +391,7 @@ namespace osu.Game.Beatmaps
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
- string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
+ string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(mapName))
{
@@ -417,7 +419,7 @@ namespace osu.Game.Beatmaps
{
var beatmapInfos = new List();
- foreach (var file in files.Where(f => f.Filename.EndsWith(".osu")))
+ foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
{
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 16207c7d2a..cb4884aa51 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -13,6 +13,7 @@ using osu.Framework.Development;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
+ [ExcludeFromDynamicCompile]
private class BeatmapOnlineLookupQueue : IDisposable
{
private readonly IAPIProvider api;
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 362c99ea3f..f5c0d97c1f 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
+using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Skinning;
@@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
+ [ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore store;
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index b76d780860..7bc1c8c7b9 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; }
- public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb"))?.Filename;
+ public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
public List Files { get; set; }
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index b30ec0ca2c..6dadbbd2da 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -307,12 +307,7 @@ namespace osu.Game.Beatmaps.Formats
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
- var breakEvent = new BreakPeriod(start, end);
-
- if (!breakEvent.HasEffect)
- return;
-
- beatmap.Breaks.Add(breakEvent);
+ beatmap.Breaks.Add(new BreakPeriod(start, end));
break;
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index c15240a4f6..7b377e481f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats
{
var pair = SplitKeyVal(line);
- bool isCombo = pair.Key.StartsWith(@"Combo");
+ bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
string[] split = pair.Value.Split(',');
diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs
index bb8ae4a66a..4c90b16745 100644
--- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs
+++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
public double Duration => EndTime - StartTime;
///
- /// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap.
+ /// Whether the break has any effect.
///
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs
index 156c4b1377..b9499c758e 100644
--- a/osu.Game/Configuration/ScoreMeterType.cs
+++ b/osu.Game/Configuration/ScoreMeterType.cs
@@ -16,7 +16,10 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")]
HitErrorRight,
- [Description("Hit Error (both)")]
+ [Description("Hit Error (bottom)")]
+ HitErrorBottom,
+
+ [Description("Hit Error (left+right)")]
HitErrorBoth,
[Description("Colour (left)")]
@@ -25,7 +28,10 @@ namespace osu.Game.Configuration
[Description("Colour (right)")]
ColourRight,
- [Description("Colour (both)")]
+ [Description("Colour (left+right)")]
ColourBoth,
+
+ [Description("Colour (bottom)")]
+ ColourBottom,
}
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 3292936f5f..8bdc804311 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -279,7 +279,7 @@ namespace osu.Game.Database
// for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
- foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename))
+ foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
{
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable);
@@ -593,7 +593,7 @@ namespace osu.Game.Database
var fileInfos = new List();
string prefix = reader.Filenames.GetCommonPrefix();
- if (!(prefix.EndsWith("/") || prefix.EndsWith("\\")))
+ if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty;
// import files to manager
diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs
index 1ccf7798e5..2d53ec066b 100644
--- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs
+++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Utils;
@@ -28,9 +27,6 @@ namespace osu.Game.Graphics.UserInterface
Current.Value = DisplayedCount = 1.0f;
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.BlueLighter;
-
protected override string FormatCount(double count) => count.FormatAccuracy();
protected override double GetProportionalDuration(double currentValue, double newValue)
diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index 73bbe5f03e..17e5ceedb9 100644
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Screens.Play.HUD;
namespace osu.Game.Graphics.UserInterface
{
- public class ScoreCounter : RollingCounter
+ public abstract class ScoreCounter : RollingCounter, IScoreCounter
{
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out;
- public bool UseCommaSeparator;
+ ///
+ /// Whether comma separators should be displayed.
+ ///
+ public bool UseCommaSeparator { get; }
///
/// How many leading zeroes the counter has.
@@ -23,14 +26,13 @@ namespace osu.Game.Graphics.UserInterface
/// Displays score.
///
/// How many leading zeroes the counter will have.
- public ScoreCounter(uint leading = 0)
+ /// Whether comma separators should be displayed.
+ protected ScoreCounter(uint leading = 0, bool useCommaSeparator = false)
{
+ UseCommaSeparator = useCommaSeparator;
LeadingZeroes = leading;
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.BlueLighter;
-
protected override double GetProportionalDuration(double currentValue, double newValue)
{
return currentValue > newValue ? currentValue - newValue : newValue - currentValue;
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index f7ed57f207..16f46581c5 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Online.Chat
if (target == null)
return;
- var parameters = text.Split(new[] { ' ' }, 2);
+ var parameters = text.Split(' ', 2);
string command = parameters[0];
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 648e4a762b..d2a117876d 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Online.Chat
public static LinkDetails GetLinkDetails(string url)
{
- var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ var args = url.Split('/', StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index d315b213ab..a0ddab702e 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -181,7 +181,7 @@ namespace osu.Game
if (args?.Length > 0)
{
- var paths = args.Where(a => !a.StartsWith(@"-")).ToArray();
+ var paths = args.Where(a => !a.StartsWith('-')).ToArray();
if (paths.Length > 0)
Task.Run(() => Import(paths));
}
@@ -289,7 +289,7 @@ namespace osu.Game
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{
- if (url.StartsWith("/"))
+ if (url.StartsWith('/'))
url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url);
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/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index c27b5f4b4a..ebee377a51 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -135,7 +135,6 @@ namespace osu.Game.Overlays.Profile.Header
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
- anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Lastfm, user.Lastfm, $@"https://last.fm/users/{user.Lastfm}");
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
// If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
@@ -149,7 +148,7 @@ namespace osu.Game.Overlays.Profile.Header
if (string.IsNullOrEmpty(content)) return false;
// newlines could be contained in API returned user content.
- content = content.Replace("\n", " ");
+ content = content.Replace('\n', ' ');
bottomLinkContainer.AddIcon(icon, text =>
{
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/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 6e94a84e7d..08f2ccb75c 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods
{
foreach (var breakPeriod in Breaks)
{
+ if (!breakPeriod.HasEffect)
+ continue;
+
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
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/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 7dcbc52cea..44b22033dc 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
string[] ss = split[5].Split(':');
endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0]));
- readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
+ readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo);
}
result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 5d93f5186b..d422bca087 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -96,11 +96,13 @@ namespace osu.Game.Rulesets
context.SaveChanges();
// add any other modes
+ var existingRulesets = context.RulesetInfo.ToList();
+
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
// todo: StartsWith can be changed to Equals on 2020-11-08
// This is to give users enough time to have their database use new abbreviated info).
- if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null)
+ if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index 70b3d0c7d4..e4a3a2fe3d 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -18,8 +18,11 @@ namespace osu.Game.Rulesets.UI
/// A container which consumes a parent gameplay clock and standardises frame counts for children.
/// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
///
- public class FrameStabilityContainer : Container, IHasReplayHandler
+ [Cached(typeof(ISamplePlaybackDisabler))]
+ public class FrameStabilityContainer : Container, IHasReplayHandler, ISamplePlaybackDisabler
{
+ private readonly Bindable samplePlaybackDisabled = new Bindable();
+
private readonly double gameplayStartTime;
///
@@ -35,7 +38,6 @@ namespace osu.Game.Rulesets.UI
public GameplayClock GameplayClock => stabilityGameplayClock;
[Cached(typeof(GameplayClock))]
- [Cached(typeof(ISamplePlaybackDisabler))]
private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
@@ -102,6 +104,8 @@ namespace osu.Game.Rulesets.UI
requireMoreUpdateLoops = true;
validState = !GameplayClock.IsPaused.Value;
+ samplePlaybackDisabled.Value = stabilityGameplayClock.ShouldDisableSamplePlayback;
+
int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames)
@@ -224,6 +228,8 @@ namespace osu.Game.Rulesets.UI
public ReplayInputHandler ReplayInputHandler { get; set; }
+ IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
+
private class StabilityGameplayClock : GameplayClock
{
public GameplayClock ParentGameplayClock;
@@ -237,7 +243,7 @@ namespace osu.Game.Rulesets.UI
{
}
- protected override bool ShouldDisableSamplePlayback =>
+ public override bool ShouldDisableSamplePlayback =>
// handle the case where playback is catching up to real-time.
base.ShouldDisableSamplePlayback
|| ParentSampleDisabler?.SamplePlaybackDisabled.Value == true
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 5a6da53839..cce6153953 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Scoring
if (archive == null)
return null;
- using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr"))))
+ using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
{
try
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 0336c74386..1527d20f54 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -79,9 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementNewCombo()
{
- if (currentPlacement == null) return;
-
- if (currentPlacement.HitObject is IHasComboInformation c)
+ if (currentPlacement?.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True;
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 7444369e84..c3560dff38 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -597,10 +597,20 @@ namespace osu.Game.Screens.Edit
{
double amount = e.ShiftPressed ? 4 : 1;
+ bool trackPlaying = clock.IsRunning;
+
+ if (trackPlaying)
+ {
+ // generally users are not looking to perform tiny seeks when the track is playing,
+ // so seeks should always be by one full beat, bypassing the beatDivisor.
+ // this multiplication undoes the division that will be applied in the underlying seek operation.
+ amount *= beatDivisor.Value;
+ }
+
if (direction < 1)
- clock.SeekBackward(!clock.IsRunning, amount);
+ clock.SeekBackward(!trackPlaying, amount);
else
- clock.SeekForward(!clock.IsRunning, amount);
+ clock.SeekForward(!trackPlaying, amount);
}
private void exportBeatmap()
diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs
index eeea6777c6..4d0872e5bb 100644
--- a/osu.Game/Screens/Play/GameplayClock.cs
+++ b/osu.Game/Screens/Play/GameplayClock.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play
/// , as this should only be done once to ensure accuracy.
///
///
- public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler
+ public class GameplayClock : IFrameBasedClock
{
private readonly IFrameBasedClock underlyingClock;
@@ -28,8 +28,6 @@ namespace osu.Game.Screens.Play
///
public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>();
- private readonly Bindable samplePlaybackDisabled = new Bindable();
-
public GameplayClock(IFrameBasedClock underlyingClock)
{
this.underlyingClock = underlyingClock;
@@ -66,13 +64,11 @@ namespace osu.Game.Screens.Play
///
/// Whether nested samples supporting the interface should be paused.
///
- protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
+ public virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
public void ProcessFrame()
{
// intentionally not updating the underlying clock (handled externally).
-
- samplePlaybackDisabled.Value = ShouldDisableSamplePlayback;
}
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
@@ -82,7 +78,5 @@ namespace osu.Game.Screens.Play
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock;
-
- IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
}
}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 9f8e55f577..6679e56871 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play
public GameplayClock GameplayClock => localGameplayClock;
[Cached(typeof(GameplayClock))]
- [Cached(typeof(ISamplePlaybackDisabler))]
private readonly LocalGameplayClock localGameplayClock;
private Bindable userAudioOffset;
diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs
deleted file mode 100644
index ea50a4a578..0000000000
--- a/osu.Game/Screens/Play/HUD/ComboCounter.cs
+++ /dev/null
@@ -1,200 +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.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics.Sprites;
-
-namespace osu.Game.Screens.Play.HUD
-{
- public abstract class ComboCounter : Container
- {
- public BindableInt Current = new BindableInt
- {
- MinValue = 0,
- };
-
- public bool IsRolling { get; protected set; }
-
- protected SpriteText PopOutCount;
-
- protected virtual double PopOutDuration => 150;
- protected virtual float PopOutScale => 2.0f;
- protected virtual Easing PopOutEasing => Easing.None;
- protected virtual float PopOutInitialAlpha => 0.75f;
-
- protected virtual double FadeOutDuration => 100;
-
- ///
- /// Duration in milliseconds for the counter roll-up animation for each element.
- ///
- protected virtual double RollingDuration => 20;
-
- ///
- /// Easing for the counter rollover animation.
- ///
- protected Easing RollingEasing => Easing.None;
-
- protected SpriteText DisplayedCountSpriteText;
-
- private int previousValue;
-
- ///
- /// Base of all combo counters.
- ///
- protected ComboCounter()
- {
- AutoSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- DisplayedCountSpriteText = new OsuSpriteText
- {
- Alpha = 0,
- },
- PopOutCount = new OsuSpriteText
- {
- Alpha = 0,
- Margin = new MarginPadding(0.05f),
- }
- };
-
- TextSize = 80;
-
- Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- DisplayedCountSpriteText.Text = FormatCount(Current.Value);
- DisplayedCountSpriteText.Anchor = Anchor;
- DisplayedCountSpriteText.Origin = Origin;
-
- StopRolling();
- }
-
- private int displayedCount;
-
- ///
- /// Value shown at the current moment.
- ///
- public virtual int DisplayedCount
- {
- get => displayedCount;
- protected set
- {
- if (displayedCount.Equals(value))
- return;
-
- updateDisplayedCount(displayedCount, value, IsRolling);
- }
- }
-
- private float textSize;
-
- public float TextSize
- {
- get => textSize;
- set
- {
- textSize = value;
-
- DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: TextSize);
- PopOutCount.Font = PopOutCount.Font.With(size: TextSize);
- }
- }
-
- ///
- /// Increments the combo by an amount.
- ///
- ///
- public void Increment(int amount = 1)
- {
- Current.Value += amount;
- }
-
- ///
- /// Stops rollover animation, forcing the displayed count to be the actual count.
- ///
- public void StopRolling()
- {
- updateCount(false);
- }
-
- protected virtual string FormatCount(int count)
- {
- return count.ToString();
- }
-
- protected virtual void OnCountRolling(int currentValue, int newValue)
- {
- transformRoll(currentValue, newValue);
- }
-
- protected virtual void OnCountIncrement(int currentValue, int newValue)
- {
- DisplayedCount = newValue;
- }
-
- protected virtual void OnCountChange(int currentValue, int newValue)
- {
- DisplayedCount = newValue;
- }
-
- private double getProportionalDuration(int currentValue, int newValue)
- {
- double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
- return difference * RollingDuration;
- }
-
- private void updateDisplayedCount(int currentValue, int newValue, bool rolling)
- {
- displayedCount = newValue;
- if (rolling)
- OnDisplayedCountRolling(currentValue, newValue);
- else if (currentValue + 1 == newValue)
- OnDisplayedCountIncrement(newValue);
- else
- OnDisplayedCountChange(newValue);
- }
-
- private void updateCount(bool rolling)
- {
- int prev = previousValue;
- previousValue = Current.Value;
-
- if (!IsLoaded)
- return;
-
- if (!rolling)
- {
- FinishTransforms(false, nameof(DisplayedCount));
- IsRolling = false;
- DisplayedCount = prev;
-
- if (prev + 1 == Current.Value)
- OnCountIncrement(prev, Current.Value);
- else
- OnCountChange(prev, Current.Value);
- }
- else
- {
- OnCountRolling(displayedCount, Current.Value);
- IsRolling = true;
- }
- }
-
- private void transformRoll(int currentValue, int newValue)
- {
- this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing);
- }
-
- protected abstract void OnDisplayedCountRolling(int currentValue, int newValue);
- protected abstract void OnDisplayedCountIncrement(int newValue);
- protected abstract void OnDisplayedCountChange(int newValue);
- }
-}
diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs
new file mode 100644
index 0000000000..d5d8ec570a
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter
+ {
+ private readonly Vector2 offset = new Vector2(-20, 5);
+
+ public DefaultAccuracyCounter()
+ {
+ Origin = Anchor.TopRight;
+ Anchor = Anchor.TopRight;
+ }
+
+ [Resolved(canBeNull: true)]
+ private HUDOverlay hud { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = colours.BlueLighter;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
+ {
+ // for now align with the score counter. eventually this will be user customisable.
+ Anchor = Anchor.TopLeft;
+ Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
similarity index 56%
rename from osu.Game/Graphics/UserInterface/SimpleComboCounter.cs
rename to osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
index c9790aed46..a5c33f6dbe 100644
--- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
@@ -4,18 +4,23 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
-namespace osu.Game.Graphics.UserInterface
+namespace osu.Game.Screens.Play.HUD
{
- ///
- /// Used as an accuracy counter. Represented visually as a percentage.
- ///
- public class SimpleComboCounter : RollingCounter
+ public class DefaultComboCounter : RollingCounter, IComboCounter
{
+ private readonly Vector2 offset = new Vector2(20, 5);
+
protected override double RollingDuration => 750;
- public SimpleComboCounter()
+ [Resolved(canBeNull: true)]
+ private HUDOverlay hud { get; set; }
+
+ public DefaultComboCounter()
{
Current.Value = DisplayedCount = 0;
}
@@ -23,6 +28,17 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter;
+ protected override void Update()
+ {
+ base.Update();
+
+ if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
+ {
+ // for now align with the score counter. eventually this will be user customisable.
+ Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset;
+ }
+ }
+
protected override string FormatCount(int count)
{
return $@"{count}x";
diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs
similarity index 91%
rename from osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs
rename to osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs
index fc4a1a5d83..b550b469e9 100644
--- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs
@@ -16,7 +16,7 @@ using osu.Framework.Utils;
namespace osu.Game.Screens.Play.HUD
{
- public class StandardHealthDisplay : HealthDisplay, IHasAccentColour
+ public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour
{
///
/// The base opacity of the glow.
@@ -71,8 +71,12 @@ namespace osu.Game.Screens.Play.HUD
}
}
- public StandardHealthDisplay()
+ public DefaultHealthDisplay()
{
+ Size = new Vector2(1, 5);
+ RelativeSizeAxes = Axes.X;
+ Margin = new MarginPadding { Top = 20 };
+
Children = new Drawable[]
{
new Box
@@ -103,13 +107,7 @@ namespace osu.Game.Screens.Play.HUD
GlowColour = colours.BlueDarker;
}
- public void Flash(JudgementResult result)
- {
- if (!result.IsHit)
- return;
-
- Scheduler.AddOnce(flash);
- }
+ public override void Flash(JudgementResult result) => Scheduler.AddOnce(flash);
private void flash()
{
diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
new file mode 100644
index 0000000000..1dcfe2e067
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class DefaultScoreCounter : ScoreCounter
+ {
+ public DefaultScoreCounter()
+ : base(6)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ }
+
+ [Resolved(canBeNull: true)]
+ private HUDOverlay hud { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = colours.BlueLighter;
+
+ // todo: check if default once health display is skinnable
+ hud?.ShowHealthbar.BindValueChanged(healthBar =>
+ {
+ this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING);
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
index edc9dedf24..5c43e00192 100644
--- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
@@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@@ -12,14 +13,18 @@ namespace osu.Game.Screens.Play.HUD
/// A container for components displaying the current player health.
/// Gets bound automatically to the when inserted to hierarchy.
///
- public abstract class HealthDisplay : Container
+ public abstract class HealthDisplay : Container, IHealthDisplay
{
- public readonly BindableDouble Current = new BindableDouble(1)
+ public Bindable Current { get; } = new BindableDouble(1)
{
MinValue = 0,
MaxValue = 1
};
+ public virtual void Flash(JudgementResult result)
+ {
+ }
+
///
/// Bind the tracked fields of to this health display.
///
diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
index 4d28f00f39..37d10a5320 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
@@ -66,54 +66,69 @@ namespace osu.Game.Screens.Play.HUD
switch (type.NewValue)
{
case ScoreMeterType.HitErrorBoth:
- createBar(false);
- createBar(true);
+ createBar(Anchor.CentreLeft);
+ createBar(Anchor.CentreRight);
break;
case ScoreMeterType.HitErrorLeft:
- createBar(false);
+ createBar(Anchor.CentreLeft);
break;
case ScoreMeterType.HitErrorRight:
- createBar(true);
+ createBar(Anchor.CentreRight);
+ break;
+
+ case ScoreMeterType.HitErrorBottom:
+ createBar(Anchor.BottomCentre);
break;
case ScoreMeterType.ColourBoth:
- createColour(false);
- createColour(true);
+ createColour(Anchor.CentreLeft);
+ createColour(Anchor.CentreRight);
break;
case ScoreMeterType.ColourLeft:
- createColour(false);
+ createColour(Anchor.CentreLeft);
break;
case ScoreMeterType.ColourRight:
- createColour(true);
+ createColour(Anchor.CentreRight);
+ break;
+
+ case ScoreMeterType.ColourBottom:
+ createColour(Anchor.BottomCentre);
break;
}
}
- private void createBar(bool rightAligned)
+ private void createBar(Anchor anchor)
{
+ bool rightAligned = (anchor & Anchor.x2) > 0;
+ bool bottomAligned = (anchor & Anchor.y2) > 0;
+
var display = new BarHitErrorMeter(hitWindows, rightAligned)
{
Margin = new MarginPadding(margin),
- Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
- Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Anchor = anchor,
+ Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0,
+ Rotation = bottomAligned ? 270 : 0
};
completeDisplayLoading(display);
}
- private void createColour(bool rightAligned)
+ private void createColour(Anchor anchor)
{
+ bool bottomAligned = (anchor & Anchor.y2) > 0;
+
var display = new ColourHitErrorMeter(hitWindows)
{
Margin = new MarginPadding(margin),
- Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
- Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Anchor = anchor,
+ Origin = bottomAligned ? Anchor.CentreLeft : anchor,
Alpha = 0,
+ Rotation = bottomAligned ? 270 : 0
};
completeDisplayLoading(display);
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
index f99c84fc01..89f135de7f 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -99,7 +99,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10),
Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ // undo any layout rotation to display the icon the correct orientation
+ Rotation = -Rotation,
},
new SpriteIcon
{
@@ -107,7 +109,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Size = new Vector2(10),
Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ // undo any layout rotation to display the icon the correct orientation
+ Rotation = -Rotation,
}
}
},
diff --git a/osu.Game/Screens/Play/HUD/IAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/IAccuracyCounter.cs
new file mode 100644
index 0000000000..0199250a08
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/IAccuracyCounter.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An interface providing a set of methods to update a accuracy counter.
+ ///
+ public interface IAccuracyCounter : IDrawable
+ {
+ ///
+ /// The current accuracy to be displayed.
+ ///
+ Bindable Current { get; }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/IComboCounter.cs b/osu.Game/Screens/Play/HUD/IComboCounter.cs
new file mode 100644
index 0000000000..ff235bf04e
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/IComboCounter.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An interface providing a set of methods to update a combo counter.
+ ///
+ public interface IComboCounter : IDrawable
+ {
+ ///
+ /// The current combo to be displayed.
+ ///
+ Bindable Current { get; }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/IHealthDisplay.cs b/osu.Game/Screens/Play/HUD/IHealthDisplay.cs
new file mode 100644
index 0000000000..b1a64bd844
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/IHealthDisplay.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An interface providing a set of methods to update a health display.
+ ///
+ public interface IHealthDisplay : IDrawable
+ {
+ ///
+ /// The current health to be displayed.
+ ///
+ Bindable Current { get; }
+
+ ///
+ /// Flash the display for a specified result type.
+ ///
+ /// The result type.
+ void Flash(JudgementResult result);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/IScoreCounter.cs b/osu.Game/Screens/Play/HUD/IScoreCounter.cs
new file mode 100644
index 0000000000..2d39a64cfe
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/IScoreCounter.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An interface providing a set of methods to update a score counter.
+ ///
+ public interface IScoreCounter : IDrawable
+ {
+ ///
+ /// The current score to be displayed.
+ ///
+ Bindable Current { get; }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
new file mode 100644
index 0000000000..4784bca7dd
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
@@ -0,0 +1,252 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// Uses the 'x' symbol and has a pop-out effect while rolling over.
+ ///
+ public class LegacyComboCounter : CompositeDrawable, IComboCounter
+ {
+ public Bindable Current { get; } = new BindableInt { MinValue = 0, };
+
+ private uint scheduledPopOutCurrentId;
+
+ private const double pop_out_duration = 150;
+
+ private const Easing pop_out_easing = Easing.None;
+
+ private const double fade_out_duration = 100;
+
+ ///
+ /// Duration in milliseconds for the counter roll-up animation for each element.
+ ///
+ private const double rolling_duration = 20;
+
+ private Drawable popOutCount;
+
+ private Drawable displayedCountSpriteText;
+
+ private int previousValue;
+
+ private int displayedCount;
+
+ private bool isRolling;
+
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
+ public LegacyComboCounter()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.BottomLeft;
+
+ Margin = new MarginPadding(10);
+
+ Scale = new Vector2(1.2f);
+ }
+
+ ///
+ /// Value shown at the current moment.
+ ///
+ public virtual int DisplayedCount
+ {
+ get => displayedCount;
+ private set
+ {
+ if (displayedCount.Equals(value))
+ return;
+
+ if (isRolling)
+ onDisplayedCountRolling(displayedCount, value);
+ else if (displayedCount + 1 == value)
+ onDisplayedCountIncrement(value);
+ else
+ onDisplayedCountChange(value);
+
+ displayedCount = value;
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new[]
+ {
+ displayedCountSpriteText = createSpriteText().With(s =>
+ {
+ s.Alpha = 0;
+ }),
+ popOutCount = createSpriteText().With(s =>
+ {
+ s.Alpha = 0;
+ s.Margin = new MarginPadding(0.05f);
+ })
+ };
+
+ Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
+
+ displayedCountSpriteText.Anchor = Anchor;
+ displayedCountSpriteText.Origin = Origin;
+ popOutCount.Origin = Origin;
+ popOutCount.Anchor = Anchor;
+
+ updateCount(false);
+ }
+
+ private void updateCount(bool rolling)
+ {
+ int prev = previousValue;
+ previousValue = Current.Value;
+
+ if (!IsLoaded)
+ return;
+
+ if (!rolling)
+ {
+ FinishTransforms(false, nameof(DisplayedCount));
+ isRolling = false;
+ DisplayedCount = prev;
+
+ if (prev + 1 == Current.Value)
+ onCountIncrement(prev, Current.Value);
+ else
+ onCountChange(prev, Current.Value);
+ }
+ else
+ {
+ onCountRolling(displayedCount, Current.Value);
+ isRolling = true;
+ }
+ }
+
+ private void transformPopOut(int newValue)
+ {
+ ((IHasText)popOutCount).Text = formatCount(newValue);
+
+ popOutCount.ScaleTo(1.6f);
+ popOutCount.FadeTo(0.75f);
+ popOutCount.MoveTo(Vector2.Zero);
+
+ popOutCount.ScaleTo(1, pop_out_duration, pop_out_easing);
+ popOutCount.FadeOut(pop_out_duration, pop_out_easing);
+ popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration, pop_out_easing);
+ }
+
+ private void transformNoPopOut(int newValue)
+ {
+ ((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
+
+ displayedCountSpriteText.ScaleTo(1);
+ }
+
+ private void transformPopOutSmall(int newValue)
+ {
+ ((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
+ displayedCountSpriteText.ScaleTo(1.1f);
+ displayedCountSpriteText.ScaleTo(1, pop_out_duration, pop_out_easing);
+ }
+
+ private void scheduledPopOutSmall(uint id)
+ {
+ // Too late; scheduled task invalidated
+ if (id != scheduledPopOutCurrentId)
+ return;
+
+ DisplayedCount++;
+ }
+
+ private void onCountIncrement(int currentValue, int newValue)
+ {
+ scheduledPopOutCurrentId++;
+
+ if (DisplayedCount < currentValue)
+ DisplayedCount++;
+
+ displayedCountSpriteText.Show();
+
+ transformPopOut(newValue);
+
+ uint newTaskId = scheduledPopOutCurrentId;
+
+ Scheduler.AddDelayed(delegate
+ {
+ scheduledPopOutSmall(newTaskId);
+ }, pop_out_duration);
+ }
+
+ private void onCountRolling(int currentValue, int newValue)
+ {
+ scheduledPopOutCurrentId++;
+
+ // Hides displayed count if was increasing from 0 to 1 but didn't finish
+ if (currentValue == 0 && newValue == 0)
+ displayedCountSpriteText.FadeOut(fade_out_duration);
+
+ transformRoll(currentValue, newValue);
+ }
+
+ private void onCountChange(int currentValue, int newValue)
+ {
+ scheduledPopOutCurrentId++;
+
+ if (newValue == 0)
+ displayedCountSpriteText.FadeOut();
+
+ DisplayedCount = newValue;
+ }
+
+ private void onDisplayedCountRolling(int currentValue, int newValue)
+ {
+ if (newValue == 0)
+ displayedCountSpriteText.FadeOut(fade_out_duration);
+ else
+ displayedCountSpriteText.Show();
+
+ transformNoPopOut(newValue);
+ }
+
+ private void onDisplayedCountChange(int newValue)
+ {
+ displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1);
+ transformNoPopOut(newValue);
+ }
+
+ private void onDisplayedCountIncrement(int newValue)
+ {
+ displayedCountSpriteText.Show();
+ transformPopOutSmall(newValue);
+ }
+
+ private void transformRoll(int currentValue, int newValue) =>
+ this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), Easing.None);
+
+ private string formatCount(int count) => $@"{count}x";
+
+ private double getProportionalDuration(int currentValue, int newValue)
+ {
+ double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
+ return difference * rolling_duration;
+ }
+
+ private OsuSpriteText createSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboText));
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs
index 99c31241f1..68d019bf71 100644
--- a/osu.Game/Screens/Play/HUD/ModDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs
@@ -48,22 +48,29 @@ namespace osu.Game.Screens.Play.HUD
{
AutoSizeAxes = Axes.Both;
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- iconsContainer = new ReverseChildIDFillFlowContainer
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
+ iconsContainer = new ReverseChildIDFillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ },
+ unrankedText = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"/ UNRANKED /",
+ Font = OsuFont.Numeric.With(size: 12)
+ }
},
- unrankedText = new OsuSpriteText
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.TopCentre,
- Text = @"/ UNRANKED /",
- Font = OsuFont.Numeric.With(size: 12)
- }
};
Current.ValueChanged += mods =>
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index fc80983834..ffcbb06fb3 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -20,14 +20,13 @@ namespace osu.Game.Screens.Play.HUD
public readonly VisualSettings VisualSettings;
- //public readonly CollectionSettings CollectionSettings;
-
- //public readonly DiscussionSettings DiscussionSettings;
-
public PlayerSettingsOverlay()
{
AlwaysPresent = true;
- RelativeSizeAxes = Axes.Both;
+
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+ AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer
{
@@ -36,7 +35,6 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
- Margin = new MarginPadding { Top = 100, Right = 10 },
Children = new PlayerSettingsGroup[]
{
//CollectionSettings = new CollectionSettings(),
diff --git a/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs
new file mode 100644
index 0000000000..76c9c30813
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class SkinnableAccuracyCounter : SkinnableDrawable, IAccuracyCounter
+ {
+ public Bindable Current { get; } = new Bindable();
+
+ public SkinnableAccuracyCounter()
+ : base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter())
+ {
+ CentreComponent = false;
+ }
+
+ private IAccuracyCounter skinnedCounter;
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ skinnedCounter = Drawable as IAccuracyCounter;
+ skinnedCounter?.Current.BindTo(Current);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs
new file mode 100644
index 0000000000..c04c50141a
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class SkinnableComboCounter : SkinnableDrawable, IComboCounter
+ {
+ public Bindable Current { get; } = new Bindable();
+
+ public SkinnableComboCounter()
+ : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter())
+ {
+ CentreComponent = false;
+ }
+
+ private IComboCounter skinnedCounter;
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ skinnedCounter = Drawable as IComboCounter;
+ skinnedCounter?.Current.BindTo(Current);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs
new file mode 100644
index 0000000000..a442ad0d9a
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class SkinnableScoreCounter : SkinnableDrawable, IScoreCounter
+ {
+ public Bindable Current { get; } = new Bindable();
+
+ public SkinnableScoreCounter()
+ : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
+ {
+ CentreComponent = false;
+ }
+
+ private IScoreCounter skinnedCounter;
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ skinnedCounter = Drawable as IScoreCounter;
+ skinnedCounter?.Current.BindTo(Current);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/StandardComboCounter.cs b/osu.Game/Screens/Play/HUD/StandardComboCounter.cs
deleted file mode 100644
index 7301300b8d..0000000000
--- a/osu.Game/Screens/Play/HUD/StandardComboCounter.cs
+++ /dev/null
@@ -1,141 +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 osuTK;
-using osu.Framework.Graphics;
-
-namespace osu.Game.Screens.Play.HUD
-{
- ///
- /// Uses the 'x' symbol and has a pop-out effect while rolling over.
- ///
- public class StandardComboCounter : ComboCounter
- {
- protected uint ScheduledPopOutCurrentId;
-
- protected virtual float PopOutSmallScale => 1.1f;
- protected virtual bool CanPopOutWhileRolling => false;
-
- public new Vector2 PopOutScale = new Vector2(1.6f);
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- PopOutCount.Origin = Origin;
- PopOutCount.Anchor = Anchor;
- }
-
- protected override string FormatCount(int count)
- {
- return $@"{count}x";
- }
-
- protected virtual void TransformPopOut(int newValue)
- {
- PopOutCount.Text = FormatCount(newValue);
-
- PopOutCount.ScaleTo(PopOutScale);
- PopOutCount.FadeTo(PopOutInitialAlpha);
- PopOutCount.MoveTo(Vector2.Zero);
-
- PopOutCount.ScaleTo(1, PopOutDuration, PopOutEasing);
- PopOutCount.FadeOut(PopOutDuration, PopOutEasing);
- PopOutCount.MoveTo(DisplayedCountSpriteText.Position, PopOutDuration, PopOutEasing);
- }
-
- protected virtual void TransformPopOutRolling(int newValue)
- {
- TransformPopOut(newValue);
- TransformPopOutSmall(newValue);
- }
-
- protected virtual void TransformNoPopOut(int newValue)
- {
- DisplayedCountSpriteText.Text = FormatCount(newValue);
- DisplayedCountSpriteText.ScaleTo(1);
- }
-
- protected virtual void TransformPopOutSmall(int newValue)
- {
- DisplayedCountSpriteText.Text = FormatCount(newValue);
- DisplayedCountSpriteText.ScaleTo(PopOutSmallScale);
- DisplayedCountSpriteText.ScaleTo(1, PopOutDuration, PopOutEasing);
- }
-
- protected virtual void ScheduledPopOutSmall(uint id)
- {
- // Too late; scheduled task invalidated
- if (id != ScheduledPopOutCurrentId)
- return;
-
- DisplayedCount++;
- }
-
- protected override void OnCountRolling(int currentValue, int newValue)
- {
- ScheduledPopOutCurrentId++;
-
- // Hides displayed count if was increasing from 0 to 1 but didn't finish
- if (currentValue == 0 && newValue == 0)
- DisplayedCountSpriteText.FadeOut(FadeOutDuration);
-
- base.OnCountRolling(currentValue, newValue);
- }
-
- protected override void OnCountIncrement(int currentValue, int newValue)
- {
- ScheduledPopOutCurrentId++;
-
- if (DisplayedCount < currentValue)
- DisplayedCount++;
-
- DisplayedCountSpriteText.Show();
-
- TransformPopOut(newValue);
-
- uint newTaskId = ScheduledPopOutCurrentId;
- Scheduler.AddDelayed(delegate
- {
- ScheduledPopOutSmall(newTaskId);
- }, PopOutDuration);
- }
-
- protected override void OnCountChange(int currentValue, int newValue)
- {
- ScheduledPopOutCurrentId++;
-
- if (newValue == 0)
- DisplayedCountSpriteText.FadeOut();
-
- base.OnCountChange(currentValue, newValue);
- }
-
- protected override void OnDisplayedCountRolling(int currentValue, int newValue)
- {
- if (newValue == 0)
- DisplayedCountSpriteText.FadeOut(FadeOutDuration);
- else
- DisplayedCountSpriteText.Show();
-
- if (CanPopOutWhileRolling)
- TransformPopOutRolling(newValue);
- else
- TransformNoPopOut(newValue);
- }
-
- protected override void OnDisplayedCountChange(int newValue)
- {
- DisplayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1);
-
- TransformNoPopOut(newValue);
- }
-
- protected override void OnDisplayedCountIncrement(int newValue)
- {
- DisplayedCountSpriteText.Show();
-
- TransformPopOutSmall(newValue);
- }
- }
-}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 26aefa138b..c3de249bf8 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -10,28 +10,30 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Play
{
+ [Cached]
public class HUDOverlay : Container
{
- private const float fade_duration = 400;
- private const Easing fade_easing = Easing.Out;
+ public const float FADE_DURATION = 400;
+
+ public const Easing FADE_EASING = Easing.Out;
public readonly KeyCounterDisplay KeyCounter;
- public readonly RollingCounter ComboCounter;
- public readonly ScoreCounter ScoreCounter;
- public readonly RollingCounter AccuracyCounter;
- public readonly HealthDisplay HealthDisplay;
+ public readonly SkinnableComboCounter ComboCounter;
+ public readonly SkinnableScoreCounter ScoreCounter;
+ public readonly SkinnableAccuracyCounter AccuracyCounter;
+ public readonly SkinnableHealthDisplay HealthDisplay;
public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay;
public readonly HitErrorDisplay HitErrorDisplay;
@@ -61,7 +63,10 @@ namespace osu.Game.Screens.Play
public Action RequestSeek;
- private readonly Container topScoreContainer;
+ private readonly FillFlowContainer bottomRightElements;
+ private readonly FillFlowContainer topRightElements;
+
+ private readonly Container mainUIElements;
private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
@@ -80,35 +85,61 @@ namespace osu.Game.Screens.Play
visibilityContainer = new Container
{
RelativeSizeAxes = Axes.Both,
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ mainUIElements = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ HealthDisplay = CreateHealthDisplay(),
+ AccuracyCounter = CreateAccuracyCounter(),
+ ScoreCounter = CreateScoreCounter(),
+ ComboCounter = CreateComboCounter(),
+ HitErrorDisplay = CreateHitErrorDisplayOverlay(),
+ }
+ },
+ },
+ new Drawable[]
+ {
+ Progress = CreateProgress(),
+ }
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ }
+ },
+ },
+ topRightElements = new FillFlowContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Margin = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
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(),
- },
- },
- Progress = CreateProgress(),
ModDisplay = CreateModsContainer(),
- HitErrorDisplay = CreateHitErrorDisplayOverlay(),
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
}
},
- new FillFlowContainer
+ bottomRightElements = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
- Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y),
+ Margin = new MarginPadding(10),
+ Spacing = new Vector2(10),
AutoSizeAxes = Axes.Both,
- LayoutDuration = fade_duration / 2,
- LayoutEasing = fade_easing,
+ LayoutDuration = FADE_DURATION / 2,
+ LayoutEasing = FADE_EASING,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
@@ -161,21 +192,8 @@ namespace osu.Game.Screens.Play
{
base.LoadComplete();
- ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, fade_duration, fade_easing)));
-
- ShowHealthbar.BindValueChanged(healthBar =>
- {
- if (healthBar.NewValue)
- {
- HealthDisplay.FadeIn(fade_duration, fade_easing);
- topScoreContainer.MoveToY(30, fade_duration, fade_easing);
- }
- else
- {
- HealthDisplay.FadeOut(fade_duration, fade_easing);
- topScoreContainer.MoveToY(0, fade_duration, fade_easing);
- }
- }, true);
+ ShowHealthbar.BindValueChanged(healthBar => HealthDisplay.FadeTo(healthBar.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING), true);
+ ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING)));
configShowHud.BindValueChanged(visible =>
{
@@ -186,6 +204,24 @@ namespace osu.Game.Screens.Play
replayLoaded.BindValueChanged(replayLoadedValueChanged, true);
}
+ protected override void Update()
+ {
+ base.Update();
+
+ float topRightOffset = 0;
+
+ // fetch the bottom-most position of any main ui element that is anchored to the top of the screen.
+ // consider this kind of temporary.
+ foreach (var d in mainUIElements)
+ {
+ if (d is SkinnableDrawable sd && (sd.Drawable.Anchor & Anchor.y0) > 0)
+ topRightOffset = Math.Max(sd.Drawable.ScreenSpaceDrawQuad.BottomRight.Y, topRightOffset);
+ }
+
+ topRightElements.Y = ToLocalSpace(new Vector2(0, topRightOffset)).Y;
+ bottomRightElements.Y = -Progress.Height;
+ }
+
private void replayLoadedValueChanged(ValueChangedEvent e)
{
PlayerSettingsOverlay.ReplayLoaded = e.NewValue;
@@ -230,34 +266,13 @@ namespace osu.Game.Screens.Play
return base.OnKeyDown(e);
}
- protected virtual RollingCounter CreateAccuracyCounter() => new PercentageCounter
- {
- BypassAutoSizeAxes = Axes.X,
- Anchor = Anchor.TopLeft,
- Origin = Anchor.TopRight,
- Margin = new MarginPadding { Top = 5, Right = 20 },
- };
+ protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter();
- protected virtual ScoreCounter CreateScoreCounter() => new ScoreCounter(6)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- };
+ protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter();
- protected virtual RollingCounter CreateComboCounter() => new SimpleComboCounter
- {
- BypassAutoSizeAxes = Axes.X,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopLeft,
- Margin = new MarginPadding { Top = 5, Left = 20 },
- };
+ protected virtual SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter();
- protected virtual HealthDisplay CreateHealthDisplay() => new StandardHealthDisplay
- {
- Size = new Vector2(1, 5),
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Top = 20 }
- };
+ protected virtual SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay();
protected virtual FailingLayer CreateFailingLayer() => new FailingLayer
{
@@ -268,7 +283,6 @@ namespace osu.Game.Screens.Play
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
- Margin = new MarginPadding(10),
};
protected virtual SongProgress CreateProgress() => new SongProgress
@@ -289,7 +303,6 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = 20, Right = 20 },
};
protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows);
@@ -302,8 +315,14 @@ namespace osu.Game.Screens.Play
AccuracyCounter?.Current.BindTo(processor.Accuracy);
ComboCounter?.Current.BindTo(processor.Combo);
- if (HealthDisplay is StandardHealthDisplay shd)
- processor.NewJudgement += shd.Flash;
+ if (HealthDisplay is IHealthDisplay shd)
+ {
+ processor.NewJudgement += judgement =>
+ {
+ if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit)
+ shd.Flash(judgement);
+ };
+ }
}
protected virtual void BindHealthProcessor(HealthProcessor processor)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index a2a53b4b75..df0a52a0e8 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -35,7 +35,8 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play
{
[Cached]
- public class Player : ScreenWithBeatmapBackground
+ [Cached(typeof(ISamplePlaybackDisabler))]
+ public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
{
///
/// The delay upon completion of the beatmap before displaying the results screen.
@@ -55,6 +56,8 @@ namespace osu.Game.Screens.Play
// We are managing our own adjustments (see OnEntering/OnExiting).
public override bool AllowRateAdjustments => false;
+ private readonly Bindable samplePlaybackDisabled = new Bindable();
+
///
/// Whether gameplay should pause when the game window focus is lost.
///
@@ -218,8 +221,12 @@ namespace osu.Game.Screens.Play
createGameplayComponents(Beatmap.Value, playableBeatmap)
});
+ // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
+ // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
+ var hudRulesetContainer = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
+
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
- GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value));
+ GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value)));
if (!DrawableRuleset.AllowGameplayOverlays)
{
@@ -229,7 +236,11 @@ namespace osu.Game.Screens.Play
skipOverlay.Hide();
}
- DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
+ DrawableRuleset.IsPaused.BindValueChanged(paused =>
+ {
+ updateGameplayState();
+ samplePlaybackDisabled.Value = paused.NewValue;
+ });
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@@ -752,5 +763,7 @@ namespace osu.Game.Screens.Play
}
#endregion
+
+ IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
}
}
diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/SkinnableHealthDisplay.cs
new file mode 100644
index 0000000000..d35d15d665
--- /dev/null
+++ b/osu.Game/Screens/Play/SkinnableHealthDisplay.cs
@@ -0,0 +1,51 @@
+// 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.Bindables;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play
+{
+ public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay
+ {
+ public Bindable Current { get; } = new BindableDouble(1)
+ {
+ MinValue = 0,
+ MaxValue = 1
+ };
+
+ public void Flash(JudgementResult result) => skinnedCounter?.Flash(result);
+
+ private HealthProcessor processor;
+
+ public void BindHealthProcessor(HealthProcessor processor)
+ {
+ if (this.processor != null)
+ throw new InvalidOperationException("Can't bind to a processor more than once");
+
+ this.processor = processor;
+
+ Current.BindTo(processor.Health);
+ }
+
+ public SkinnableHealthDisplay()
+ : base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay())
+ {
+ CentreComponent = false;
+ }
+
+ private IHealthDisplay skinnedCounter;
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ skinnedCounter = Drawable as IHealthDisplay;
+ skinnedCounter?.Current.BindTo(Current);
+ }
+ }
+}
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()
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 39fa4f777d..4b6b3be45c 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -79,10 +79,10 @@ namespace osu.Game.Screens.Select
}
private static int getLengthScale(string value) =>
- value.EndsWith("ms") ? 1 :
- value.EndsWith("s") ? 1000 :
- value.EndsWith("m") ? 60000 :
- value.EndsWith("h") ? 3600000 : 1000;
+ value.EndsWith("ms", StringComparison.Ordinal) ? 1 :
+ value.EndsWith('s') ? 1000 :
+ value.EndsWith('m') ? 60000 :
+ value.EndsWith('h') ? 3600000 : 1000;
private static bool parseFloatWithPoint(string value, out float result) =>
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
diff --git a/osu.Game/Skinning/GameplaySkinComponent.cs b/osu.Game/Skinning/GameplaySkinComponent.cs
index 2aa380fa90..80f6efc07a 100644
--- a/osu.Game/Skinning/GameplaySkinComponent.cs
+++ b/osu.Game/Skinning/GameplaySkinComponent.cs
@@ -18,6 +18,6 @@ namespace osu.Game.Skinning
protected virtual string ComponentName => Component.ToString();
public string LookupName =>
- string.Join("/", new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s)));
+ string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s)));
}
}
diff --git a/osu.Game/Skinning/HUDSkinComponent.cs b/osu.Game/Skinning/HUDSkinComponent.cs
new file mode 100644
index 0000000000..cc053421b7
--- /dev/null
+++ b/osu.Game/Skinning/HUDSkinComponent.cs
@@ -0,0 +1,22 @@
+// 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;
+
+namespace osu.Game.Skinning
+{
+ public class HUDSkinComponent : ISkinComponent
+ {
+ public readonly HUDSkinComponents Component;
+
+ public HUDSkinComponent(HUDSkinComponents component)
+ {
+ Component = component;
+ }
+
+ protected virtual string ComponentName => Component.ToString();
+
+ public string LookupName =>
+ string.Join('/', new[] { "HUD", ComponentName }.Where(s => !string.IsNullOrEmpty(s)));
+ }
+}
diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs
new file mode 100644
index 0000000000..b01be2d5a0
--- /dev/null
+++ b/osu.Game/Skinning/HUDSkinComponents.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Skinning
+{
+ public enum HUDSkinComponents
+ {
+ ComboCounter,
+ ScoreCounter,
+ AccuracyCounter,
+ HealthDisplay,
+ ScoreText,
+ ComboText,
+ }
+}
diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs
new file mode 100644
index 0000000000..29d7046694
--- /dev/null
+++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyAccuracyCounter : PercentageCounter, IAccuracyCounter
+ {
+ private readonly ISkin skin;
+
+ public LegacyAccuracyCounter(ISkin skin)
+ {
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+
+ Scale = new Vector2(0.6f);
+ Margin = new MarginPadding(10);
+
+ this.skin = skin;
+ }
+
+ [Resolved(canBeNull: true)]
+ private HUDOverlay hud { get; set; }
+
+ protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText));
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (hud?.ScoreCounter.Drawable is LegacyScoreCounter score)
+ {
+ // for now align with the score counter. eventually this will be user customisable.
+ Y = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs
new file mode 100644
index 0000000000..489e23ab7a
--- /dev/null
+++ b/osu.Game/Skinning/LegacyHealthDisplay.cs
@@ -0,0 +1,266 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay
+ {
+ private const double epic_cutoff = 0.5;
+
+ private readonly Skin skin;
+ private LegacyHealthPiece fill;
+ private LegacyHealthPiece marker;
+
+ private float maxFillWidth;
+
+ private bool isNewStyle;
+
+ public Bindable Current { get; } = new BindableDouble(1)
+ {
+ MinValue = 0,
+ MaxValue = 1
+ };
+
+ public LegacyHealthDisplay(Skin skin)
+ {
+ this.skin = skin;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ isNewStyle = getTexture(skin, "marker") != null;
+
+ // background implementation is the same for both versions.
+ AddInternal(new Sprite { Texture = getTexture(skin, "bg") });
+
+ if (isNewStyle)
+ {
+ AddRangeInternal(new[]
+ {
+ fill = new LegacyNewStyleFill(skin),
+ marker = new LegacyNewStyleMarker(skin),
+ });
+ }
+ else
+ {
+ AddRangeInternal(new[]
+ {
+ fill = new LegacyOldStyleFill(skin),
+ marker = new LegacyOldStyleMarker(skin),
+ });
+ }
+
+ fill.Current.BindTo(Current);
+ marker.Current.BindTo(Current);
+
+ maxFillWidth = fill.Width;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ fill.Width = Interpolation.ValueAt(
+ Math.Clamp(Clock.ElapsedFrameTime, 0, 200),
+ fill.Width, (float)Current.Value * maxFillWidth, 0, 200, Easing.OutQuint);
+
+ marker.Position = fill.Position + new Vector2(fill.DrawWidth, fill.DrawHeight / 2);
+ }
+
+ public void Flash(JudgementResult result) => marker.Flash(result);
+
+ private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}");
+
+ private static Color4 getFillColour(double hp)
+ {
+ if (hp < 0.2)
+ return Interpolation.ValueAt(0.2 - hp, Color4.Black, Color4.Red, 0, 0.2);
+
+ if (hp < epic_cutoff)
+ return Interpolation.ValueAt(0.5 - hp, Color4.White, Color4.Black, 0, 0.5);
+
+ return Color4.White;
+ }
+
+ public class LegacyOldStyleMarker : LegacyMarker
+ {
+ private readonly Texture normalTexture;
+ private readonly Texture dangerTexture;
+ private readonly Texture superDangerTexture;
+
+ public LegacyOldStyleMarker(Skin skin)
+ {
+ normalTexture = getTexture(skin, "ki");
+ dangerTexture = getTexture(skin, "kidanger");
+ superDangerTexture = getTexture(skin, "kidanger2");
+ }
+
+ public override Sprite CreateSprite() => new Sprite
+ {
+ Texture = normalTexture,
+ Origin = Anchor.Centre,
+ };
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(hp =>
+ {
+ if (hp.NewValue < 0.2f)
+ Main.Texture = superDangerTexture;
+ else if (hp.NewValue < epic_cutoff)
+ Main.Texture = dangerTexture;
+ else
+ Main.Texture = normalTexture;
+ });
+ }
+ }
+
+ public class LegacyNewStyleMarker : LegacyMarker
+ {
+ private readonly Skin skin;
+
+ public LegacyNewStyleMarker(Skin skin)
+ {
+ this.skin = skin;
+ }
+
+ public override Sprite CreateSprite() => new Sprite
+ {
+ Texture = getTexture(skin, "marker"),
+ Origin = Anchor.Centre,
+ };
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Main.Colour = getFillColour(Current.Value);
+ Main.Blending = Current.Value < epic_cutoff ? BlendingParameters.Inherit : BlendingParameters.Additive;
+ }
+ }
+
+ internal class LegacyOldStyleFill : LegacyHealthPiece
+ {
+ public LegacyOldStyleFill(Skin skin)
+ {
+ // required for sizing correctly..
+ var firstFrame = getTexture(skin, "colour-0");
+
+ if (firstFrame == null)
+ {
+ InternalChild = new Sprite { Texture = getTexture(skin, "colour") };
+ Size = InternalChild.Size;
+ }
+ else
+ {
+ InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty();
+ Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight);
+ }
+
+ Position = new Vector2(3, 10) * 1.6f;
+ Masking = true;
+ }
+ }
+
+ internal class LegacyNewStyleFill : LegacyHealthPiece
+ {
+ public LegacyNewStyleFill(Skin skin)
+ {
+ InternalChild = new Sprite
+ {
+ Texture = getTexture(skin, "colour"),
+ };
+
+ Size = InternalChild.Size;
+ Position = new Vector2(7.5f, 7.8f) * 1.6f;
+ Masking = true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Colour = getFillColour(Current.Value);
+ }
+ }
+
+ public abstract class LegacyMarker : LegacyHealthPiece
+ {
+ protected Sprite Main;
+
+ private Sprite explode;
+
+ protected LegacyMarker()
+ {
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ Main = CreateSprite(),
+ explode = CreateSprite().With(s =>
+ {
+ s.Alpha = 0;
+ s.Blending = BlendingParameters.Additive;
+ }),
+ };
+ }
+
+ public abstract Sprite CreateSprite();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Current.BindValueChanged(val =>
+ {
+ if (val.NewValue > val.OldValue)
+ bulgeMain();
+ });
+ }
+
+ public override void Flash(JudgementResult result)
+ {
+ bulgeMain();
+
+ bool isEpic = Current.Value >= epic_cutoff;
+
+ explode.Blending = isEpic ? BlendingParameters.Additive : BlendingParameters.Inherit;
+ explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120);
+ explode.FadeOutFromOne(120);
+ }
+
+ private void bulgeMain() =>
+ Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out);
+ }
+
+ public class LegacyHealthPiece : CompositeDrawable, IHealthDisplay
+ {
+ public Bindable Current { get; } = new Bindable();
+
+ public virtual void Flash(JudgementResult result)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index a9d88e77ad..3dbec23194 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.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.Diagnostics;
using System.Globalization;
@@ -115,16 +116,16 @@ namespace osu.Game.Skinning
currentConfig.MinimumColumnWidth = minWidth;
break;
- case string _ when pair.Key.StartsWith("Colour"):
+ case string _ when pair.Key.StartsWith("Colour", StringComparison.Ordinal):
HandleColours(currentConfig, line);
break;
// Custom sprite paths
- case string _ when pair.Key.StartsWith("NoteImage"):
- case string _ when pair.Key.StartsWith("KeyImage"):
- case string _ when pair.Key.StartsWith("Hit"):
- case string _ when pair.Key.StartsWith("Stage"):
- case string _ when pair.Key.StartsWith("Lighting"):
+ case string _ when pair.Key.StartsWith("NoteImage", StringComparison.Ordinal):
+ case string _ when pair.Key.StartsWith("KeyImage", StringComparison.Ordinal):
+ case string _ when pair.Key.StartsWith("Hit", StringComparison.Ordinal):
+ case string _ when pair.Key.StartsWith("Stage", StringComparison.Ordinal):
+ case string _ when pair.Key.StartsWith("Lighting", StringComparison.Ordinal):
currentConfig.ImageLookups[pair.Key] = pair.Value;
break;
}
diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs
new file mode 100644
index 0000000000..e54c4e8eb4
--- /dev/null
+++ b/osu.Game/Skinning/LegacyScoreCounter.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyScoreCounter : ScoreCounter
+ {
+ private readonly ISkin skin;
+
+ protected override double RollingDuration => 1000;
+ protected override Easing RollingEasing => Easing.Out;
+
+ public new Bindable Current { get; } = new Bindable();
+
+ public LegacyScoreCounter(ISkin skin)
+ : base(6)
+ {
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+
+ this.skin = skin;
+
+ // base class uses int for display, but externally we bind to ScoreProcessor as a double for now.
+ Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue);
+
+ Scale = new Vector2(0.96f);
+ Margin = new MarginPadding(10);
+ }
+
+ protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText));
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index e38913b13a..94b09684d3 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -18,6 +18,8 @@ using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning
@@ -323,10 +325,51 @@ namespace osu.Game.Skinning
return null;
}
+ private string scorePrefix => GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score";
+
+ private string comboPrefix => GetConfig(LegacySkinConfiguration.LegacySetting.ComboPrefix)?.Value ?? "score";
+
+ private bool hasScoreFont => this.HasFont(scorePrefix);
+
public override Drawable GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
+ case HUDSkinComponent hudComponent:
+ {
+ if (!hasScoreFont)
+ return null;
+
+ switch (hudComponent.Component)
+ {
+ case HUDSkinComponents.ComboCounter:
+ return new LegacyComboCounter();
+
+ case HUDSkinComponents.ScoreCounter:
+ return new LegacyScoreCounter(this);
+
+ case HUDSkinComponents.AccuracyCounter:
+ return new LegacyAccuracyCounter(this);
+
+ case HUDSkinComponents.HealthDisplay:
+ return new LegacyHealthDisplay(this);
+
+ case HUDSkinComponents.ComboText:
+ return new LegacySpriteText(this, comboPrefix)
+ {
+ Spacing = new Vector2(-(GetConfig(LegacySkinConfiguration.LegacySetting.ComboOverlap)?.Value ?? -2), 0)
+ };
+
+ case HUDSkinComponents.ScoreText:
+ return new LegacySpriteText(this, scorePrefix)
+ {
+ Spacing = new Vector2(-(GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2), 0)
+ };
+ }
+
+ return null;
+ }
+
case GameplaySkinComponent resultComponent:
switch (resultComponent.Component)
{
@@ -397,7 +440,7 @@ namespace osu.Game.Skinning
// Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
string lastPiece = componentName.Split('/').Last();
- yield return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece;
+ yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
}
private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample)
@@ -408,7 +451,7 @@ namespace osu.Game.Skinning
// for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin.
// using .EndsWith() is intentional as it ensures parity in all edge cases
// (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not).
- lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix));
+ lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal));
// also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort.
// going forward specifying banks shall always be required, even for elements that wouldn't require it on stable,
diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs
index 828804b9cb..84a834ec22 100644
--- a/osu.Game/Skinning/LegacySkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacySkinConfiguration.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Skinning
Version,
ComboPrefix,
ComboOverlap,
+ ScorePrefix,
+ ScoreOverlap,
AnimationFramerate,
LayeredHitSounds
}
diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs
index 773a9dc5c6..8394657b1c 100644
--- a/osu.Game/Skinning/LegacySpriteText.cs
+++ b/osu.Game/Skinning/LegacySpriteText.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Skinning
{
private readonly LegacyGlyphStore glyphStore;
- public LegacySpriteText(ISkin skin, string font)
+ public LegacySpriteText(ISkin skin, string font = "score")
{
Shadow = false;
UseFullGlyphHeight = false;
@@ -34,7 +34,9 @@ namespace osu.Game.Skinning
public ITexturedCharacterGlyph Get(string fontName, char character)
{
- var texture = skin.GetTexture($"{fontName}-{character}");
+ var lookup = getLookupName(character);
+
+ var texture = skin.GetTexture($"{fontName}-{lookup}");
if (texture == null)
return null;
@@ -42,6 +44,24 @@ namespace osu.Game.Skinning
return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, null), texture, 1f / texture.ScaleAdjust);
}
+ private static string getLookupName(char character)
+ {
+ switch (character)
+ {
+ case ',':
+ return "comma";
+
+ case '.':
+ return "dot";
+
+ case '%':
+ return "percent";
+
+ default:
+ return character.ToString();
+ }
+ }
+
public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
}
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 7af400e807..37a2309e01 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -18,12 +18,14 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
+ [ExcludeFromDynamicCompile]
public class SkinManager : ArchiveModelManager, ISkinSource
{
private readonly AudioManager audio;
diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index d9a5036649..5a48bc4baf 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -19,6 +19,12 @@ namespace osu.Game.Skinning
///
public Drawable Drawable { get; private set; }
+ ///
+ /// Whether the drawable component should be centered in available space.
+ /// Defaults to true.
+ ///
+ public bool CentreComponent { get; set; } = true;
+
public new Axes AutoSizeAxes
{
get => base.AutoSizeAxes;
@@ -84,8 +90,13 @@ namespace osu.Game.Skinning
if (Drawable != null)
{
scaling.Invalidate();
- Drawable.Origin = Anchor.Centre;
- Drawable.Anchor = Anchor.Centre;
+
+ if (CentreComponent)
+ {
+ Drawable.Origin = Anchor.Centre;
+ Drawable.Anchor = Anchor.Centre;
+ }
+
InternalChild = Drawable;
}
else
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index a856789d96..fe4f735325 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -35,12 +35,12 @@ namespace osu.Game.Tests.Visual
}
[BackgroundDependencyLoader]
- private void load(AudioManager audio, SkinManager skinManager)
+ private void load(AudioManager audio, SkinManager skinManager, OsuGameBase game)
{
var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly);
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true);
- defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
+ defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio);
specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true);
oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true);
}
diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
index b5fcb56c06..4ebf2a7368 100644
--- a/osu.Game/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.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.Threading.Tasks;
using Newtonsoft.Json;
@@ -73,15 +74,15 @@ namespace osu.Game.Updater
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
- bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe"));
+ bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.MacOsx:
- bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip"));
+ bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.Linux:
- bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage"));
+ bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.iOS:
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index f8bb8f4c6a..89786e3bd8 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -111,9 +111,6 @@ namespace osu.Game.Users
[JsonProperty(@"twitter")]
public string Twitter;
- [JsonProperty(@"lastfm")]
- public string Lastfm;
-
[JsonProperty(@"skype")]
public string Skype;
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index 981251784e..e8e41cdbbe 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Utils
// since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports.
if (lastException != null &&
- lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace))
+ lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal))
return;
lastException = exception;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index e8498129a1..ffcbcc81b4 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -25,7 +25,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 4b38d9f22d..004481e9ba 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 64f3d41acb..3ef419c572 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -199,7 +199,9 @@
WARNING
WARNING
WARNING
+ WARNING
HINT
+ WARNING
WARNING
DO_NOT_SHOW
DO_NOT_SHOW
@@ -773,6 +775,7 @@ See the LICENCE file in the repository root for full licence text.
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
True
True
True