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 3df894fbcc..2d531cf01e 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.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9289a6162c..a221ca7966 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
}
};
- trailsTarget.Add(trails = new CatcherTrailDisplay(this));
+ trails = new CatcherTrailDisplay(this);
updateCatcher();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // don't add in above load as we may potentially modify a parent in an unsafe manner.
+ trailsTarget.Add(trails);
+ }
+
///
/// Creates proxied content to be displayed beneath hitobjects.
///
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 524ea27efa..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 });
@@ -116,7 +120,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
- density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
+ if (prevNoteTimes.Count >= 2)
+ density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
@@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData:
{
- conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
+ conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index fe146c5324..30d33de06e 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
private const float osu_base_scoring_distance = 100;
- public readonly double EndTime;
- public readonly double SegmentDuration;
+ public readonly int StartTime;
+ public readonly int EndTime;
+ public readonly int SegmentDuration;
public readonly int SpanCount;
private PatternType convertType;
@@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
- SpanCount = repeatsData?.SpanCount() ?? 1;
+ Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
- // The true distance, accounting for any repeats
- double distance = (distanceData?.Distance ?? 0) * SpanCount;
- // The velocity of the osu! hit object - calculated as the velocity of a slider
- double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
- // The duration of the osu! hit object
- double osuDuration = distance / osuVelocity;
+ double beatLength;
+#pragma warning disable 618
+ if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
+#pragma warning restore 618
+ beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ else
+ beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
- EndTime = hitObject.StartTime + osuDuration;
- SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
+ SpanCount = repeatsData?.SpanCount() ?? 1;
+ StartTime = (int)Math.Round(hitObject.StartTime);
+
+ // This matches stable's calculation.
+ EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
+
+ SegmentDuration = (EndTime - StartTime) / SpanCount;
}
public override IEnumerable Generate()
@@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
- if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
+ if (EndTime != (int)Math.Round(obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns == 1)
{
var pattern = new Pattern();
- addToPattern(pattern, 0, HitObject.StartTime, EndTime);
+ addToPattern(pattern, 0, StartTime, EndTime);
return pattern;
}
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
- return generateRandomHoldNotes(HitObject.StartTime, 1);
+ return generateRandomHoldNotes(StartTime, 1);
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
- return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
+ return generateRandomNotes(StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
- return generateStair(HitObject.StartTime);
+ return generateStair(StartTime);
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
- return generateRandomMultipleNotes(HitObject.StartTime);
+ return generateRandomMultipleNotes(StartTime);
- double duration = EndTime - HitObject.StartTime;
+ double duration = EndTime - StartTime;
if (duration >= 4000)
- return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
+ return generateNRandomNotes(StartTime, 0.23, 0, 0);
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
- return generateTiledHoldNotes(HitObject.StartTime);
+ return generateTiledHoldNotes(StartTime);
- return generateHoldAndNormalNotes(HitObject.StartTime);
+ return generateHoldAndNormalNotes(StartTime);
}
if (SegmentDuration <= 110)
@@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
- return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
+ return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
+ return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
+ return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
}
if (ConversionDifficulty > 4)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
+ return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
+ return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
}
if (ConversionDifficulty > 2.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
+ return generateNRandomNotes(StartTime, 0.3, 0, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
+ return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
}
if (convertType.HasFlag(PatternType.LowProbability))
- return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
+ return generateNRandomNotes(StartTime, 0.17, 0, 0);
- return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
+ return generateNRandomNotes(StartTime, 0.27, 0, 0);
}
///
@@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Start time of each hold note.
/// Number of hold notes.
/// The containing the hit objects.
- private Pattern generateRandomHoldNotes(double startTime, int noteCount)
+ private Pattern generateRandomHoldNotes(int startTime, int noteCount)
{
// - - - -
// ■ - ■ ■
@@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The start time.
/// The number of notes.
/// The containing the hit objects.
- private Pattern generateRandomNotes(double startTime, int noteCount)
+ private Pattern generateRandomNotes(int startTime, int noteCount)
{
// - - - -
// x - - -
@@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time.
/// The containing the hit objects.
- private Pattern generateStair(double startTime)
+ private Pattern generateStair(int startTime)
{
// - - - -
// x - - -
@@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time.
/// The containing the hit objects.
- private Pattern generateRandomMultipleNotes(double startTime)
+ private Pattern generateRandomMultipleNotes(int startTime)
{
// - - - -
// x - - -
@@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The probability required for 3 hold notes to be generated.
/// The probability required for 4 hold notes to be generated.
/// The containing the hit objects.
- private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
+ private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
{
// - - - -
// ■ - ■ ■
@@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
- canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
+ canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
p2 = 1;
@@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The first hold note start time.
/// The containing the hit objects.
- private Pattern generateTiledHoldNotes(double startTime)
+ private Pattern generateTiledHoldNotes(int startTime)
{
// - - - -
// ■ ■ ■ ■
@@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int columnRepeat = Math.Min(SpanCount, TotalColumns);
+ // Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
+ int endTime = startTime + SegmentDuration * SpanCount;
+
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
@@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i < columnRepeat; i++)
{
nextColumn = FindAvailableColumn(nextColumn, pattern);
- addToPattern(pattern, nextColumn, startTime, EndTime);
+ addToPattern(pattern, nextColumn, startTime, endTime);
startTime += SegmentDuration;
}
@@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The start time of notes.
/// The containing the hit objects.
- private Pattern generateHoldAndNormalNotes(double startTime)
+ private Pattern generateHoldAndNormalNotes(int startTime)
{
// - - - -
// ■ x x -
@@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i <= SpanCount; i++)
{
- if (!(ignoreHead && startTime == HitObject.StartTime))
+ if (!(ignoreHead && startTime == StartTime))
{
for (int j = 0; j < noteCount; j++)
{
@@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The time to retrieve the sample info list from.
///
- private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
+ private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
/// Retrieves the list of node samples that occur at time greater than or equal to .
///
/// The time to retrieve node samples at.
- private List> nodeSamplesAt(double time)
+ private List> nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
return null;
- // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
- var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
+ var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
// avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
@@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// The column to add the note to.
/// The start time of the note.
/// The end time of the note (set to for a non-hold note).
- private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
+ private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
{
ManiaHitObject newObject;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index d5286a3779..f816a70ab3 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
{
- private readonly double endTime;
+ private readonly int endTime;
+ private readonly PatternType convertType;
- public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
- : base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
+ public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
+ : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
- endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
+ endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
+
+ convertType = PreviousPattern.ColumnWithObjects == TotalColumns
+ ? PatternType.None
+ : PatternType.ForceNotStack;
}
public override IEnumerable Generate()
@@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
case 8:
- addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
+ addToPattern(pattern, getRandomColumn(), generateHold);
break;
default:
- if (TotalColumns > 0)
- addToPattern(pattern, GetRandomColumn(), generateHold);
+ addToPattern(pattern, getRandomColumn(0), generateHold);
break;
}
return pattern;
}
+ private int getRandomColumn(int? lowerBound = null)
+ {
+ if ((convertType & PatternType.ForceNotStack) > 0)
+ return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
+
+ return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
+ }
+
///
/// Constructs and adds a note to a pattern.
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 84f950997d..bc4ab55767 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 4:
centreProbability = 0;
- p2 = Math.Min(p2 * 2, 0.2);
+
+ // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
+ // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
+ // so it needs to be converted to from a probability and then back after the multiplication.
+ p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
p3 = 0;
break;
@@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 6:
centreProbability = 0;
- p2 = Math.Min(p2 * 2, 0.5);
- p3 = Math.Min(p3 * 2, 0.15);
+
+ // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
+ // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
+ // so it needs to be converted to from a probability and then back after the multiplication.
+ p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
+ p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
break;
}
+ // The stable values were allowed to exceed 1, which indicate <0% probability.
+ // These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
+ p2 = Math.Clamp(p2, 0, 1);
+ p3 = Math.Clamp(p3, 0, 1);
+
double centreVal = Random.NextDouble();
int noteCount = GetRandomNoteCount(p2, p3);
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/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index dab923d75b..b6e1af57fd 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsInstanceOf(decoder);
}
}
+
+ [Test]
+ public void TestMultiSegmentSliders()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("multi-segment-slider.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream);
+
+ // Multi-segment
+ var first = ((IHasPath)decoded.HitObjects[0]).Path;
+
+ Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
+ Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
+ Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
+ Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
+
+ Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
+ Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
+ Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
+ Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
+ Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
+ Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
+ Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
+
+ // Single-segment
+ var second = ((IHasPath)decoded.HitObjects[1]).Path;
+
+ Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
+ Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
+ Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
+ Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
+ Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
+ Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
+
+ // Implicit multi-segment
+ var third = ((IHasPath)decoded.HitObjects[2]).Path;
+
+ Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
+ Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
+ Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
+ Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
+ Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
+
+ Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
+ Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
+ Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
+ Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
+ Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
+ Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
+ Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
+ }
+ }
}
}
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/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index b6ab73eceb..045246e5ed 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get();
+ var osuStorage = storage as MigratableStorage;
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath(".");
@@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
- foreach (var file in OsuStorage.IGNORE_FILES)
+ Assert.That(osuStorage, Is.Not.Null);
+
+ foreach (var file in osuStorage.IgnoreFiles)
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
- foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
+ foreach (var dir in osuStorage.IgnoreDirectories)
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);
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/multi-segment-slider.osu b/osu.Game.Tests/Resources/multi-segment-slider.osu
new file mode 100644
index 0000000000..6eabe640e4
--- /dev/null
+++ b/osu.Game.Tests/Resources/multi-segment-slider.osu
@@ -0,0 +1,11 @@
+osu file format v128
+
+[HitObjects]
+// Multi-segment
+63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0:
+
+// Single-segment
+63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0:
+
+// Implicit multi-segment
+32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
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/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index ace57aad1d..9f16312121 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
}
+
+ ///
+ /// Test to see that all s contribute to score portions in correct amounts.
+ ///
+ /// Scoring mode to test.
+ /// The that will be applied to selected hit objects.
+ /// The maximum achievable.
+ /// Expected score after all objects have been judged, rounded to the nearest integer.
+ ///
+ /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
+ ///
+ /// For standardised scoring, is calculated using the following formula:
+ /// 1_000_000 * (((3 * ) / (4 * )) * 30% + (bestCombo / maxCombo) * 70%)
+ ///
+ ///
+ /// For classic scoring, is calculated using the following formula:
+ /// / * 936
+ /// where 936 is simplified from:
+ /// 75% * 4 * 300 * (1 + 1/25)
+ ///
+ ///
+ [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
+ [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
+ [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
+ [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
+ [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
+ public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
+ {
+ var minResult = new TestJudgement(hitResult).MinResult;
+
+ IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = new List(Enumerable.Repeat(new TestHitObject(maxResult), 4))
+ };
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
+
+ for (int i = 0; i < 4; i++)
+ {
+ var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
+ {
+ Type = i == 2 ? minResult : hitResult
+ };
+ scoreProcessor.ApplyResult(judgementResult);
+ }
+
+ Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
+ }
+
+ ///
+ /// This test uses a beatmap with four small ticks and one object with the of .
+ /// Its goal is to ensure that with the of ,
+ /// small ticks contribute to the accuracy portion, but not the combo portion.
+ /// In contrast, does not have separate combo and accuracy portion (they are multiplied by each other).
+ ///
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
+ public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
+ {
+ IEnumerable hitObjects = Enumerable
+ .Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
+ .Append(new TestHitObject(HitResult.Ok));
+ IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = hitObjects.ToList()
+ };
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
+
+ for (int i = 0; i < 4; i++)
+ {
+ var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
+ {
+ Type = i == 2 ? HitResult.SmallTickMiss : hitResult
+ };
+ scoreProcessor.ApplyResult(judgementResult);
+ }
+
+ var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
+ {
+ Type = HitResult.Ok
+ };
+ scoreProcessor.ApplyResult(lastJudgementResult);
+
+ Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
+ }
+
+ [Test]
+ public void TestEmptyBeatmap(
+ [Values(ScoringMode.Standardised, ScoringMode.Classic)]
+ ScoringMode scoringMode)
+ {
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
+
+ Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
+ }
+
+ [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
+ [TestCase(HitResult.Meh, HitResult.Miss)]
+ [TestCase(HitResult.Ok, HitResult.Miss)]
+ [TestCase(HitResult.Good, HitResult.Miss)]
+ [TestCase(HitResult.Great, HitResult.Miss)]
+ [TestCase(HitResult.Perfect, HitResult.Miss)]
+ [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
+ [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
+ [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
+ [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
+ public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
+ {
+ Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult);
+ }
+
+ [TestCase(HitResult.None, false)]
+ [TestCase(HitResult.IgnoreMiss, false)]
+ [TestCase(HitResult.IgnoreHit, false)]
+ [TestCase(HitResult.Miss, true)]
+ [TestCase(HitResult.Meh, true)]
+ [TestCase(HitResult.Ok, true)]
+ [TestCase(HitResult.Good, true)]
+ [TestCase(HitResult.Great, true)]
+ [TestCase(HitResult.Perfect, true)]
+ [TestCase(HitResult.SmallTickMiss, false)]
+ [TestCase(HitResult.SmallTickHit, false)]
+ [TestCase(HitResult.LargeTickMiss, true)]
+ [TestCase(HitResult.LargeTickHit, true)]
+ [TestCase(HitResult.SmallBonus, false)]
+ [TestCase(HitResult.LargeBonus, false)]
+ public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
+ {
+ Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo());
+ }
+
+ [TestCase(HitResult.None, false)]
+ [TestCase(HitResult.IgnoreMiss, false)]
+ [TestCase(HitResult.IgnoreHit, false)]
+ [TestCase(HitResult.Miss, true)]
+ [TestCase(HitResult.Meh, true)]
+ [TestCase(HitResult.Ok, true)]
+ [TestCase(HitResult.Good, true)]
+ [TestCase(HitResult.Great, true)]
+ [TestCase(HitResult.Perfect, true)]
+ [TestCase(HitResult.SmallTickMiss, true)]
+ [TestCase(HitResult.SmallTickHit, true)]
+ [TestCase(HitResult.LargeTickMiss, true)]
+ [TestCase(HitResult.LargeTickHit, true)]
+ [TestCase(HitResult.SmallBonus, false)]
+ [TestCase(HitResult.LargeBonus, false)]
+ public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
+ {
+ Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy());
+ }
+
+ [TestCase(HitResult.None, false)]
+ [TestCase(HitResult.IgnoreMiss, false)]
+ [TestCase(HitResult.IgnoreHit, false)]
+ [TestCase(HitResult.Miss, false)]
+ [TestCase(HitResult.Meh, false)]
+ [TestCase(HitResult.Ok, false)]
+ [TestCase(HitResult.Good, false)]
+ [TestCase(HitResult.Great, false)]
+ [TestCase(HitResult.Perfect, false)]
+ [TestCase(HitResult.SmallTickMiss, false)]
+ [TestCase(HitResult.SmallTickHit, false)]
+ [TestCase(HitResult.LargeTickMiss, false)]
+ [TestCase(HitResult.LargeTickHit, false)]
+ [TestCase(HitResult.SmallBonus, true)]
+ [TestCase(HitResult.LargeBonus, true)]
+ public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
+ {
+ Assert.AreEqual(expectedReturnValue, hitResult.IsBonus());
+ }
+
+ [TestCase(HitResult.None, false)]
+ [TestCase(HitResult.IgnoreMiss, false)]
+ [TestCase(HitResult.IgnoreHit, true)]
+ [TestCase(HitResult.Miss, false)]
+ [TestCase(HitResult.Meh, true)]
+ [TestCase(HitResult.Ok, true)]
+ [TestCase(HitResult.Good, true)]
+ [TestCase(HitResult.Great, true)]
+ [TestCase(HitResult.Perfect, true)]
+ [TestCase(HitResult.SmallTickMiss, false)]
+ [TestCase(HitResult.SmallTickHit, true)]
+ [TestCase(HitResult.LargeTickMiss, false)]
+ [TestCase(HitResult.LargeTickHit, true)]
+ [TestCase(HitResult.SmallBonus, true)]
+ [TestCase(HitResult.LargeBonus, true)]
+ public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
+ {
+ Assert.AreEqual(expectedReturnValue, hitResult.IsHit());
+ }
+
+ [TestCase(HitResult.None, false)]
+ [TestCase(HitResult.IgnoreMiss, false)]
+ [TestCase(HitResult.IgnoreHit, false)]
+ [TestCase(HitResult.Miss, true)]
+ [TestCase(HitResult.Meh, true)]
+ [TestCase(HitResult.Ok, true)]
+ [TestCase(HitResult.Good, true)]
+ [TestCase(HitResult.Great, true)]
+ [TestCase(HitResult.Perfect, true)]
+ [TestCase(HitResult.SmallTickMiss, true)]
+ [TestCase(HitResult.SmallTickHit, true)]
+ [TestCase(HitResult.LargeTickMiss, true)]
+ [TestCase(HitResult.LargeTickHit, true)]
+ [TestCase(HitResult.SmallBonus, true)]
+ [TestCase(HitResult.LargeBonus, true)]
+ public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
+ {
+ Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
+ }
+
+ private class TestJudgement : Judgement
+ {
+ public override HitResult MaxResult { get; }
+
+ public TestJudgement(HitResult maxResult)
+ {
+ MaxResult = maxResult;
+ }
+ }
+
+ private class TestHitObject : HitObject
+ {
+ private readonly HitResult maxResult;
+
+ public override Judgement CreateJudgement()
+ {
+ return new TestJudgement(maxResult);
+ }
+
+ public TestHitObject(HitResult maxResult)
+ {
+ this.maxResult = maxResult;
+ }
+ }
}
}
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..e212ceeba7
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
@@ -0,0 +1,54 @@
+// 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.Extensions.IEnumerableExtensions;
+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;
+ });
+ }
+
+ [Test]
+ public void TestVeryLargeScore()
+ {
+ AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000));
+ }
+ }
+}
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 3aff390a47..8669235a7a 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -394,7 +394,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.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
new file mode 100644
index 0000000000..567d9f0d62
--- /dev/null
+++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
@@ -0,0 +1,169 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Platform;
+using osu.Game.Tournament.Configuration;
+using osu.Game.Tests;
+
+namespace osu.Game.Tournament.Tests.NonVisual
+{
+ [TestFixture]
+ public class CustomTourneyDirectoryTest
+ {
+ [Test]
+ public void TestDefaultDirectory()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
+ {
+ try
+ {
+ var osu = loadOsu(host);
+ var storage = osu.Dependencies.Get();
+
+ Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestCustomDirectory()
+ {
+ using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
+ {
+ string osuDesktopStorage = basePath(nameof(TestCustomDirectory));
+ const string custom_tournament = "custom";
+
+ // need access before the game has constructed its own storage yet.
+ Storage storage = new DesktopStorage(osuDesktopStorage, host);
+ // manual cleaning so we can prepare a config file.
+ storage.DeleteDirectory(string.Empty);
+
+ using (var storageConfig = new TournamentStorageManager(storage))
+ storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
+
+ try
+ {
+ var osu = loadOsu(host);
+
+ storage = osu.Dependencies.Get();
+
+ Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament)));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestMigration()
+ {
+ using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration.
+ {
+ string osuRoot = basePath(nameof(TestMigration));
+ string configFile = Path.Combine(osuRoot, "tournament.ini");
+
+ if (File.Exists(configFile))
+ File.Delete(configFile);
+
+ // Recreate the old setup that uses "tournament" as the base path.
+ string oldPath = Path.Combine(osuRoot, "tournament");
+
+ string videosPath = Path.Combine(oldPath, "videos");
+ string modsPath = Path.Combine(oldPath, "mods");
+ string flagsPath = Path.Combine(oldPath, "flags");
+
+ Directory.CreateDirectory(videosPath);
+ Directory.CreateDirectory(modsPath);
+ Directory.CreateDirectory(flagsPath);
+
+ // Define testing files corresponding to the specific file migrations that are needed
+ string bracketFile = Path.Combine(osuRoot, "bracket.json");
+
+ string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
+ string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
+ string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
+
+ // Define sample files to test recursive copying
+ string videoFile = Path.Combine(videosPath, "video.mp4");
+ string modFile = Path.Combine(modsPath, "mod.png");
+ string flagFile = Path.Combine(flagsPath, "flag.png");
+
+ File.WriteAllText(bracketFile, "{}");
+ File.WriteAllText(drawingsConfig, "test");
+ File.WriteAllText(drawingsFile, "test");
+ File.WriteAllText(drawingsResult, "test");
+ File.WriteAllText(videoFile, "test");
+ File.WriteAllText(modFile, "test");
+ File.WriteAllText(flagFile, "test");
+
+ try
+ {
+ var osu = loadOsu(host);
+
+ var storage = osu.Dependencies.Get();
+
+ string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
+
+ videosPath = Path.Combine(migratedPath, "videos");
+ modsPath = Path.Combine(migratedPath, "mods");
+ flagsPath = Path.Combine(migratedPath, "flags");
+
+ videoFile = Path.Combine(videosPath, "video.mp4");
+ modFile = Path.Combine(modsPath, "mod.png");
+ flagFile = Path.Combine(flagsPath, "flag.png");
+
+ Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
+
+ Assert.True(storage.Exists("bracket.json"));
+ Assert.True(storage.Exists("drawings.txt"));
+ Assert.True(storage.Exists("drawings_results.txt"));
+
+ Assert.True(storage.Exists("drawings.ini"));
+
+ Assert.True(storage.Exists(videoFile));
+ Assert.True(storage.Exists(modFile));
+ Assert.True(storage.Exists(flagFile));
+ }
+ finally
+ {
+ host.Storage.Delete("tournament.ini");
+ host.Storage.DeleteDirectory("tournaments");
+ host.Exit();
+ }
+ }
+ }
+
+ private TournamentGameBase loadOsu(GameHost host)
+ {
+ var osu = new TournamentGameBase();
+ Task.Run(() => host.Run(osu));
+ waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
+ return osu;
+ }
+
+ private static void waitForOrAssert(Func result, string failureMessage, int timeout = 90000)
+ {
+ Task task = Task.Run(() =>
+ {
+ while (!result()) Thread.Sleep(200);
+ });
+
+ Assert.IsTrue(task.Wait(timeout), failureMessage);
+ }
+
+ private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
+ }
+}
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
index 317c5f6a56..2709580385 100644
--- a/osu.Game.Tournament/Components/TourneyVideo.cs
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video;
using osu.Framework.Timing;
using osu.Game.Graphics;
+using osu.Game.Tournament.IO;
namespace osu.Game.Tournament.Components
{
@@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
private readonly string filename;
private readonly bool drawFallbackGradient;
private Video video;
-
private ManualClock manualClock;
public TourneyVideo(string filename, bool drawFallbackGradient = false)
@@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader]
- private void load(TournamentStorage storage)
+ private void load(TournamentVideoResourceStore storage)
{
- var stream = storage.GetStream($@"videos/{filename}");
+ var stream = storage.GetStream(filename);
if (stream != null)
{
diff --git a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs
new file mode 100644
index 0000000000..e3d0a9e75c
--- /dev/null
+++ b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs
@@ -0,0 +1,23 @@
+// 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.Configuration;
+using osu.Framework.Platform;
+
+namespace osu.Game.Tournament.Configuration
+{
+ public class TournamentStorageManager : IniConfigManager
+ {
+ protected override string Filename => "tournament.ini";
+
+ public TournamentStorageManager(Storage storage)
+ : base(storage)
+ {
+ }
+ }
+
+ public enum StorageConfig
+ {
+ CurrentTournament,
+ }
+}
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
new file mode 100644
index 0000000000..2e8a6ce667
--- /dev/null
+++ b/osu.Game.Tournament/IO/TournamentStorage.cs
@@ -0,0 +1,72 @@
+// 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.Logging;
+using osu.Framework.Platform;
+using osu.Game.IO;
+using System.IO;
+using osu.Game.Tournament.Configuration;
+
+namespace osu.Game.Tournament.IO
+{
+ public class TournamentStorage : MigratableStorage
+ {
+ private const string default_tournament = "default";
+ private readonly Storage storage;
+ private readonly TournamentStorageManager storageConfig;
+
+ public TournamentStorage(Storage storage)
+ : base(storage.GetStorageForDirectory("tournaments"), string.Empty)
+ {
+ this.storage = storage;
+
+ storageConfig = new TournamentStorageManager(storage);
+
+ if (storage.Exists("tournament.ini"))
+ {
+ ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament)));
+ }
+ else
+ Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
+
+ Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
+ }
+
+ public override void Migrate(Storage newStorage)
+ {
+ // this migration only happens once on moving to the per-tournament storage system.
+ // listed files are those known at that point in time.
+ // this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
+
+ var source = new DirectoryInfo(storage.GetFullPath("tournament"));
+ var destination = new DirectoryInfo(newStorage.GetFullPath("."));
+
+ if (source.Exists)
+ {
+ Logger.Log("Migrating tournament assets to default tournament storage.");
+ CopyRecursive(source, destination);
+ DeleteRecursive(source);
+ }
+
+ moveFileIfExists("bracket.json", destination);
+ moveFileIfExists("drawings.txt", destination);
+ moveFileIfExists("drawings_results.txt", destination);
+ moveFileIfExists("drawings.ini", destination);
+
+ ChangeTargetStorage(newStorage);
+ storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
+ storageConfig.Save();
+ }
+
+ private void moveFileIfExists(string file, DirectoryInfo destination)
+ {
+ if (!storage.Exists(file))
+ return;
+
+ Logger.Log($"Migrating {file} to default tournament storage.");
+ var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
+ AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
+ fileInfo.Delete();
+ }
+ }
+}
diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs
similarity index 58%
rename from osu.Game.Tournament/TournamentStorage.cs
rename to osu.Game.Tournament/IO/TournamentVideoResourceStore.cs
index 139ad3857b..4b26840b79 100644
--- a/osu.Game.Tournament/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs
@@ -4,12 +4,12 @@
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
-namespace osu.Game.Tournament
+namespace osu.Game.Tournament.IO
{
- internal class TournamentStorage : NamespacedResourceStore
+ public class TournamentVideoResourceStore : NamespacedResourceStore
{
- public TournamentStorage(Storage storage)
- : base(new StorageBackedResourceStore(storage), "tournament")
+ public TournamentVideoResourceStore(Storage storage)
+ : base(new StorageBackedResourceStore(storage), "videos")
{
AddExtension("m4v");
AddExtension("avi");
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.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 5fc1d03f6d..dbda6aa023 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -8,11 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
-using osu.Framework.IO.Stores;
using osu.Framework.Platform;
+using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
+using osu.Game.Tournament.IO;
using osu.Game.Tournament.Models;
using osu.Game.Users;
using osuTK.Input;
@@ -23,13 +24,8 @@ namespace osu.Game.Tournament
public class TournamentGameBase : OsuGameBase
{
private const string bracket_filename = "bracket.json";
-
private LadderInfo ladder;
-
- private Storage storage;
-
- private TournamentStorage tournamentStorage;
-
+ private TournamentStorage storage;
private DependencyContainer dependencies;
private FileBasedIPC ipc;
@@ -39,15 +35,14 @@ namespace osu.Game.Tournament
}
[BackgroundDependencyLoader]
- private void load(Storage storage)
+ private void load(Storage baseStorage)
{
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
- dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage));
+ dependencies.CacheAs(storage = new TournamentStorage(baseStorage));
+ dependencies.Cache(new TournamentVideoResourceStore(storage));
- Textures.AddStore(new TextureLoaderStore(tournamentStorage));
-
- this.storage = storage;
+ Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
readBracket();
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
index 8c12ca6f6e..c1f4c07833 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -13,10 +13,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
+using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Beatmaps
{
@@ -238,6 +240,24 @@ namespace osu.Game.Beatmaps
return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo);
}
+ catch (BeatmapInvalidForRulesetException e)
+ {
+ // Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset.
+
+ // Ensure the beatmap's default ruleset isn't the one already being converted to.
+ // This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided.
+ if (rulesetInfo.Equals(beatmapInfo.Ruleset))
+ {
+ Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
+ return difficultyCache[key] = new StarDifficulty();
+ }
+
+ // Check the cache first because this is now a different ruleset than the one previously guarded against.
+ if (tryGetExisting(beatmapInfo, beatmapInfo.Ruleset, Array.Empty(), out var existingDefault, out var existingDefaultKey))
+ return existingDefault;
+
+ return computeDifficulty(existingDefaultKey, beatmapInfo, beatmapInfo.Ruleset);
+ }
catch
{
return difficultyCache[key] = new StarDifficulty();
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/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs
index 91a557094d..b96181416d 100644
--- a/osu.Game/Graphics/UserInterface/RollingCounter.cs
+++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs
@@ -56,8 +56,7 @@ namespace osu.Game.Graphics.UserInterface
return;
displayedCount = value;
- if (displayedCountSpriteText != null)
- displayedCountSpriteText.Text = FormatCount(value);
+ UpdateDisplay();
}
}
@@ -73,10 +72,17 @@ namespace osu.Game.Graphics.UserInterface
private void load()
{
displayedCountSpriteText = CreateSpriteText();
- displayedCountSpriteText.Text = FormatCount(DisplayedCount);
+
+ UpdateDisplay();
Child = displayedCountSpriteText;
}
+ protected void UpdateDisplay()
+ {
+ if (displayedCountSpriteText != null)
+ displayedCountSpriteText.Text = FormatCount(DisplayedCount);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index 73bbe5f03e..d75e49a4ce 100644
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -1,35 +1,37 @@
// 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.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;
-
///
- /// How many leading zeroes the counter has.
+ /// Whether comma separators should be displayed.
///
- public uint LeadingZeroes { get; }
+ public bool UseCommaSeparator { get; }
+
+ public Bindable RequiredDisplayDigits { get; } = new Bindable();
///
/// Displays score.
///
/// How many leading zeroes the counter will have.
- public ScoreCounter(uint leading = 0)
+ /// Whether comma separators should be displayed.
+ protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
{
- LeadingZeroes = leading;
- }
+ UseCommaSeparator = useCommaSeparator;
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.BlueLighter;
+ RequiredDisplayDigits.Value = leading;
+ RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
+ }
protected override double GetProportionalDuration(double currentValue, double newValue)
{
@@ -38,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
protected override string FormatCount(double count)
{
- string format = new string('0', (int)LeadingZeroes);
+ string format = new string('0', RequiredDisplayDigits.Value);
if (UseCommaSeparator)
{
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
index 0e995ca73d..ec68223a3d 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -73,8 +73,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
new Container
{
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
+ // top right works better when the vertical height of the component changes smoothly (avoids weird layout animations).
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>
diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs
new file mode 100644
index 0000000000..1b76725b04
--- /dev/null
+++ b/osu.Game/IO/MigratableStorage.cs
@@ -0,0 +1,132 @@
+// 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 System.Threading;
+using osu.Framework.Platform;
+
+namespace osu.Game.IO
+{
+ ///
+ /// A that is migratable to different locations.
+ ///
+ public abstract class MigratableStorage : WrappedStorage
+ {
+ ///
+ /// A relative list of directory paths which should not be migrated.
+ ///
+ public virtual string[] IgnoreDirectories => Array.Empty();
+
+ ///
+ /// A relative list of file paths which should not be migrated.
+ ///
+ public virtual string[] IgnoreFiles => Array.Empty();
+
+ protected MigratableStorage(Storage storage, string subPath = null)
+ : base(storage, subPath)
+ {
+ }
+
+ ///
+ /// A general purpose migration method to move the storage to a different location.
+ /// The target storage of the migration.
+ ///
+ public virtual void Migrate(Storage newStorage)
+ {
+ var source = new DirectoryInfo(GetFullPath("."));
+ var destination = new DirectoryInfo(newStorage.GetFullPath("."));
+
+ // using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
+ var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
+ var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
+
+ if (sourceUri == destinationUri)
+ throw new ArgumentException("Destination provided is already the current location", destination.FullName);
+
+ if (sourceUri.IsBaseOf(destinationUri))
+ throw new ArgumentException("Destination provided is inside the source", destination.FullName);
+
+ // ensure the new location has no files present, else hard abort
+ if (destination.Exists)
+ {
+ if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
+ throw new ArgumentException("Destination provided already has files or directories present", destination.FullName);
+ }
+
+ CopyRecursive(source, destination);
+ ChangeTargetStorage(newStorage);
+ DeleteRecursive(source);
+ }
+
+ protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
+ {
+ foreach (System.IO.FileInfo fi in target.GetFiles())
+ {
+ if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
+ continue;
+
+ AttemptOperation(() => fi.Delete());
+ }
+
+ foreach (DirectoryInfo dir in target.GetDirectories())
+ {
+ if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
+ continue;
+
+ AttemptOperation(() => dir.Delete(true));
+ }
+
+ if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
+ AttemptOperation(target.Delete);
+ }
+
+ protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
+ {
+ // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
+ if (!destination.Exists)
+ Directory.CreateDirectory(destination.FullName);
+
+ foreach (System.IO.FileInfo fi in source.GetFiles())
+ {
+ if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
+ continue;
+
+ AttemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
+ }
+
+ foreach (DirectoryInfo dir in source.GetDirectories())
+ {
+ if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
+ continue;
+
+ CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
+ }
+ }
+
+ ///
+ /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
+ ///
+ /// The action to perform.
+ /// The number of attempts (250ms wait between each).
+ protected static void AttemptOperation(Action action, int attempts = 10)
+ {
+ while (true)
+ {
+ try
+ {
+ action();
+ return;
+ }
+ catch (Exception)
+ {
+ if (attempts-- == 0)
+ throw;
+ }
+
+ Thread.Sleep(250);
+ }
+ }
+ }
+}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 1d15294666..8097f61ea4 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -1,11 +1,8 @@
// 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.Diagnostics;
-using System.IO;
using System.Linq;
-using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -13,7 +10,7 @@ using osu.Game.Configuration;
namespace osu.Game.IO
{
- public class OsuStorage : WrappedStorage
+ public class OsuStorage : MigratableStorage
{
///
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
@@ -36,9 +33,9 @@ namespace osu.Game.IO
private readonly StorageConfigManager storageConfig;
private readonly Storage defaultStorage;
- public static readonly string[] IGNORE_DIRECTORIES = { "cache" };
+ public override string[] IgnoreDirectories => new[] { "cache" };
- public static readonly string[] IGNORE_FILES =
+ public override string[] IgnoreFiles => new[]
{
"framework.ini",
"storage.ini"
@@ -103,106 +100,11 @@ namespace osu.Game.IO
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
}
- public void Migrate(string newLocation)
+ public override void Migrate(Storage newStorage)
{
- var source = new DirectoryInfo(GetFullPath("."));
- var destination = new DirectoryInfo(newLocation);
-
- // using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
- var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
- var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
-
- if (sourceUri == destinationUri)
- throw new ArgumentException("Destination provided is already the current location", nameof(newLocation));
-
- if (sourceUri.IsBaseOf(destinationUri))
- throw new ArgumentException("Destination provided is inside the source", nameof(newLocation));
-
- // ensure the new location has no files present, else hard abort
- if (destination.Exists)
- {
- if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
- throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation));
-
- deleteRecursive(destination);
- }
-
- copyRecursive(source, destination);
-
- ChangeTargetStorage(host.GetStorage(newLocation));
-
- storageConfig.Set(StorageConfig.FullPath, newLocation);
+ base.Migrate(newStorage);
+ storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
-
- deleteRecursive(source);
- }
-
- private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
- {
- foreach (System.IO.FileInfo fi in target.GetFiles())
- {
- if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
- continue;
-
- attemptOperation(() => fi.Delete());
- }
-
- foreach (DirectoryInfo dir in target.GetDirectories())
- {
- if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
- continue;
-
- attemptOperation(() => dir.Delete(true));
- }
-
- if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
- attemptOperation(target.Delete);
- }
-
- private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
- {
- // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
- Directory.CreateDirectory(destination.FullName);
-
- foreach (System.IO.FileInfo fi in source.GetFiles())
- {
- if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
- continue;
-
- attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
- }
-
- foreach (DirectoryInfo dir in source.GetDirectories())
- {
- if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
- continue;
-
- copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
- }
- }
-
- ///
- /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
- ///
- /// The action to perform.
- /// The number of attempts (250ms wait between each).
- private static void attemptOperation(Action action, int attempts = 10)
- {
- while (true)
- {
- try
- {
- action();
- return;
- }
- catch (Exception)
- {
- if (attempts-- == 0)
- throw;
- }
-
- Thread.Sleep(250);
- }
}
}
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/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index 084ba89f6e..2acee394a6 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
@@ -150,9 +151,9 @@ namespace osu.Game.Online.Leaderboards
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
- replacePlaceholder(new RetrievalFailurePlaceholder
+ replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
{
- OnRetry = UpdateScores,
+ Action = UpdateScores,
});
break;
diff --git a/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs b/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs
deleted file mode 100644
index d109f28e72..0000000000
--- a/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs
+++ /dev/null
@@ -1,65 +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;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-using osu.Game.Online.Placeholders;
-using osuTK;
-
-namespace osu.Game.Online.Leaderboards
-{
- public class RetrievalFailurePlaceholder : Placeholder
- {
- public Action OnRetry;
-
- public RetrievalFailurePlaceholder()
- {
- AddArbitraryDrawable(new RetryButton
- {
- Action = () => OnRetry?.Invoke(),
- Padding = new MarginPadding { Right = 10 }
- });
-
- AddText(@"Couldn't retrieve scores!");
- }
-
- public class RetryButton : OsuHoverContainer
- {
- private readonly SpriteIcon icon;
-
- public new Action Action;
-
- public RetryButton()
- {
- AutoSizeAxes = Axes.Both;
-
- Child = new OsuClickableContainer
- {
- AutoSizeAxes = Axes.Both,
- Action = () => Action?.Invoke(),
- Child = icon = new SpriteIcon
- {
- Icon = FontAwesome.Solid.Sync,
- Size = new Vector2(TEXT_SIZE),
- Shadow = true,
- },
- };
- }
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- icon.ScaleTo(0.8f, 4000, Easing.OutQuint);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- icon.ScaleTo(1, 1000, Easing.OutElastic);
- base.OnMouseUp(e);
- }
- }
- }
-}
diff --git a/osu.Game/Online/Placeholders/ClickablePlaceholder.cs b/osu.Game/Online/Placeholders/ClickablePlaceholder.cs
new file mode 100644
index 0000000000..936ad79c64
--- /dev/null
+++ b/osu.Game/Online/Placeholders/ClickablePlaceholder.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 System;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Online.Placeholders
+{
+ public class ClickablePlaceholder : Placeholder
+ {
+ public Action Action;
+
+ public ClickablePlaceholder(string actionMessage, IconUsage icon)
+ {
+ OsuTextFlowContainer textFlow;
+
+ AddArbitraryDrawable(new OsuAnimatedButton
+ {
+ AutoSizeAxes = Framework.Graphics.Axes.Both,
+ Child = textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
+ {
+ AutoSizeAxes = Framework.Graphics.Axes.Both,
+ Margin = new Framework.Graphics.MarginPadding(5)
+ },
+ Action = () => Action?.Invoke()
+ });
+
+ textFlow.AddIcon(icon, i =>
+ {
+ i.Padding = new Framework.Graphics.MarginPadding { Right = 10 };
+ });
+
+ textFlow.AddText(actionMessage);
+ }
+ }
+}
diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
index 73b0fa27c3..f8a326a52e 100644
--- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs
+++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
@@ -2,45 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Online.Placeholders
{
- public sealed class LoginPlaceholder : Placeholder
+ public sealed class LoginPlaceholder : ClickablePlaceholder
{
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
public LoginPlaceholder(string actionMessage)
+ : base(actionMessage, FontAwesome.Solid.UserLock)
{
- AddIcon(FontAwesome.Solid.UserLock, cp =>
- {
- cp.Font = cp.Font.With(size: TEXT_SIZE);
- cp.Padding = new MarginPadding { Right = 10 };
- });
-
- AddText(actionMessage);
- }
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- this.ScaleTo(0.8f, 4000, Easing.OutQuint);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- this.ScaleTo(1, 1000, Easing.OutElastic);
- base.OnMouseUp(e);
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- login?.Show();
- return base.OnClick(e);
+ Action = () => login?.Show();
}
}
}
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/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index f61ff43ca9..84766f196a 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -406,7 +406,7 @@ namespace osu.Game
public void Migrate(string path)
{
contextFactory.FlushConnections();
- (Storage as OsuStorage)?.Migrate(path);
+ (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
}
}
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/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 73968761e2..66b3b8c4ca 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -76,7 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsEnumDropdown
{
LabelText = "Score display mode",
- Current = config.GetBindable(OsuSetting.ScoreDisplayMode)
+ Current = config.GetBindable(OsuSetting.ScoreDisplayMode),
+ Keywords = new[] { "scoring" }
}
};
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 f6adeced96..44b22033dc 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -70,53 +70,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
else if (type.HasFlag(LegacyHitObjectType.Slider))
{
- PathType pathType = PathType.Catmull;
double? length = null;
- string[] pointSplit = split[5].Split('|');
-
- int pointCount = 1;
-
- foreach (var t in pointSplit)
- {
- if (t.Length > 1)
- pointCount++;
- }
-
- var points = new Vector2[pointCount];
-
- int pointIndex = 1;
-
- foreach (string t in pointSplit)
- {
- if (t.Length == 1)
- {
- switch (t)
- {
- case @"C":
- pathType = PathType.Catmull;
- break;
-
- case @"B":
- pathType = PathType.Bezier;
- break;
-
- case @"L":
- pathType = PathType.Linear;
- break;
-
- case @"P":
- pathType = PathType.PerfectCurve;
- break;
- }
-
- continue;
- }
-
- string[] temp = t.Split(':');
- points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
- }
-
int repeatCount = Parsing.ParseInt(split[6]);
if (repeatCount > 9000)
@@ -183,7 +138,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
- result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
+ result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
}
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
@@ -204,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);
@@ -252,8 +207,108 @@ namespace osu.Game.Rulesets.Objects.Legacy
bankInfo.Filename = split.Length > 4 ? split[4] : null;
}
- private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
+ private PathType convertPathType(string input)
{
+ switch (input[0])
+ {
+ default:
+ case 'C':
+ return PathType.Catmull;
+
+ case 'B':
+ return PathType.Bezier;
+
+ case 'L':
+ return PathType.Linear;
+
+ case 'P':
+ return PathType.PerfectCurve;
+ }
+ }
+
+ ///
+ /// Converts a given point string into a set of path control points.
+ ///
+ ///
+ /// A point string takes the form: X|1:1|2:2|2:2|3:3|Y|1:1|2:2.
+ /// This has three segments:
+ ///
+ /// -
+ /// X: { (1,1), (2,2) } (implicit segment)
+ ///
+ /// -
+ /// X: { (2,2), (3,3) } (implicit segment)
+ ///
+ /// -
+ /// Y: { (3,3), (1,1), (2, 2) } (explicit segment)
+ ///
+ ///
+ ///
+ /// The point string.
+ /// The positional offset to apply to the control points.
+ /// All control points in the resultant path.
+ private PathControlPoint[] convertPathString(string pointString, Vector2 offset)
+ {
+ // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints().
+ string[] pointSplit = pointString.Split('|');
+
+ var controlPoints = new List>();
+ int startIndex = 0;
+ int endIndex = 0;
+ bool first = true;
+
+ while (++endIndex < pointSplit.Length)
+ {
+ // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1).
+ if (pointSplit[endIndex].Length > 1)
+ continue;
+
+ // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment.
+ // The start of the next segment is the index after the type descriptor.
+ string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null;
+
+ controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), endPoint, first, offset));
+ startIndex = endIndex;
+ first = false;
+ }
+
+ if (endIndex > startIndex)
+ controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), null, first, offset));
+
+ return mergePointsLists(controlPoints);
+ }
+
+ ///
+ /// Converts a given point list into a set of path segments.
+ ///
+ /// The point list.
+ /// Any extra endpoint to consider as part of the points. This will NOT be returned.
+ /// Whether this is the first segment in the set. If true the first of the returned segments will contain a zero point.
+ /// The positional offset to apply to the control points.
+ /// The set of points contained by as one or more segments of the path, prepended by an extra zero point if is true.
+ private IEnumerable> convertPoints(ReadOnlyMemory points, string endPoint, bool first, Vector2 offset)
+ {
+ PathType type = convertPathType(points.Span[0]);
+
+ int readOffset = first ? 1 : 0; // First control point is zero for the first segment.
+ int readablePoints = points.Length - 1; // Total points readable from the base point span.
+ int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span.
+
+ var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength];
+
+ // Fill any non-read points.
+ for (int i = 0; i < readOffset; i++)
+ vertices[i] = new PathControlPoint();
+
+ // Parse into control points.
+ for (int i = 1; i < points.Length; i++)
+ readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]);
+
+ // If an endpoint is given, add it to the end.
+ if (endPoint != null)
+ readPoint(endPoint, offset, out vertices[^1]);
+
+ // Edge-case rules (to match stable).
if (type == PathType.PerfectCurve)
{
if (vertices.Length != 3)
@@ -265,29 +320,64 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
}
- var points = new List(vertices.Length)
- {
- new PathControlPoint
- {
- Position = { Value = vertices[0] },
- Type = { Value = type }
- }
- };
+ // The first control point must have a definite type.
+ vertices[0].Type.Value = type;
- for (int i = 1; i < vertices.Length; i++)
+ // A path can have multiple implicit segments of the same type if there are two sequential control points with the same position.
+ // To handle such cases, this code may return multiple path segments with the final control point in each segment having a non-null type.
+ // For the point string X|1:1|2:2|2:2|3:3, this code returns the segments:
+ // X: { (1,1), (2, 2) }
+ // X: { (3, 3) }
+ // Note: (2, 2) is not returned in the second segments, as it is implicit in the path.
+ int startIndex = 0;
+ int endIndex = 0;
+
+ while (++endIndex < vertices.Length - endPointLength)
{
- if (vertices[i] == vertices[i - 1])
- {
- points[^1].Type.Value = type;
+ if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value)
continue;
- }
- points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
+ // Force a type on the last point, and return the current control point set as a segment.
+ vertices[endIndex - 1].Type.Value = type;
+ yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
+
+ // Skip the current control point - as it's the same as the one that's just been returned.
+ startIndex = endIndex + 1;
}
- return points.ToArray();
+ if (endIndex > startIndex)
+ yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
- static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
+ static void readPoint(string value, Vector2 startPos, out PathControlPoint point)
+ {
+ string[] vertexSplit = value.Split(':');
+
+ Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos;
+ point = new PathControlPoint { Position = { Value = pos } };
+ }
+
+ static bool isLinear(PathControlPoint[] p) => Precision.AlmostEquals(0, (p[1].Position.Value.Y - p[0].Position.Value.Y) * (p[2].Position.Value.X - p[0].Position.Value.X)
+ - (p[1].Position.Value.X - p[0].Position.Value.X) * (p[2].Position.Value.Y - p[0].Position.Value.Y));
+ }
+
+ private PathControlPoint[] mergePointsLists(List> controlPointList)
+ {
+ int totalCount = 0;
+
+ foreach (var arr in controlPointList)
+ totalCount += arr.Length;
+
+ var mergedArray = new PathControlPoint[totalCount];
+ var mergedArrayMemory = mergedArray.AsMemory();
+ int copyIndex = 0;
+
+ foreach (var arr in controlPointList)
+ {
+ arr.CopyTo(mergedArrayMemory.Slice(copyIndex));
+ copyIndex += arr.Length;
+ }
+
+ return mergedArray;
}
///
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/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 7a5b707357..33271d9689 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion;
private readonly double comboPortion;
- private int maxHighestCombo;
+ private int maxAchievableCombo;
private double maxBaseScore;
private double rollingMaxBaseScore;
private double baseScore;
@@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Scoring
private double getScore(ScoringMode mode)
{
- return GetScore(mode, maxHighestCombo,
+ return GetScore(mode, maxAchievableCombo,
maxBaseScore > 0 ? baseScore / maxBaseScore : 0,
- maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0,
+ maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1,
scoreResultCounts);
}
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Scoring
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
- return getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
+ return getBonusScore(statistics) + (accuracyRatio * Math.Max(1, maxCombo) * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
}
}
@@ -265,14 +265,8 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults)
{
- maxHighestCombo = HighestCombo.Value;
+ maxAchievableCombo = HighestCombo.Value;
maxBaseScore = baseScore;
-
- if (maxBaseScore == 0 || maxHighestCombo == 0)
- {
- Mode.Value = ScoringMode.Classic;
- Mode.Disabled = true;
- }
}
baseScore = 0;
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/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index f2ac61eaf4..07de2bf601 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -136,7 +136,11 @@ namespace osu.Game.Rulesets.UI
KeyBindingContainer.Add(receptor);
keyCounter.SetReceptor(receptor);
- keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b)));
+ keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings
+ .Select(b => b.GetAction())
+ .Distinct()
+ .OrderBy(action => action)
+ .Select(action => new KeyCounterAction(action)));
}
public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler
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/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index be3bca3242..9aff4ddf8f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
- Colour = colours.Blue.Opacity(0.2f),
+ BaseColour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
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 54%
rename from osu.Game/Graphics/UserInterface/SimpleComboCounter.cs
rename to osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
index c9790aed46..63e7a88550 100644
--- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs
@@ -4,18 +4,21 @@
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
{
- protected override double RollingDuration => 750;
+ private readonly Vector2 offset = new Vector2(20, 5);
- public SimpleComboCounter()
+ [Resolved(canBeNull: true)]
+ private HUDOverlay hud { get; set; }
+
+ public DefaultComboCounter()
{
Current.Value = DisplayedCount = 0;
}
@@ -23,6 +26,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..7f5e81d5ef
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/IScoreCounter.cs
@@ -0,0 +1,25 @@
+// 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; }
+
+ ///
+ /// The number of digits required to display most sane scores.
+ /// This may be exceeded in very rare cases, but is useful to pad or space the display to avoid it jumping around.
+ ///
+ Bindable RequiredDisplayDigits { 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..b46f5684b1
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.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;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class SkinnableScoreCounter : SkinnableDrawable, IScoreCounter
+ {
+ public Bindable Current { get; } = new Bindable();
+
+ private Bindable scoreDisplayMode;
+
+ public Bindable RequiredDisplayDigits { get; } = new Bindable();
+
+ public SkinnableScoreCounter()
+ : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
+ {
+ CentreComponent = false;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
+ scoreDisplayMode.BindValueChanged(scoreMode =>
+ {
+ switch (scoreMode.NewValue)
+ {
+ case ScoringMode.Standardised:
+ RequiredDisplayDigits.Value = 6;
+ break;
+
+ case ScoringMode.Classic:
+ RequiredDisplayDigits.Value = 8;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(scoreMode));
+ }
+ }, true);
+ }
+
+ private IScoreCounter skinnedCounter;
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ skinnedCounter = Drawable as IScoreCounter;
+
+ skinnedCounter?.Current.BindTo(Current);
+ skinnedCounter?.RequiredDisplayDigits.BindTo(RequiredDisplayDigits);
+ }
+ }
+}
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 74714e7e59..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.
///
@@ -89,6 +92,11 @@ namespace osu.Game.Screens.Play
public BreakOverlay BreakOverlay;
+ ///
+ /// Whether the gameplay is currently in a break.
+ ///
+ public readonly IBindable IsBreakTime = new BindableBool();
+
private BreakTracker breakTracker;
private SkipOverlay skipOverlay;
@@ -213,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)
{
@@ -224,9 +236,12 @@ namespace osu.Game.Screens.Play
skipOverlay.Hide();
}
- DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
+ DrawableRuleset.IsPaused.BindValueChanged(paused =>
+ {
+ updateGameplayState();
+ samplePlaybackDisabled.Value = paused.NewValue;
+ });
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
- breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@@ -256,7 +271,8 @@ namespace osu.Game.Screens.Play
foreach (var mod in Mods.Value.OfType())
mod.ApplyToHealthProcessor(HealthProcessor);
- breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
+ IsBreakTime.BindTo(breakTracker.IsBreakTime);
+ IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
private Drawable createUnderlayComponents() =>
@@ -354,6 +370,7 @@ namespace osu.Game.Screens.Play
private void onBreakTimeChanged(ValueChangedEvent isBreakTime)
{
+ updateGameplayState();
updatePauseOnFocusLostState();
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
}
@@ -746,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/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index c48cd238c0..026ce01857 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -10,9 +10,11 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds;
@@ -22,7 +24,7 @@ using osuTK;
namespace osu.Game.Screens.Ranking
{
- public abstract class ResultsScreen : OsuScreen
+ public abstract class ResultsScreen : OsuScreen, IKeyBindingHandler
{
protected const float BACKGROUND_BLUR = 20;
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y;
@@ -314,6 +316,22 @@ namespace osu.Game.Screens.Ranking
}
}
+ public bool OnPressed(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.Select:
+ statisticsPanel.ToggleVisibility();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(GlobalAction action)
+ {
+ }
+
private class VerticalScrollContainer : OsuScrollContainer
{
protected override Container Content => content;
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..5eda374337
--- /dev/null
+++ b/osu.Game/Skinning/LegacyAccuracyCounter.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 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))
+ ?.With(s => s.Anchor = s.Origin = Anchor.TopRight);
+
+ 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..5bffeff5a8
--- /dev/null
+++ b/osu.Game/Skinning/LegacyScoreCounter.cs
@@ -0,0 +1,40 @@
+// 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))
+ .With(s => s.Anchor = s.Origin = Anchor.TopRight);
+ }
+}
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 8b10f0a7f7..de7bde824f 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 88abbca73d..9c22dec330 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -80,7 +80,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