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.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs
new file mode 100644
index 0000000000..25bd659a5d
--- /dev/null
+++ b/osu.Android/GameplayScreenRotationLocker.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Android.Content.PM;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game;
+
+namespace osu.Android
+{
+ public class GameplayScreenRotationLocker : Component
+ {
+ private Bindable localUserPlaying;
+
+ [Resolved]
+ private OsuGameActivity gameActivity { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game)
+ {
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(updateLock, true);
+ }
+
+ private void updateLock(ValueChangedEvent userPlaying)
+ {
+ gameActivity.RunOnUiThread(() =>
+ {
+ gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
+ });
+ }
+ }
+}
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index db73bb7e7f..7e250dce0e 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -12,7 +12,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
- protected override Framework.Game CreateGame() => new OsuGameAndroid();
+ protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 7542a2b997..21d6336b2c 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -4,6 +4,7 @@
using System;
using Android.App;
using Android.OS;
+using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
@@ -11,6 +12,15 @@ namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
+ [Cached]
+ private readonly OsuGameActivity gameActivity;
+
+ public OsuGameAndroid(OsuGameActivity activity)
+ : base(null)
+ {
+ gameActivity = activity;
+ }
+
public override Version AssemblyVersion
{
get
@@ -55,6 +65,12 @@ namespace osu.Android
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
+ }
+
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
}
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 0598a50530..a2638e95c8 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -21,6 +21,7 @@
r8
+
@@ -53,4 +54,4 @@
-
+
\ No newline at end of file
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 2079f136d2..836b968a67 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -125,12 +125,14 @@ namespace osu.Desktop
{
base.SetHost(host);
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+
switch (host.Window)
{
// Legacy osuTK DesktopGameWindow
case DesktopGameWindow desktopGameWindow:
desktopGameWindow.CursorState |= CursorState.Hidden;
- desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
+ desktopGameWindow.SetIconFromStream(iconStream);
desktopGameWindow.Title = Name;
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
break;
@@ -138,6 +140,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden;
+ desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
break;
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
index 86174ceb90..efc3f21149 100644
--- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -5,24 +5,24 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
+using osu.Game;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
- private Bindable allowScreenSuspension;
private Bindable disableWinKey;
+ private Bindable localUserPlaying;
- private GameHost host;
+ [Resolved]
+ private GameHost host { get; set; }
[BackgroundDependencyLoader]
- private void load(GameHost host, OsuConfigManager config)
+ private void load(OsuGame game, OsuConfigManager config)
{
- this.host = host;
-
- allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
- allowScreenSuspension.BindValueChanged(_ => updateBlocking());
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+ localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
@@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
private void updateBlocking()
{
- bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
+ bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 1e708cce4b..1b8368794c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
Origin = Anchor.Centre,
Scale = new Vector2(4f),
}, skin);
+ });
+ AddStep("get trails container", () =>
+ {
trails = catcherArea.OfType().Single();
catcherArea.MovableCatcher.SetHyperDashState(2);
});
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 6b8b70ed54..e209d012fa 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.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 System.Threading;
@@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
+ int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
@@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat:
AddNested(new Fruit
{
- Samples = Samples,
+ Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
});
@@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
- set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
+ set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double EndTime => StartTime + Duration;
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.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
index fec1360b26..d49ffa01c5 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -10,7 +10,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
- "Samples": ["drum-hitnormal"]
+ "Samples": ["-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@@ -19,7 +19,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
- "Samples": ["drum-hitnormal"]
+ "Samples": ["-hitnormal"]
}]
}, {
"StartTime": 3750.0,
diff --git a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs
new file mode 100644
index 0000000000..23d9d265be
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs
@@ -0,0 +1,41 @@
+// 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.Game.Rulesets.UI;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestPlayfieldBorder : OsuTestScene
+ {
+ public TestPlayfieldBorder()
+ {
+ Bindable playfieldBorderStyle = new Bindable();
+
+ AddStep("add drawables", () =>
+ {
+ Child = new Container
+ {
+ Size = new Vector2(400, 300),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new PlayfieldBorder
+ {
+ PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle }
+ }
+ }
+ };
+ });
+
+ AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None);
+ AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners);
+ AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index f76635a932..e8272057f3 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -3,6 +3,7 @@
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Configuration
{
@@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
Set(OsuRulesetSetting.SnakingInSliders, true);
Set(OsuRulesetSetting.SnakingOutSliders, true);
Set(OsuRulesetSetting.ShowCursorTrail, true);
+ Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{
SnakingInSliders,
SnakingOutSliders,
- ShowCursorTrail
+ ShowCursorTrail,
+ PlayfieldBorderStyle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index 3dbbdcc5d0..e14d6647d2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 912a705d16..edd684d886 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
- LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
+ LayerBelowRuleset.AddRange(new Drawable[]
+ {
+ new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
+ },
+ distanceSnapGridContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 917382eccf..755ce0866a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
+
+ // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
+ // For now, the samples are attached to and played by the slider itself at the correct end time.
+ Samples = this.GetNodeSamples(repeatCount + 1);
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType())
- repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
+ repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null)
- HeadCircle.Samples = getNodeSamples(0);
+ HeadCircle.Samples = this.GetNodeSamples(0);
}
- private IList getNodeSamples(int nodeIndex) =>
- nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
-
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 4ef9bbe091..50727d590a 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -17,12 +17,16 @@ using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Osu.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
+ private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
@@ -33,12 +37,19 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
+ private readonly Bindable playfieldBorderStyle = new BindableBool();
+
private readonly IDictionary> poolDictionary = new Dictionary>();
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
+ playfieldBorder = new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 3
+ },
spinnerProxies = new ProxyContainer
{
RelativeSizeAxes = Axes.Both
@@ -76,6 +87,12 @@ namespace osu.Game.Rulesets.Osu.UI
AddRangeInternal(poolDictionary.Values);
}
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuRulesetConfigManager config)
+ {
+ config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
+ }
+
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 3870f303b4..705ba3e929 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
@@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = "Cursor trail",
Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
},
+ new SettingsEnumDropdown
+ {
+ LabelText = "Playfield border style",
+ Current = config.GetBindable(OsuRulesetSetting.PlayfieldBorderStyle),
+ },
};
}
}
diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
index 0f6d956b3c..7c1ddd757f 100644
--- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
+++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
@@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(key1, Is.EqualTo(key2));
}
+
+ [TestCase(1.3, DifficultyRating.Easy)]
+ [TestCase(1.993, DifficultyRating.Easy)]
+ [TestCase(1.998, DifficultyRating.Normal)]
+ [TestCase(2.4, DifficultyRating.Normal)]
+ [TestCase(2.693, DifficultyRating.Normal)]
+ [TestCase(2.698, DifficultyRating.Hard)]
+ [TestCase(3.5, DifficultyRating.Hard)]
+ [TestCase(3.993, DifficultyRating.Hard)]
+ [TestCase(3.997, DifficultyRating.Insane)]
+ [TestCase(5.0, DifficultyRating.Insane)]
+ [TestCase(5.292, DifficultyRating.Insane)]
+ [TestCase(5.297, DifficultyRating.Expert)]
+ [TestCase(6.2, DifficultyRating.Expert)]
+ [TestCase(6.493, DifficultyRating.Expert)]
+ [TestCase(6.498, DifficultyRating.ExpertPlus)]
+ [TestCase(8.3, DifficultyRating.ExpertPlus)]
+ public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
+ {
+ var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating);
+
+ Assert.AreEqual(expectedBracket, actualBracket);
+ }
}
}
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/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index 30686cb947..24a0a662ba 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
+
+ [TestCase("202010", true)]
+ [TestCase("20201010", false)]
+ [TestCase("153", true)]
+ [TestCase("1535", false)]
+ public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
+ {
+ var beatmap = getExampleBeatmap();
+ beatmap.OnlineBeatmapID = 20201010;
+ beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
+
+ var criteria = new FilterCriteria { SearchText = query };
+ var carouselItem = new CarouselBeatmap(beatmap);
+ carouselItem.Filter(criteria);
+
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
}
}
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..6ec673704c 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);
@@ -65,17 +89,17 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExternalHideDoesntAffectConfig()
{
- bool originalConfigValue = false;
+ HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks;
createNew();
- AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.ShowInterface));
+ AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
- AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface));
+ AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode));
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
- AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface));
+ AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode));
}
[Test]
@@ -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/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 4fac7bb45f..888a2f2197 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
@@ -35,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestPlayerLoaderContainer container;
private TestPlayer player;
+ private bool epilepsyWarning;
+
[Resolved]
private AudioManager audioManager { get; set; }
@@ -59,6 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning;
foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToTrack(Beatmap.Value.Track);
@@ -251,6 +255,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for player load", () => player.IsLoaded);
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestEpilepsyWarning(bool warning)
+ {
+ AddStep("change epilepsy warning", () => epilepsyWarning = warning);
+ AddStep("load dummy beatmap", () => ResetPlayer(false));
+
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+
+ AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning);
+ }
+
private class TestPlayerLoaderContainer : Container
{
[Cached]
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..4699784327 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
@@ -40,6 +41,12 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets;
}
+ [Test]
+ public void TestManyPanels()
+ {
+ loadBeatmaps(count: 5000, randomDifficulties: true);
+ }
+
[Test]
public void TestKeyRepeat()
{
@@ -394,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
- AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
+ AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
}
[Test]
@@ -707,21 +714,22 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15);
}
- private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null)
+ private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null, bool randomDifficulties = false)
{
- createCarousel(carouselAdjust);
-
- if (beatmapSets == null)
- {
- beatmapSets = new List();
-
- for (int i = 1; i <= set_count; i++)
- beatmapSets.Add(createTestBeatmapSet(i));
- }
-
bool changed = false;
- AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () =>
+
+ createCarousel(c =>
{
+ carouselAdjust?.Invoke(c);
+
+ if (beatmapSets == null)
+ {
+ beatmapSets = new List();
+
+ for (int i = 1; i <= (count ?? set_count); i++)
+ beatmapSets.Add(createTestBeatmapSet(i, randomDifficulties));
+ }
+
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
@@ -807,7 +815,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private bool selectedBeatmapVisible()
{
- var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
+ var currentlySelected = carousel.Items.FirstOrDefault(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
if (currentlySelected == null)
return true;
@@ -820,7 +828,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection is visible", selectedBeatmapVisible);
}
- private BeatmapSetInfo createTestBeatmapSet(int id)
+ private BeatmapSetInfo createTestBeatmapSet(int id, bool randomDifficultyCount = false)
{
return new BeatmapSetInfo
{
@@ -834,42 +842,37 @@ namespace osu.Game.Tests.Visual.SongSelect
Title = $"test set #{id}!",
AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5))
},
- Beatmaps = new List(new[]
- {
- new BeatmapInfo
- {
- OnlineBeatmapID = id * 10,
- Version = "Normal",
- StarDifficulty = 2,
- BaseDifficulty = new BeatmapDifficulty
- {
- OverallDifficulty = 3.5f,
- }
- },
- new BeatmapInfo
- {
- OnlineBeatmapID = id * 10 + 1,
- Version = "Hard",
- StarDifficulty = 5,
- BaseDifficulty = new BeatmapDifficulty
- {
- OverallDifficulty = 5,
- }
- },
- new BeatmapInfo
- {
- OnlineBeatmapID = id * 10 + 2,
- Version = "Insane",
- StarDifficulty = 6,
- BaseDifficulty = new BeatmapDifficulty
- {
- OverallDifficulty = 7,
- }
- },
- }),
+ Beatmaps = getBeatmaps(randomDifficultyCount ? RNG.Next(1, 20) : 3).ToList()
};
}
+ private IEnumerable getBeatmaps(int count)
+ {
+ int id = 0;
+
+ for (int i = 0; i < count; i++)
+ {
+ float diff = (float)i / count * 10;
+
+ string version = "Normal";
+ if (diff > 6.6)
+ version = "Insane";
+ else if (diff > 3.3)
+ version = "Hard";
+
+ yield return new BeatmapInfo
+ {
+ OnlineBeatmapID = id++ * 10,
+ Version = version,
+ StarDifficulty = diff,
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ OverallDifficulty = diff,
+ }
+ };
+ }
+ }
+
private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id)
{
var toReturn = new BeatmapSetInfo
@@ -908,10 +911,25 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapCarousel : BeatmapCarousel
{
- public new List Items => base.Items;
-
public bool PendingFilterTask => PendingFilter != null;
+ public IEnumerable Items
+ {
+ get
+ {
+ foreach (var item in ScrollableContent)
+ {
+ yield return item;
+
+ if (item is DrawableCarouselBeatmapSet set)
+ {
+ foreach (var difficulty in set.DrawableBeatmaps)
+ yield return difficulty;
+ }
+ }
+ }
+ }
+
protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty();
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 0299b7a084..cd97ffe9e7 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -507,7 +507,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var selectedPanel = songSelect.Carousel.ChildrenOfType().First(s => s.Item.State.Value == CarouselItemState.Selected);
// special case for converts checked here.
- return selectedPanel.ChildrenOfType().All(i =>
+ return selectedPanel.ChildrenOfType().All(i =>
i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0);
});
@@ -606,10 +606,10 @@ namespace osu.Game.Tests.Visual.SongSelect
set = songSelect.Carousel.ChildrenOfType().First();
});
- DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null;
+ FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon", () =>
{
- difficultyIcon = set.ChildrenOfType()
+ difficultyIcon = set.ChildrenOfType()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
});
@@ -634,13 +634,13 @@ namespace osu.Game.Tests.Visual.SongSelect
}));
BeatmapInfo filteredBeatmap = null;
- DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null;
+ FilterableDifficultyIcon filteredIcon = null;
AddStep("Get filtered icon", () =>
{
filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
- filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex);
+ filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex);
});
AddStep("Click on a filtered difficulty", () =>
@@ -674,10 +674,10 @@ namespace osu.Game.Tests.Visual.SongSelect
return set != null;
});
- DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null;
+ FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon for different ruleset", () =>
{
- difficultyIcon = set.ChildrenOfType()
+ difficultyIcon = set.ChildrenOfType()
.First(icon => icon.Item.Beatmap.Ruleset.ID == 3);
});
@@ -725,10 +725,10 @@ namespace osu.Game.Tests.Visual.SongSelect
return set != null;
});
- DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null;
+ FilterableGroupedDifficultyIcon groupIcon = null;
AddStep("Find group icon for different ruleset", () =>
{
- groupIcon = set.ChildrenOfType()
+ groupIcon = set.ChildrenOfType()
.First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3);
});
@@ -821,9 +821,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap);
- private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, DrawableCarouselBeatmapSet.FilterableDifficultyIcon icon)
+ private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
{
- return set.ChildrenOfType().ToList().FindIndex(i => i == icon);
+ return set.ChildrenOfType().ToList().FindIndex(i => i == icon);
}
private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id));
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 945a60fb62..c1f4c07833 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -13,9 +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
{
@@ -124,13 +127,22 @@ namespace osu.Game.Beatmaps
/// The that best describes .
public static DifficultyRating GetDifficultyRating(double starRating)
{
- if (starRating < 2.0) return DifficultyRating.Easy;
- if (starRating < 2.7) return DifficultyRating.Normal;
- if (starRating < 4.0) return DifficultyRating.Hard;
- if (starRating < 5.3) return DifficultyRating.Insane;
- if (starRating < 6.5) return DifficultyRating.Expert;
+ if (Precision.AlmostBigger(starRating, 6.5, 0.005))
+ return DifficultyRating.ExpertPlus;
- return DifficultyRating.ExpertPlus;
+ if (Precision.AlmostBigger(starRating, 5.3, 0.005))
+ return DifficultyRating.Expert;
+
+ if (Precision.AlmostBigger(starRating, 4.0, 0.005))
+ return DifficultyRating.Insane;
+
+ if (Precision.AlmostBigger(starRating, 2.7, 0.005))
+ return DifficultyRating.Hard;
+
+ if (Precision.AlmostBigger(starRating, 2.0, 0.005))
+ return DifficultyRating.Normal;
+
+ return DifficultyRating.Easy;
}
private CancellationTokenSource trackedUpdateCancellationSource;
@@ -228,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..ffd8d14048 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -92,13 +92,14 @@ namespace osu.Game.Beatmaps
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; }
+ public bool EpilepsyWarning { get; set; }
// Editor
// This bookmarks stuff is necessary because DB doesn't know how to store int[]
[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/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
index 45327d4514..a1d5e33d1e 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
@@ -47,7 +47,10 @@ namespace osu.Game.Beatmaps.Drawables
private readonly IReadOnlyList mods;
private readonly bool shouldShowTooltip;
- private readonly IBindable difficultyBindable = new Bindable();
+
+ private readonly bool performBackgroundDifficultyLookup;
+
+ private readonly Bindable difficultyBindable = new Bindable();
private Drawable background;
@@ -70,10 +73,12 @@ namespace osu.Game.Beatmaps.Drawables
///
/// The beatmap to show the difficulty of.
/// Whether to display a tooltip when hovered.
- public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true)
+ /// Whether to perform difficulty lookup (including calculation if necessary).
+ public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
{
this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap));
this.shouldShowTooltip = shouldShowTooltip;
+ this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup;
AutoSizeAxes = Axes.Both;
@@ -112,9 +117,13 @@ namespace osu.Game.Beatmaps.Drawables
// the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment)
Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
},
- new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0),
};
+ if (performBackgroundDifficultyLookup)
+ iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0));
+ else
+ difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0);
+
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating));
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index b30ec0ca2c..442be6e837 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -175,6 +175,10 @@ namespace osu.Game.Beatmaps.Formats
case @"WidescreenStoryboard":
beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break;
+
+ case @"EpilepsyWarning":
+ beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
+ break;
}
}
@@ -307,12 +311,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/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs
new file mode 100644
index 0000000000..b0b55dd811
--- /dev/null
+++ b/osu.Game/Configuration/HUDVisibilityMode.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Configuration
+{
+ public enum HUDVisibilityMode
+ {
+ Never,
+
+ [Description("Hide during gameplay")]
+ HideDuringGameplay,
+
+ [Description("Hide during breaks")]
+ HideDuringBreaks,
+
+ Always
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 78179a781a..7d601c0cb9 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.HitLighting, true);
- Set(OsuSetting.ShowInterface, true);
+ Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
Set(OsuSetting.ShowProgressGraph, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
@@ -190,7 +190,7 @@ namespace osu.Game.Configuration
AlwaysPlayFirstComboBreak,
ScoreMeter,
FloatingComments,
- ShowInterface,
+ HUDVisibilityMode,
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
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/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
new file mode 100644
index 0000000000..1c05de832e
--- /dev/null
+++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
@@ -0,0 +1,508 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using osu.Game.Database;
+
+namespace osu.Game.Migrations
+{
+ [DbContext(typeof(OsuDbContext))]
+ [Migration("20201019224408_AddEpilepsyWarning")]
+ partial class AddEpilepsyWarning
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ApproachRate");
+
+ b.Property("CircleSize");
+
+ b.Property("DrainRate");
+
+ b.Property("OverallDifficulty");
+
+ b.Property("SliderMultiplier");
+
+ b.Property("SliderTickRate");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapDifficulty");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AudioLeadIn");
+
+ b.Property("BPM");
+
+ b.Property("BaseDifficultyID");
+
+ b.Property("BeatDivisor");
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("Countdown");
+
+ b.Property("DistanceSpacing");
+
+ b.Property("EpilepsyWarning");
+
+ b.Property("GridSize");
+
+ b.Property("Hash");
+
+ b.Property("Hidden");
+
+ b.Property("Length");
+
+ b.Property("LetterboxInBreaks");
+
+ b.Property("MD5Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapID");
+
+ b.Property("Path");
+
+ b.Property("RulesetID");
+
+ b.Property("SpecialStyle");
+
+ b.Property("StackLeniency");
+
+ b.Property("StarDifficulty");
+
+ b.Property("Status");
+
+ b.Property("StoredBookmarks");
+
+ b.Property("TimelineZoom");
+
+ b.Property("Version");
+
+ b.Property("WidescreenStoryboard");
+
+ b.HasKey("ID");
+
+ b.HasIndex("BaseDifficultyID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("Hash");
+
+ b.HasIndex("MD5Hash");
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapID")
+ .IsUnique();
+
+ b.HasIndex("RulesetID");
+
+ b.ToTable("BeatmapInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Artist");
+
+ b.Property("ArtistUnicode");
+
+ b.Property("AudioFile");
+
+ b.Property("AuthorString")
+ .HasColumnName("Author");
+
+ b.Property("BackgroundFile");
+
+ b.Property("PreviewTime");
+
+ b.Property("Source");
+
+ b.Property("Tags");
+
+ b.Property("Title");
+
+ b.Property("TitleUnicode");
+
+ b.Property("VideoFile");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapMetadata");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.HasKey("ID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("FileInfoID");
+
+ b.ToTable("BeatmapSetFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("DateAdded");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapSetID");
+
+ b.Property("Protected");
+
+ b.Property("Status");
+
+ b.HasKey("ID");
+
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapSetID")
+ .IsUnique();
+
+ b.ToTable("BeatmapSetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Key")
+ .HasColumnName("Key");
+
+ b.Property("RulesetID");
+
+ b.Property("SkinInfoID");
+
+ b.Property("StringValue")
+ .HasColumnName("Value");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("SkinInfoID");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("Settings");
+ });
+
+ modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Hash");
+
+ b.Property("ReferenceCount");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("ReferenceCount");
+
+ b.ToTable("FileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("IntAction")
+ .HasColumnName("Action");
+
+ b.Property("KeysString")
+ .HasColumnName("Keys");
+
+ b.Property("RulesetID");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("IntAction");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("KeyBinding");
+ });
+
+ modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Available");
+
+ b.Property("InstantiationInfo");
+
+ b.Property("Name");
+
+ b.Property("ShortName");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Available");
+
+ b.HasIndex("ShortName")
+ .IsUnique();
+
+ b.ToTable("RulesetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.Property("ScoreInfoID");
+
+ b.HasKey("ID");
+
+ b.HasIndex("FileInfoID");
+
+ b.HasIndex("ScoreInfoID");
+
+ b.ToTable("ScoreFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Accuracy")
+ .HasColumnType("DECIMAL(1,4)");
+
+ b.Property("BeatmapInfoID");
+
+ b.Property("Combo");
+
+ b.Property("Date");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("MaxCombo");
+
+ b.Property("ModsJson")
+ .HasColumnName("Mods");
+
+ b.Property("OnlineScoreID");
+
+ b.Property("PP");
+
+ b.Property("Rank");
+
+ b.Property("RulesetID");
+
+ b.Property("StatisticsJson")
+ .HasColumnName("Statistics");
+
+ b.Property("TotalScore");
+
+ b.Property("UserID")
+ .HasColumnName("UserID");
+
+ b.Property("UserString")
+ .HasColumnName("User");
+
+ b.HasKey("ID");
+
+ b.HasIndex("BeatmapInfoID");
+
+ b.HasIndex("OnlineScoreID")
+ .IsUnique();
+
+ b.HasIndex("RulesetID");
+
+ b.ToTable("ScoreInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.Property