diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b3f7c67c51..97fcb52ab1 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2021.608.0",
+ "version": "2021.705.0",
"commands": [
"localisation"
]
diff --git a/.editorconfig b/.editorconfig
index f4d7e08d08..19bd89c52f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -157,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
-csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@@ -168,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
-csharp_style_prefer_index_operator = true:warning
-csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_index_operator = false:silent
+csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers
diff --git a/README.md b/README.md
index 2213b42121..e95c12cfdc 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
-The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
+The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
## Status
@@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward.
+- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu!
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 5eb5efa54c..3dd6be7307 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d7c116411a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 89b551286b..bb0a487274 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d7c116411a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index c845d7f276..cd57d7478e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 47cd39dc5a..910751a723 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
+ scheduleRecheck = false;
+
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
- scheduleRecheck = false;
}
else
{
@@ -110,13 +111,14 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
+ scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
+ Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@@ -141,7 +143,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index ad5c323e9b..53a4e5edf5 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -5,8 +5,8 @@
true
A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
-
osu!lazer
- osu!lazer
+ osu!
+ osu!
lazer.ico
app.manifest
0.0.0
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index fa182f8e70..1757fd7c73 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -3,7 +3,7 @@
osulazer
0.0.0
- osu!lazer
+ osu!
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
@@ -20,4 +20,3 @@
-
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 7a74563b2b..da8a0540f4 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs
new file mode 100644
index 0000000000..158c8edba5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs
@@ -0,0 +1,66 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class CatchEditorTestSceneContainer : Container
+ {
+ [Cached(typeof(Playfield))]
+ public readonly ScrollingPlayfield Playfield;
+
+ protected override Container Content { get; }
+
+ public CatchEditorTestSceneContainer()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Width = CatchPlayfield.WIDTH;
+ Height = 1000;
+ Padding = new MarginPadding
+ {
+ Bottom = 100
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ TimeRange = 1000,
+ RelativeSizeAxes = Axes.Both,
+ Child = Playfield = new TestCatchPlayfield
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ new PlayfieldBorder
+ {
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
+ Clock = new FramedClock(new StopwatchClock(true))
+ },
+ Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ };
+ }
+
+ private class TestCatchPlayfield : CatchEditorPlayfield
+ {
+ public TestCatchPlayfield()
+ : base(new BeatmapDifficulty { CircleSize = 0 })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs
new file mode 100644
index 0000000000..1d30ae34cd
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs
@@ -0,0 +1,79 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
+ {
+ protected const double TIME_SNAP = 100;
+
+ protected DrawableCatchHitObject LastObject;
+
+ protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
+
+ protected override Container Content => contentContainer;
+
+ private readonly CatchEditorTestSceneContainer contentContainer;
+
+ protected CatchPlacementBlueprintTestScene()
+ {
+ base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
+
+ contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ HitObjectContainer.Clear();
+ ResetPlacement();
+ LastObject = null;
+ });
+
+ protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
+ {
+ float y = HitObjectContainer.PositionAtTime(time);
+ Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ InputManager.MoveMouseTo(pos);
+ });
+
+ protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
+ {
+ InputManager.Click(button);
+ });
+
+ protected IEnumerable FruitOutlines => Content.ChildrenOfType();
+
+ // Unused because AddHitObject is overriden
+ protected override Container CreateHitObjectContainer() => new Container();
+
+ protected override void AddHitObject(DrawableHitObject hitObject)
+ {
+ LastObject = (DrawableCatchHitObject)hitObject;
+ contentContainer.Playfield.HitObjectContainer.Add(hitObject);
+ }
+
+ protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
+ {
+ var result = base.SnapForBlueprint(blueprint);
+ result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
+ return result;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
new file mode 100644
index 0000000000..dcdc32145b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
@@ -0,0 +1,24 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
+
+ protected override Container Content => contentContainer;
+
+ private readonly CatchEditorTestSceneContainer contentContainer;
+
+ protected CatchSelectionBlueprintTestScene()
+ {
+ base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..e3811b7669
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
@@ -0,0 +1,87 @@
+// 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 NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
+
+ protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
+
+ protected override void AddHitObject(DrawableHitObject hitObject)
+ {
+ // Create nested bananas (but positions are not randomized because beatmap processing is not done).
+ hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
+
+ base.AddHitObject(hitObject);
+ }
+
+ [Test]
+ public void TestBasicPlacement()
+ {
+ const double start_time = 100;
+ const double end_time = 500;
+
+ AddMoveStep(start_time, 0);
+ AddClickStep(MouseButton.Left);
+ AddMoveStep(end_time, 0);
+ AddClickStep(MouseButton.Right);
+ AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
+ AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
+ AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
+ }
+
+ [Test]
+ public void TestReversePlacement()
+ {
+ const double start_time = 100;
+ const double end_time = 500;
+
+ AddMoveStep(end_time, 0);
+ AddClickStep(MouseButton.Left);
+ AddMoveStep(start_time, 0);
+ AddClickStep(MouseButton.Right);
+ AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
+ AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
+ }
+
+ [Test]
+ public void TestFinishWithZeroDuration()
+ {
+ AddMoveStep(100, 0);
+ AddClickStep(MouseButton.Left);
+ AddClickStep(MouseButton.Right);
+ AddAssert("banana shower is not placed", () => LastObject == null);
+ AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
+ }
+
+ [Test]
+ public void TestOpacity()
+ {
+ AddMoveStep(100, 0);
+ AddClickStep(MouseButton.Left);
+ AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
+ AddMoveStep(200, 0);
+ AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
+ AddMoveStep(100, 0);
+ AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
+ }
+
+ private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType().Single();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..4b1c45ae2f
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs
@@ -0,0 +1,44 @@
+// 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 NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
+
+ protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
+
+ [Test]
+ public void TestFruitPlacementPosition()
+ {
+ const double time = 300;
+ const float x = CatchPlayfield.CENTER_X;
+
+ AddMoveStep(time, x);
+ AddClickStep(MouseButton.Left);
+
+ AddAssert("outline position is correct", () =>
+ {
+ var outline = FruitOutlines.Single();
+ return Precision.AlmostEquals(outline.X, x) &&
+ Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
+ });
+
+ AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
+ AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..1b96175020
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
+ {
+ public TestSceneJuiceStreamSelectionBlueprint()
+ {
+ var hitObject = new JuiceStream
+ {
+ OriginalX = 100,
+ StartTime = 100,
+ Path = new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(200, 100),
+ new Vector2(0, 200),
+ }),
+ };
+ var controlPoint = new ControlPointInfo();
+ controlPoint.Add(0, new TimingControlPoint
+ {
+ BeatLength = 100
+ });
+ hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
+ AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ec186bcfb2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -0,0 +1,114 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using Direction = osu.Game.Rulesets.Catch.UI.Direction;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchSkinConfiguration : OsuTestScene
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ private Catcher catcher;
+
+ private readonly Container container;
+
+ public TestSceneCatchSkinConfiguration()
+ {
+ Add(droppedObjectContainer = new DroppedObjectContainer());
+ Add(container = new Container { RelativeSizeAxes = Axes.Both });
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestCatcherPlateFlipping(bool flip)
+ {
+ AddStep("setup catcher", () =>
+ {
+ var skin = new TestSkin { FlipCatcherPlate = flip };
+ container.Child = new SkinProvidingContainer(skin)
+ {
+ Child = catcher = new Catcher(new Container())
+ {
+ Anchor = Anchor.Centre
+ }
+ };
+ });
+
+ Fruit fruit = new Fruit();
+
+ AddStep("catch fruit", () => catchFruit(fruit, 20));
+
+ float position = 0;
+
+ AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
+
+ AddStep("face left", () => catcher.VisualDirection = Direction.Left);
+
+ if (flip)
+ AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ else
+ AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+
+ AddStep("face right", () => catcher.VisualDirection = Direction.Right);
+
+ AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ }
+
+ private float getCaughtObjectPosition(Fruit fruit)
+ {
+ var caughtObject = catcher.ChildrenOfType().Single(c => c.HitObject == fruit);
+ return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
+ }
+
+ private void catchFruit(Fruit fruit, float x)
+ {
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ var drawableFruit = new DrawableFruit(fruit) { X = x };
+ var judgement = fruit.CreateJudgement();
+ catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
+ {
+ Type = judgement.MaxResult
+ });
+ }
+
+ private class TestSkin : DefaultSkin
+ {
+ public bool FlipCatcherPlate { get; set; }
+
+ public TestSkin()
+ : base(null)
+ {
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is CatchSkinConfiguration config)
+ {
+ if (config == CatchSkinConfiguration.FlipCatcherPlate)
+ return SkinUtils.As(new Bindable(FlipCatcherPlate));
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 1ad45d2f13..8359657f84 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -194,9 +194,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
- catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y < -25));
+ catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 7fa981d492..e7b0259ea2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("finish hyper-dashing", () =>
{
- catcherArea.MovableCatcher.SetHyperDashState(1);
+ catcherArea.MovableCatcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms();
});
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 83d0744588..484da8e22e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
index 69054e2c81..5a32d241ad 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
@@ -23,5 +24,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
: base(new THitObject())
{
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
index 298f9474b0..720d730858 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint
where THitObject : CatchHitObject
{
+ protected override bool AlwaysShowWhenSelected => true;
+
public override Vector2 ScreenSpaceSelectionPoint
{
get
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
index 8769acc382..345b59bdcd 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,7 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
- Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
InternalChild = new BorderPiece();
}
@@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
Colour = osuColour.Yellow;
}
- public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
{
- X = hitObject.EffectiveX;
- Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
Scale = new Vector2(hitObject.Scale);
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
new file mode 100644
index 0000000000..48d90e8b24
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
@@ -0,0 +1,53 @@
+// 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 osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class NestedOutlineContainer : CompositeDrawable
+ {
+ private readonly List nestedHitObjects = new List();
+
+ public NestedOutlineContainer()
+ {
+ Anchor = Anchor.BottomLeft;
+ }
+
+ public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
+ {
+ X = parentHitObject.OriginalX;
+ Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
+ }
+
+ public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
+ {
+ nestedHitObjects.Clear();
+ nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
+ .OfType()
+ .Where(h => !(h is TinyDroplet)));
+
+ while (nestedHitObjects.Count < InternalChildren.Count)
+ RemoveInternal(InternalChildren[^1]);
+
+ while (InternalChildren.Count < nestedHitObjects.Count)
+ AddInternal(new FruitOutline());
+
+ for (int i = 0; i < nestedHitObjects.Count; i++)
+ {
+ var hitObject = nestedHitObjects[i];
+ var outline = (FruitOutline)InternalChildren[i];
+ outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
+ outline.Scale *= hitObject is Droplet ? 0.5f : 1;
+ }
+ }
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
new file mode 100644
index 0000000000..96111beda4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
@@ -0,0 +1,83 @@
+// 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 osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class ScrollingPath : CompositeDrawable
+ {
+ private readonly Path drawablePath;
+
+ private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
+
+ public ScrollingPath()
+ {
+ Anchor = Anchor.BottomLeft;
+
+ InternalChildren = new Drawable[]
+ {
+ drawablePath = new SmoothPath
+ {
+ PathRadius = 2,
+ Alpha = 0.5f
+ },
+ };
+ }
+
+ public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ {
+ X = hitObject.OriginalX;
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ }
+
+ public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
+ {
+ double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+
+ computeDistanceXs(hitObject);
+ drawablePath.Vertices = vertices
+ .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
+ .ToArray();
+ drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
+ }
+
+ private void computeDistanceXs(JuiceStream hitObject)
+ {
+ vertices.Clear();
+
+ var sliderVertices = new List();
+ hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
+
+ if (sliderVertices.Count == 0)
+ return;
+
+ double distance = 0;
+ Vector2 lastPosition = Vector2.Zero;
+
+ for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
+ {
+ foreach (var position in sliderVertices)
+ {
+ distance += Vector2.Distance(lastPosition, position);
+ lastPosition = position;
+
+ vertices.Add((distance, position.X));
+ }
+
+ sliderVertices.Reverse();
+ }
+ }
+
+ // Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
index d6b8c35a09..bf7b962e0a 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -3,7 +3,10 @@
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osuTK;
@@ -17,9 +20,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private float minNestedX;
private float maxNestedX;
+ private readonly ScrollingPath scrollingPath;
+
+ private readonly NestedOutlineContainer nestedOutlineContainer;
+
+ private readonly Cached pathCache = new Cached();
+
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject)
{
+ InternalChildren = new Drawable[]
+ {
+ scrollingPath = new ScrollingPath(),
+ nestedOutlineContainer = new NestedOutlineContainer()
+ };
}
[BackgroundDependencyLoader]
@@ -29,7 +43,28 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
computeObjectBounds();
}
- private void onDefaultsApplied(HitObject _) => computeObjectBounds();
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsSelected) return;
+
+ scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
+ nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
+
+ if (pathCache.IsValid) return;
+
+ scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
+ nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
+
+ pathCache.Validate();
+ }
+
+ private void onDefaultsApplied(HitObject _)
+ {
+ computeObjectBounds();
+ pathCache.Invalidate();
+ }
private void computeObjectBounds()
{
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index d9712bc8e9..d360274aa6 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -20,6 +22,16 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LayerBelowRuleset.Add(new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
+ });
+ }
+
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
index c1a491d1ce..7eebf04ca2 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -24,23 +27,90 @@ namespace osu.Game.Rulesets.Catch.Edit
var blueprint = moveEvent.Blueprint;
Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
+
float deltaX = targetPosition.X - originalPosition.X;
+ deltaX = limitMovement(deltaX, EditorBeatmap.SelectedHitObjects);
+
+ if (deltaX == 0)
+ {
+ // Even if there is no positional change, there may be a time change.
+ return true;
+ }
EditorBeatmap.PerformOnSelection(h =>
{
if (!(h is CatchHitObject hitObject)) return;
- if (hitObject is BananaShower) return;
-
- // TODO: confine in bounds
- hitObject.OriginalXBindable.Value += deltaX;
+ hitObject.OriginalX += deltaX;
// Move the nested hit objects to give an instant result before nested objects are recreated.
foreach (var nested in hitObject.NestedHitObjects.OfType())
- nested.OriginalXBindable.Value += deltaX;
+ nested.OriginalX += deltaX;
});
return true;
}
+
+ ///
+ /// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
+ ///
+ /// The positional movement.
+ /// The objects to be moved.
+ /// The positional movement with the restriction applied.
+ private float limitMovement(float deltaX, IEnumerable movingObjects)
+ {
+ float minX = float.PositiveInfinity;
+ float maxX = float.NegativeInfinity;
+
+ foreach (float x in movingObjects.SelectMany(getOriginalPositions))
+ {
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+
+ // To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
+ // Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
+ // We only need to apply the inequality to extreme values of `x`.
+ float lowerBound = -minX;
+ float upperBound = CatchPlayfield.WIDTH - maxX;
+ // The inequality may be unsatisfiable if the objects were already out of bounds.
+ // In that case, don't move objects at all.
+ if (lowerBound > upperBound)
+ return 0;
+
+ return Math.Clamp(deltaX, lowerBound, upperBound);
+ }
+
+ ///
+ /// Enumerate X positions that should be contained in-bounds after move offset is applied.
+ ///
+ private IEnumerable getOriginalPositions(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ yield return fruit.OriginalX;
+
+ break;
+
+ case JuiceStream juiceStream:
+ foreach (var nested in juiceStream.NestedHitObjects.OfType())
+ {
+ // Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application.
+ if (!(nested is TinyDroplet))
+ yield return nested.OriginalX;
+ }
+
+ break;
+
+ case BananaShower _:
+ // A banana shower occupies the whole screen width.
+ // If the selection contains a banana shower, the selection cannot be moved horizontally.
+ yield return 0;
+ yield return CatchPlayfield.WIDTH;
+
+ break;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index bd7a1df2e4..e59a0a0431 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -12,37 +12,29 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
- [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
- [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
- protected override void ApplyLimits(bool extended)
- {
- base.ApplyLimits(extended);
-
- CircleSize.MaxValue = extended ? 11 : 10;
- ApproachRate.MaxValue = extended ? 11 : 10;
- }
-
public override string SettingDescription
{
get
@@ -61,20 +53,12 @@ namespace osu.Game.Rulesets.Catch.Mods
}
}
- protected override void TransferSettings(BeatmapDifficulty difficulty)
- {
- base.TransferSettings(difficulty);
-
- TransferSetting(CircleSize, difficulty.CircleSize);
- TransferSetting(ApproachRate, difficulty.ApproachRate);
- }
-
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
- ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
- ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
+ if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
+ if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index ae45182960..0b8c0e28a7 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -20,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The horizontal position of the hit object between 0 and .
///
+ ///
+ /// Only setter is exposed.
+ /// Use or to get the horizontal position.
+ ///
+ [JsonIgnore]
public float X
{
set => OriginalXBindable.Value = value;
@@ -34,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float XOffset
{
+ get => XOffsetBindable.Value;
set => XOffsetBindable.Value = value;
}
@@ -44,7 +51,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value specified in the beatmap, not affected by the beatmap processing.
/// Use for a gameplay.
///
- public float OriginalX => OriginalXBindable.Value;
+ public float OriginalX
+ {
+ get => OriginalXBindable.Value;
+ set => OriginalXBindable.Value = value;
+ }
///
/// The effective horizontal position of the hit object between 0 and .
@@ -53,9 +64,9 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value plus the offset applied by the beatmap processing.
/// Use if a value not affected by the offset is desired.
///
- public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
+ public float EffectiveX => OriginalX + XOffset;
- public double TimePreempt = 1000;
+ public double TimePreempt { get; set; } = 1000;
public readonly Bindable IndexInBeatmapBindable = new Bindable();
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 35fd58826e..3088d024d1 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
+ [JsonIgnore]
public double Velocity { get; private set; }
+
+ [JsonIgnore]
public double TickDistance { get; private set; }
///
@@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public float EndX => OriginalX + this.CurvePositionAt(1).X;
+ [JsonIgnore]
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index 0cd3af01df..aa7cabf38b 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The target fruit if we are to initiate a hyperdash.
///
+ [JsonIgnore]
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ea8d742b1a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public enum CatchSkinConfiguration
+ {
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ FlipCatcherPlate
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 287ed1b4c7..5e744ec001 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -103,6 +103,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable)result;
+
+ case CatchSkinConfiguration config:
+ switch (config)
+ {
+ case CatchSkinConfiguration.FlipCatcherPlate:
+ // Don't flip catcher plate contents if the catcher is provided by this legacy skin.
+ if (GetDrawableComponent(new CatchSkinComponent(CatchSkinComponents.Catcher)) != null)
+ return (IBindable)new Bindable();
+
+ break;
+ }
+
+ break;
}
return base.GetConfig(lookup);
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index dcab9459ee..57523d3505 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -56,11 +56,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
- ///
- /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
- ///
- public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
-
///
/// The amount by which caught fruit should be scaled down to fit on the plate.
///
@@ -84,8 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState
{
- get => body.AnimationState.Value;
- private set => body.AnimationState.Value = value;
+ get => Body.AnimationState.Value;
+ private set => Body.AnimationState.Value = value;
}
///
@@ -108,18 +103,22 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- public Direction VisualDirection
- {
- get => Scale.X > 0 ? Direction.Right : Direction.Left;
- set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
- }
+ ///
+ /// The currently facing direction.
+ ///
+ public Direction VisualDirection { get; set; } = Direction.Right;
+
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ private bool flipCatcherPlate;
///
/// Width of the area that can be used to attempt catches during gameplay.
///
private readonly float catchWidth;
- private readonly SkinnableCatcher body;
+ internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@@ -157,8 +156,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
+ // offset fruit vertically to better place "above" the plate.
+ Y = -5
},
- body = new SkinnableCatcher(),
+ Body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@@ -347,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+ flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
+
runHyperDashStateTransition(HyperDashing);
}
@@ -354,6 +357,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
+ var scaleFromDirection = new Vector2((int)VisualDirection, 1);
+ Body.Scale = scaleFromDirection;
+ caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
+
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
@@ -388,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
- // offset fruit vertically to better place "above" the plate.
- position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
-
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@@ -465,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break;
case DroppedObjectAnimation.Explode:
- var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
+ float originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * caughtObjectContainer.Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index 7e4a5b6a86..b59fabcb70 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI
CatcherTrail sprite = trailPool.Get();
sprite.AnimationState = catcher.CurrentState;
- sprite.Scale = catcher.Scale;
+ sprite.Scale = catcher.Scale * catcher.Body.Scale;
sprite.Position = catcher.Position;
target.Add(sprite);
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index b2a0912d19..6df555617b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs
new file mode 100644
index 0000000000..2eab5a4ce6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSlidersTest.cs
@@ -0,0 +1,145 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckTooShortSlidersTest
+ {
+ private CheckTooShortSliders check;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortSliders();
+ }
+
+ [Test]
+ public void TestLongSlider()
+ {
+ Slider slider = new Slider
+ {
+ StartTime = 0,
+ RepeatCount = 0,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(100, 0))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { slider });
+ }
+
+ [Test]
+ public void TestShortSlider()
+ {
+ Slider slider = new Slider
+ {
+ StartTime = 0,
+ RepeatCount = 0,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(25, 0))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { slider });
+ }
+
+ [Test]
+ public void TestTooShortSliderExpert()
+ {
+ Slider slider = new Slider
+ {
+ StartTime = 0,
+ RepeatCount = 0,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(10, 0))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { slider }, DifficultyRating.Expert);
+ }
+
+ [Test]
+ public void TestTooShortSlider()
+ {
+ Slider slider = new Slider
+ {
+ StartTime = 0,
+ RepeatCount = 0,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(10, 0))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertTooShort(new List { slider });
+ }
+
+ [Test]
+ public void TestTooShortSliderWithRepeats()
+ {
+ // Would be ok if we looked at the duration, but not if we look at the span duration.
+ Slider slider = new Slider
+ {
+ StartTime = 0,
+ RepeatCount = 2,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0)),
+ new PathControlPoint(new Vector2(10, 0))
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertTooShort(new List { slider });
+ }
+
+ private void assertOk(List hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
+ {
+ Assert.That(check.Run(getContext(hitObjects, difficultyRating)), Is.Empty);
+ }
+
+ private void assertTooShort(List hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
+ {
+ var issues = check.Run(getContext(hitObjects, difficultyRating)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.First().Template is CheckTooShortSliders.IssueTemplateTooShort);
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects, DifficultyRating difficultyRating)
+ {
+ var beatmap = new Beatmap { HitObjects = hitObjects };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs
new file mode 100644
index 0000000000..6a3f168ee1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs
@@ -0,0 +1,116 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckTooShortSpinnersTest
+ {
+ private CheckTooShortSpinners check;
+ private BeatmapDifficulty difficulty;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortSpinners();
+ difficulty = new BeatmapDifficulty();
+ }
+
+ [Test]
+ public void TestLongSpinner()
+ {
+ Spinner spinner = new Spinner { StartTime = 0, Duration = 4000 };
+ spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
+
+ assertOk(new List { spinner }, difficulty);
+ }
+
+ [Test]
+ public void TestShortSpinner()
+ {
+ Spinner spinner = new Spinner { StartTime = 0, Duration = 750 };
+ spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
+
+ assertOk(new List { spinner }, difficulty);
+ }
+
+ [Test]
+ public void TestVeryShortSpinner()
+ {
+ // Spinners at a certain duration only get 1000 points if approached by auto at a certain angle, making it difficult to determine.
+ Spinner spinner = new Spinner { StartTime = 0, Duration = 475 };
+ spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
+
+ assertVeryShort(new List { spinner }, difficulty);
+ }
+
+ [Test]
+ public void TestTooShortSpinner()
+ {
+ Spinner spinner = new Spinner { StartTime = 0, Duration = 400 };
+ spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
+
+ assertTooShort(new List { spinner }, difficulty);
+ }
+
+ [Test]
+ public void TestTooShortSpinnerVaryingOd()
+ {
+ const double duration = 450;
+
+ var difficultyLowOd = new BeatmapDifficulty { OverallDifficulty = 1 };
+ Spinner spinnerLowOd = new Spinner { StartTime = 0, Duration = duration };
+ spinnerLowOd.ApplyDefaults(new ControlPointInfo(), difficultyLowOd);
+
+ var difficultyHighOd = new BeatmapDifficulty { OverallDifficulty = 10 };
+ Spinner spinnerHighOd = new Spinner { StartTime = 0, Duration = duration };
+ spinnerHighOd.ApplyDefaults(new ControlPointInfo(), difficultyHighOd);
+
+ assertOk(new List { spinnerLowOd }, difficultyLowOd);
+ assertTooShort(new List { spinnerHighOd }, difficultyHighOd);
+ }
+
+ private void assertOk(List hitObjects, BeatmapDifficulty beatmapDifficulty)
+ {
+ Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
+ }
+
+ private void assertVeryShort(List hitObjects, BeatmapDifficulty beatmapDifficulty)
+ {
+ var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
+ }
+
+ private void assertTooShort(List hitObjects, BeatmapDifficulty beatmapDifficulty)
+ {
+ var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects, BeatmapDifficulty beatmapDifficulty)
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
+ };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 78bb88322a..2326a0c391 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorCentre)
- return SkinUtils.As(new BindableBool(false));
+ return SkinUtils.As(new BindableBool());
break;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index 1d500dcc14..3252e6d912 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
+ private IBeatmap beatmap;
+
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+ => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
+ private Slider slider;
private DrawableSlider drawableSlider;
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ slider = null;
+ drawableSlider = null;
+ });
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- double startTime = hitObjects[sliderIndex].StartTime;
- addSeekStep(startTime);
- retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
+ retrieveSlider(sliderIndex);
setSnaking(true);
- ensureSnakingIn(startTime + fade_in_modifier);
+ addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i < sliderIndex; i++)
{
// non-final repeats should not snake out
- ensureNoSnakingOut(startTime, i);
+ addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
// final repeat should snake out
- ensureSnakingOut(startTime, sliderIndex);
+ addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex);
}
[TestCase(0)]
@@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- double startTime = hitObjects[sliderIndex].StartTime;
- addSeekStep(startTime);
- retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
+ retrieveSlider(sliderIndex);
setSnaking(false);
- ensureNoSnakingIn(startTime + fade_in_modifier);
+ addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i <= sliderIndex; i++)
{
// no snaking out ever, including final repeat
- ensureNoSnakingOut(startTime, i);
+ addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
}
@@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
- checkPositionChange(16600, sliderRepeat, positionAlmostSame);
+ addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
@@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests
setSnaking(true);
base.SetUpSteps();
- checkPositionChange(16600, sliderRepeat, positionDecreased);
+ addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
- private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
- (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
-
- private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
- private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
-
- private void ensureSnakingOut(double startTime, int repeatIndex)
+ private void retrieveSlider(int index)
{
- var repeatTime = timeAtRepeat(startTime, repeatIndex);
+ AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
+ addSeekStep(() => slider);
+ AddUntilStep("retrieve drawable slider", () =>
+ (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
+ }
+ private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
+ private void addEnsureNoSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame);
+
+ private void addEnsureSnakingOutSteps(Func startTime, int repeatIndex)
+ {
if (repeatIndex % 2 == 0)
- checkPositionChange(repeatTime, sliderStart, positionIncreased);
+ addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased);
else
- checkPositionChange(repeatTime, sliderEnd, positionDecreased);
+ addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased);
}
- private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
- checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
+ private void addEnsureNoSnakingOutStep(Func startTime, int repeatIndex)
+ => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
- private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
- private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd;
+ private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
+ private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)getSliderStart : getSliderEnd;
- private List sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
- private Vector2 sliderStart() => sliderCurve.First();
- private Vector2 sliderEnd() => sliderCurve.Last();
+ private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
+ private Vector2 getSliderStart() => getSliderCurve().First();
+ private Vector2 getSliderEnd() => getSliderCurve().Last();
- private Vector2 sliderRepeat()
+ private Vector2 getSliderRepeat()
{
- var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
+ var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]);
var repeat = drawable.ChildrenOfType>().First().Children.First();
return repeat.Position;
}
@@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
- private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion)
+ private void addCheckPositionChangeSteps(Func startTime, Func positionToCheck, Func positionAssertion)
{
Vector2 previousPosition = Vector2.Zero;
@@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(startTime);
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
- addSeekStep(startTime + 100);
+ addSeekStep(() => startTime() + 100);
AddAssert($"{positionDescription} {assertionDescription}", () =>
{
var currentPosition = positionToCheck.Invoke();
@@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
- private void addSeekStep(double time)
+ private void addSeekStep(Func slider)
{
- AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
-
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
- protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ private void addSeekStep(Func time)
{
- HitObjects = hitObjects
- };
+ AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ }
- private readonly List hitObjects = new List
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
+
+ private static List createHitObjects() => new List
{
new Slider
{
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 1efd19f49d..68be34d153 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs
new file mode 100644
index 0000000000..159498c479
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs
@@ -0,0 +1,48 @@
+// 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 osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Checks
+{
+ public class CheckTooShortSliders : ICheck
+ {
+ ///
+ /// The shortest acceptable duration between the head and tail of the slider (so ignoring repeats).
+ ///
+ private const double span_duration_threshold = 125; // 240 BPM 1/2
+
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short sliders");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateTooShort(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ if (context.InterpretedDifficulty > DifficultyRating.Easy)
+ yield break;
+
+ foreach (var hitObject in context.Beatmap.HitObjects)
+ {
+ if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold)
+ yield return new IssueTemplateTooShort(this).Create(slider);
+ }
+ }
+
+ public class IssueTemplateTooShort : IssueTemplate
+ {
+ public IssueTemplateTooShort(ICheck check)
+ : base(check, IssueType.Problem, "This slider is too short ({0:0} ms), expected at least {1:0} ms.")
+ {
+ }
+
+ public Issue Create(Slider slider) => new Issue(slider, this, slider.SpanDuration, span_duration_threshold);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
new file mode 100644
index 0000000000..0d0c3d9e69
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.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 osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Checks
+{
+ public class CheckTooShortSpinners : ICheck
+ {
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short spinners");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateTooShort(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
+
+ // These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
+ // It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
+ double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok.
+ double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok.
+
+ foreach (var hitObject in context.Beatmap.HitObjects)
+ {
+ if (!(hitObject is Spinner spinner))
+ continue;
+
+ if (spinner.Duration < problemThreshold)
+ yield return new IssueTemplateTooShort(this).Create(spinner);
+ else if (spinner.Duration < warningThreshold)
+ yield return new IssueTemplateVeryShort(this).Create(spinner);
+ }
+ }
+
+ public class IssueTemplateTooShort : IssueTemplate
+ {
+ public IssueTemplateTooShort(ICheck check)
+ : base(check, IssueType.Problem, "This spinner is too short. Auto cannot achieve 1000 points on this.")
+ {
+ }
+
+ public Issue Create(Spinner spinner) => new Issue(spinner, this);
+ }
+
+ public class IssueTemplateVeryShort : IssueTemplate
+ {
+ public IssueTemplateVeryShort(ICheck check)
+ : base(check, IssueType.Warning, "This spinner may be too short. Ensure auto can achieve 1000 points on this.")
+ {
+ }
+
+ public Issue Create(Spinner spinner) => new Issue(spinner, this);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
index 896e904f3f..221723e4cd 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
@@ -15,10 +15,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
// Compose
new CheckOffscreenObjects(),
+ new CheckTooShortSpinners(),
// Spread
new CheckTimeDistanceEquality(),
- new CheckLowDiffOverlaps()
+ new CheckLowDiffOverlaps(),
+ new CheckTooShortSliders(),
};
public IEnumerable Run(BeatmapVerifierContext context)
diff --git a/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs
new file mode 100644
index 0000000000..4a3b187e83
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Marker interface for any mod which completely hides the approach circles.
+ /// Used for incompatibility with .
+ ///
+ ///
+ /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
+ ///
+ public interface IHidesApproachCircles
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
deleted file mode 100644
index 60a5825241..0000000000
--- a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Osu.Mods
-{
- ///
- /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
- ///
- public interface IMutateApproachCircles
- {
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs
new file mode 100644
index 0000000000..1458abfe05
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Marker interface for any mod which requires the approach circles to be visible.
+ /// Used for incompatibility with .
+ ///
+ ///
+ /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
+ ///
+ public interface IRequiresApproachCircles
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
index 526e29ad53..d832411104 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
+ public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IRequiresApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index ebf6f9dda7..636cd63c69 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -158,17 +158,17 @@ namespace osu.Game.Rulesets.Osu.Mods
var firstObj = beatmap.HitObjects[0];
var startDelay = firstObj.StartTime - firstObj.TimePreempt;
- using (BeginAbsoluteSequence(startDelay + break_close_late, true))
+ using (BeginAbsoluteSequence(startDelay + break_close_late))
leaveBreak();
foreach (var breakInfo in beatmap.Breaks)
{
if (breakInfo.HasEffect)
{
- using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early, true))
+ using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early))
{
enterBreak();
- using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late, true))
+ using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late))
leaveBreak();
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index 1cb25edecf..3a6b232f9f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -11,34 +10,26 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
- [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
- [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
- protected override void ApplyLimits(bool extended)
- {
- base.ApplyLimits(extended);
-
- CircleSize.MaxValue = extended ? 11 : 10;
- ApproachRate.MaxValue = extended ? 11 : 10;
- }
-
public override string SettingDescription
{
get
@@ -55,20 +46,12 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
- protected override void TransferSettings(BeatmapDifficulty difficulty)
- {
- base.TransferSettings(difficulty);
-
- TransferSetting(CircleSize, difficulty.CircleSize);
- TransferSetting(ApproachRate, difficulty.ApproachRate);
- }
-
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
- ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
- ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
+ if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
+ if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 16b38cd0b1..9c7784a00a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -15,12 +15,12 @@ using osu.Game.Rulesets.Osu.Skinning;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModHidden : ModHidden, IMutateApproachCircles
+ public class OsuModHidden : ModHidden, IHidesApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 6dfabed0df..778447e444 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
///
/// Adjusts the size of hit objects during their fade in animation.
///
- public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override ModType Type => ModType.Fun;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
index d3ca2973f0..95e7d13ee7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
- // todo: this mod should be able to be compatible with hidden with a bit of further implementation.
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
+ // further implementation will be required for supporting that.
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
@@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
{
circle.ApproachCircle.Hide();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 84263221a7..07ce009cf8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
{
var h = hitObject.HitObject;
- using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
(hitCircle ?? hitObject).Hide();
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index b5905d7015..8122ab563e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
- using (drawable.BeginAbsoluteSequence(appearTime, true))
+ using (drawable.BeginAbsoluteSequence(appearTime))
{
drawable
.MoveToOffset(appearOffset)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index a01cec4bb3..ff6ba6e121 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
- using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
+ using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration))
wiggle();
}
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
- using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
+ using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration))
wiggle();
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 7b0cf651c8..b88bf9108b 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -233,35 +233,43 @@ namespace osu.Game.Rulesets.Osu.Replays
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
+ bool hasWaited = false;
if (waitTime > lastFrame.Time)
{
lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions };
+ hasWaited = true;
AddFrameToReplay(lastFrame);
}
- Vector2 lastPosition = lastFrame.Position;
-
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
+ OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
- // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
- if (timeDifference > 0 && // Sanity checks
- ((lastPosition - targetPos).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
- timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
+ if (timeDifference > 0)
{
- // Perform eased movement
+ // If the last frame is a key-up frame and there has been no wait period, adjust the last frame's position such that it begins eased movement instantaneously.
+ if (lastLastFrame != null && lastFrame is OsuKeyUpReplayFrame && !hasWaited)
+ {
+ // [lastLastFrame] ... [lastFrame] ... [current frame]
+ // We want to find the cursor position at lastFrame, so interpolate between lastLastFrame and the new target position.
+ lastFrame.Position = Interpolation.ValueAt(lastFrame.Time, lastFrame.Position, targetPos, lastLastFrame.Time, h.StartTime, easing);
+ }
+
+ Vector2 lastPosition = lastFrame.Position;
+
+ // Perform the rest of the eased movement until the target position is reached.
for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
}
+ }
- buttonIndex = 0;
- }
- else
- {
+ // Start alternating once the time separation is too small (faster than ~225BPM).
+ if (timeDifference > 0 && timeDifference < 266)
buttonIndex++;
- }
+ else
+ buttonIndex = 0;
}
///
@@ -284,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = h.GetEndTime() + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
- var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
+ var endFrame = new OsuKeyUpReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
// Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1;
@@ -381,5 +389,13 @@ namespace osu.Game.Rulesets.Osu.Replays
}
#endregion
+
+ private class OsuKeyUpReplayFrame : OsuReplayFrame
+ {
+ public OsuKeyUpReplayFrame(double time, Vector2 position)
+ : base(time, position)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
index 542f3eff0d..4ea0831627 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
@@ -130,18 +130,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Spinner spinner = drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
- using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
@@ -157,17 +157,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
@@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
index 8feeca56e8..8943a91076 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
@@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
+ // ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
base.ApplyTransformsAt(time, false);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index ae8d6a61f8..1e170036e4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -100,17 +100,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case DrawableSpinner d:
Spinner spinner = d.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
fixedMiddle.FadeColour(Color4.White);
- using (BeginDelayedSequence(spinner.TimePreempt, true))
+ using (BeginDelayedSequence(spinner.TimePreempt))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index cbe721d21d..e3e8f3ce88 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -89,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Spinner spinner = d.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 317649785e..93aba608e6 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
///
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
- /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
+ /// This offset is negated to bring all constants into window-space.
/// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
///
protected const float SPINNER_TOP_OFFSET = 45f - 16f;
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400);
- using (BeginAbsoluteSequence(startTime, true))
+ using (BeginAbsoluteSequence(startTime))
{
clear.FadeInFromZero(400, Easing.Out);
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
const double fade_out_duration = 50;
- using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true))
+ using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration))
clear.FadeOut(fade_out_duration);
}
else
@@ -182,14 +182,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength))
spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
if (state == ArmedState.Hit)
{
- using (BeginAbsoluteSequence(d.HitStateUpdateTime, true))
+ using (BeginAbsoluteSequence(d.HitStateUpdateTime))
spin.FadeOut(300);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 8fb167ba10..532fdc5cb0 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
index 4006652bd5..9540e35780 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -11,14 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDifficultyAdjust : ModDifficultyAdjust
{
- [SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
- public BindableNumber ScrollSpeed { get; } = new BindableFloat
+ [SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable ScrollSpeed { get; } = new DifficultyBindable
{
Precision = 0.05f,
MinValue = 0.25f,
MaxValue = 4,
- Default = 1,
- Value = 1,
+ ReadCurrentFromDifficulty = _ => 1,
};
public override string SettingDescription
@@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
base.ApplySettings(difficulty);
- ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
+ if (ScrollSpeed.Value != null) difficulty.SliderMultiplier *= ScrollSpeed.Value.Value;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 60f9521996..888f47d341 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
- using (BeginDelayedSequence(-ring_appear_offset, true))
+ using (BeginDelayedSequence(-ring_appear_offset))
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}
diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
index 6c8133660f..9fba0f1668 100644
--- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
+++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestSingleSpan()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestRepeat()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestNonEvenTicks()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestLegacyLastTickOffset()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900));
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5;
const double min_distance = velocity * 10;
- var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
Assert.Multiple(() =>
{
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index cac331451b..642ecf00b8 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test]
public void TestDefaultsPopulationAndQuery()
{
- Assert.That(query().Count, Is.EqualTo(0));
+ Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
- Assert.That(query().Count, Is.EqualTo(3));
+ Assert.That(queryCount(), Is.EqualTo(3));
- Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
- Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
+ Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
+ Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
- private IQueryable query() => realmContextFactory.Context.All();
+ private int queryCount(GlobalAction? match = null)
+ {
+ using (var usage = realmContextFactory.GetForRead())
+ {
+ var results = usage.Realm.All();
+ if (match.HasValue)
+ results = results.Where(k => k.ActionInt == (int)match.Value);
+ return results.Count();
+ }
+ }
[Test]
public void TestUpdateViaQueriedReference()
@@ -59,25 +68,28 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer);
- var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
-
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
-
- var tsr = ThreadSafeReference.Create(backBinding);
-
- using (var usage = realmContextFactory.GetForWrite())
+ using (var primaryUsage = realmContextFactory.GetForRead())
{
- var binding = usage.Realm.ResolveReference(tsr);
- binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
+ var backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back);
- usage.Commit();
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
+
+ var tsr = ThreadSafeReference.Create(backBinding);
+
+ using (var usage = realmContextFactory.GetForWrite())
+ {
+ var binding = usage.Realm.ResolveReference(tsr);
+ binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
+
+ usage.Commit();
+ }
+
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
+
+ // check still correct after re-query.
+ backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back);
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
-
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
-
- // check still correct after re-query.
- backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
[TearDown]
diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs
new file mode 100644
index 0000000000..93b20cd166
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs
@@ -0,0 +1,94 @@
+// 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 Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckZeroLengthObjectsTest
+ {
+ private CheckZeroLengthObjects check;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckZeroLengthObjects();
+ }
+
+ [Test]
+ public void TestCircle()
+ {
+ assertOk(new List
+ {
+ new HitCircle { StartTime = 1000, Position = new Vector2(0, 0) }
+ });
+ }
+
+ [Test]
+ public void TestRegularSlider()
+ {
+ assertOk(new List
+ {
+ getSliderMock(1000).Object
+ });
+ }
+
+ [Test]
+ public void TestZeroLengthSlider()
+ {
+ assertZeroLength(new List
+ {
+ getSliderMock(0).Object
+ });
+ }
+
+ [Test]
+ public void TestNegativeLengthSlider()
+ {
+ assertZeroLength(new List
+ {
+ getSliderMock(-1000).Object
+ });
+ }
+
+ private Mock getSliderMock(double duration)
+ {
+ var mockSlider = new Mock();
+ mockSlider.As().Setup(d => d.Duration).Returns(duration);
+
+ return mockSlider;
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertZeroLength(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.First().Template is CheckZeroLengthObjects.IssueTemplateZeroLength);
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap { HitObjects = hitObjects };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs
new file mode 100644
index 0000000000..dab4825919
--- /dev/null
+++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.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 NUnit.Framework;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Tests.Localisation
+{
+ [TestFixture]
+ public class BeatmapMetadataRomanisationTest
+ {
+ [Test]
+ public void TestRomanisation()
+ {
+ var metadata = new BeatmapMetadata
+ {
+ Artist = "Romanised Artist",
+ ArtistUnicode = "Unicode Artist",
+ Title = "Romanised title",
+ TitleUnicode = "Unicode Title"
+ };
+ var romanisableString = metadata.ToRomanisableString();
+
+ Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
+ Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
+ }
+
+ [Test]
+ public void TestRomanisationNoUnicode()
+ {
+ var metadata = new BeatmapMetadata
+ {
+ Artist = "Romanised Artist",
+ Title = "Romanised title"
+ };
+ var romanisableString = metadata.ToRomanisableString();
+
+ Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
new file mode 100644
index 0000000000..84cf796835
--- /dev/null
+++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.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.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Tests.Mods
+{
+ [TestFixture]
+ public class ModDifficultyAdjustTest
+ {
+ private TestModDifficultyAdjust testMod;
+
+ [SetUp]
+ public void Setup()
+ {
+ testMod = new TestModDifficultyAdjust();
+ }
+
+ [Test]
+ public void TestUnchangedSettingsFollowAppliedDifficulty()
+ {
+ var result = applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 10,
+ OverallDifficulty = 10
+ });
+
+ Assert.That(result.DrainRate, Is.EqualTo(10));
+ Assert.That(result.OverallDifficulty, Is.EqualTo(10));
+
+ result = applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 1,
+ OverallDifficulty = 1
+ });
+
+ Assert.That(result.DrainRate, Is.EqualTo(1));
+ Assert.That(result.OverallDifficulty, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestChangedSettingsOverrideAppliedDifficulty()
+ {
+ testMod.OverallDifficulty.Value = 4;
+
+ var result = applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 10,
+ OverallDifficulty = 10
+ });
+
+ Assert.That(result.DrainRate, Is.EqualTo(10));
+ Assert.That(result.OverallDifficulty, Is.EqualTo(4));
+
+ result = applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 1,
+ OverallDifficulty = 1
+ });
+
+ Assert.That(result.DrainRate, Is.EqualTo(1));
+ Assert.That(result.OverallDifficulty, Is.EqualTo(4));
+ }
+
+ [Test]
+ public void TestChangedSettingsRetainedWhenSameValueIsApplied()
+ {
+ testMod.OverallDifficulty.Value = 4;
+
+ // Apply and de-apply the same value as the mod.
+ applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
+ var result = applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 10 });
+
+ Assert.That(result.OverallDifficulty, Is.EqualTo(4));
+ }
+
+ [Test]
+ public void TestChangedSettingSerialisedWhenSameValueIsApplied()
+ {
+ applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
+ testMod.OverallDifficulty.Value = 4;
+
+ var result = (TestModDifficultyAdjust)new APIMod(testMod).ToMod(new TestRuleset());
+
+ Assert.That(result.OverallDifficulty.Value, Is.EqualTo(4));
+ }
+
+ [Test]
+ public void TestChangedSettingsRevertedToDefault()
+ {
+ applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 10,
+ OverallDifficulty = 10
+ });
+
+ testMod.OverallDifficulty.Value = 4;
+ testMod.ResetSettingsToDefaults();
+
+ Assert.That(testMod.DrainRate.Value, Is.Null);
+ Assert.That(testMod.OverallDifficulty.Value, Is.Null);
+
+ var applied = applyDifficulty(new BeatmapDifficulty
+ {
+ DrainRate = 10,
+ OverallDifficulty = 10
+ });
+
+ Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
+ }
+
+ ///
+ /// Applies a to the mod and returns a new
+ /// representing the result if the mod were applied to a fresh instance.
+ ///
+ private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
+ {
+ // ensure that ReadFromDifficulty doesn't pollute the values.
+ var newDifficulty = difficulty.Clone();
+
+ testMod.ReadFromDifficulty(difficulty);
+
+ testMod.ApplyToDifficulty(newDifficulty);
+ return newDifficulty;
+ }
+
+ private class TestModDifficultyAdjust : ModDifficultyAdjust
+ {
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override IEnumerable GetModsFor(ModType type)
+ {
+ if (type == ModType.DifficultyIncrease)
+ yield return new TestModDifficultyAdjust();
+ }
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override string Description => string.Empty;
+ public override string ShortName => string.Empty;
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index a540ad7247..4c44e2ec72 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -184,6 +184,9 @@ namespace osu.Game.Tests.NonVisual
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
+ // some files may have been left behind for whatever reason, but that's not what we're testing here.
+ customPath = prepareCustomPath();
+
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
}
diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
new file mode 100644
index 0000000000..97105b6b6a
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
@@ -0,0 +1,123 @@
+// 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.CodeAnalysis;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+
+namespace osu.Game.Tests.NonVisual
+{
+ public class FirstAvailableHitWindowsTest
+ {
+ private TestDrawableRuleset testDrawableRuleset;
+
+ [SetUp]
+ public void Setup()
+ {
+ testDrawableRuleset = new TestDrawableRuleset();
+ }
+
+ [Test]
+ public void TestResultIfOnlyParentHitWindowIsEmpty()
+ {
+ var testObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(new HitWindows());
+ testObject.AddNested(nested);
+ testDrawableRuleset.HitObjects = new List { testObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, nested.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfParentHitWindowsIsNotEmpty()
+ {
+ var testObject = new TestHitObject(new HitWindows());
+ HitObject nested = new TestHitObject(new HitWindows());
+ testObject.AddNested(nested);
+ testDrawableRuleset.HitObjects = new List { testObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, testObject.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfParentAndChildHitWindowsAreEmpty()
+ {
+ var firstObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(HitWindows.Empty);
+ firstObject.AddNested(nested);
+
+ var secondObject = new TestHitObject(new HitWindows());
+ testDrawableRuleset.HitObjects = new List { firstObject, secondObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfAllHitWindowsAreEmpty()
+ {
+ var firstObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(HitWindows.Empty);
+ firstObject.AddNested(nested);
+
+ testDrawableRuleset.HitObjects = new List { firstObject };
+
+ Assert.IsNull(testDrawableRuleset.FirstAvailableHitWindows);
+ }
+
+ [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
+ private class TestDrawableRuleset : DrawableRuleset
+ {
+ public List HitObjects;
+ public override IEnumerable Objects => HitObjects;
+
+ public override event Action NewResult;
+ public override event Action RevertResult;
+
+ public override Playfield Playfield { get; }
+ public override Container Overlays { get; }
+ public override Container FrameStableComponents { get; }
+ public override IFrameStableClock FrameStableClock { get; }
+ internal override bool FrameStablePlayback { get; set; }
+ public override IReadOnlyList Mods { get; }
+
+ public override double GameplayStartTime { get; }
+ public override GameplayCursorContainer Cursor { get; }
+
+ public TestDrawableRuleset()
+ : base(new OsuRuleset())
+ {
+ // won't compile without this.
+ NewResult?.Invoke(null);
+ RevertResult?.Invoke(null);
+ }
+
+ public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
+
+ public override void SetRecordTarget(Score score) => throw new NotImplementedException();
+
+ public override void RequestResume(Action continueResume) => throw new NotImplementedException();
+
+ public override void CancelResume() => throw new NotImplementedException();
+ }
+
+ public class TestHitObject : HitObject
+ {
+ public TestHitObject(HitWindows hitWindows)
+ {
+ HitWindows = hitWindows;
+ HitWindows.SetDifficulty(0.5f);
+ }
+
+ public new void AddNested(HitObject nested) => base.AddNested(nested);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index adc1d6aede..0983b806e2 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
@@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("create room initially in gameplay", () =>
{
- Room.RoomID.Value = null;
+ var newRoom = new Room();
+ newRoom.CopyFrom(SelectedRoom.Value);
+
+ newRoom.RoomID.Value = null;
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
@@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
});
};
- RoomManager.CreateRoom(Room);
+ RoomManager.CreateRoom(newRoom);
});
AddUntilStep("wait for room join", () => Client.Room != null);
diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
index d4e591cf09..6851df3832 100644
--- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
+++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
@@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay
}
[Test]
- public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
+ public void TestPlayerClocksStartWhenAllHaveFrames()
{
setWaiting(() => player1, false);
- assertMasterState(false);
assertPlayerClockState(() => player1, false);
assertPlayerClockState(() => player2, false);
setWaiting(() => player2, false);
- assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, true);
}
[Test]
- public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
- {
- AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
- assertMasterState(false);
- }
-
- [Test]
- public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
+ public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
- assertMasterState(true);
+ assertPlayerClockState(() => player1, true);
+ assertPlayerClockState(() => player2, false);
}
[Test]
@@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay
private void setPlayerClockTime(Func playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
- private void assertMasterState(bool running)
- => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
-
private void assertCatchingUp(Func playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
@@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay
private class TestManualClock : ManualClock, IAdjustableClock
{
+ public TestManualClock()
+ {
+ IsRunning = true;
+ }
+
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
new file mode 100644
index 0000000000..c70ad751be
--- /dev/null
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
@@ -0,0 +1,11 @@
+#include "sh_Utils.h"
+
+varying mediump vec2 v_TexCoord;
+varying mediump vec4 v_TexRect;
+
+void main(void)
+{
+ float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
+ gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
+}
+
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
new file mode 100644
index 0000000000..4485356fa4
--- /dev/null
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
@@ -0,0 +1,31 @@
+#include "sh_Utils.h"
+
+attribute highp vec2 m_Position;
+attribute lowp vec4 m_Colour;
+attribute mediump vec2 m_TexCoord;
+attribute mediump vec4 m_TexRect;
+attribute mediump vec2 m_BlendRange;
+
+varying highp vec2 v_MaskingPosition;
+varying lowp vec4 v_Colour;
+varying mediump vec2 v_TexCoord;
+varying mediump vec4 v_TexRect;
+varying mediump vec2 v_BlendRange;
+
+uniform highp mat4 g_ProjMatrix;
+uniform highp mat3 g_ToMaskingSpace;
+
+void main(void)
+{
+ // Transform from screen space to masking space.
+ highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
+ v_MaskingPosition = maskingPos.xy / maskingPos.z;
+
+ v_Colour = m_Colour;
+ v_TexCoord = m_TexCoord;
+ v_TexRect = m_TexRect;
+ v_BlendRange = m_BlendRange;
+
+ gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
+}
+
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index f421a30283..c357fccd27 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -13,8 +13,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
@@ -31,12 +33,14 @@ namespace osu.Game.Tests.Rulesets
DrawableWithDependencies drawable = null;
TestTextureStore textureStore = null;
TestSampleStore sampleStore = null;
+ TestShaderManager shaderManager = null;
AddStep("add dependencies", () =>
{
Child = drawable = new DrawableWithDependencies();
textureStore = drawable.ParentTextureStore;
sampleStore = drawable.ParentSampleStore;
+ shaderManager = drawable.ParentShaderManager;
});
AddStep("clear children", Clear);
@@ -52,12 +56,14 @@ namespace osu.Game.Tests.Rulesets
AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed);
AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed);
+ AddAssert("parent shader manager not disposed", () => !shaderManager.IsDisposed);
}
private class DrawableWithDependencies : CompositeDrawable
{
public TestTextureStore ParentTextureStore { get; private set; }
public TestSampleStore ParentSampleStore { get; private set; }
+ public TestShaderManager ParentShaderManager { get; private set; }
public DrawableWithDependencies()
{
@@ -70,6 +76,7 @@ namespace osu.Game.Tests.Rulesets
dependencies.CacheAs(ParentTextureStore = new TestTextureStore());
dependencies.CacheAs(ParentSampleStore = new TestSampleStore());
+ dependencies.CacheAs(ParentShaderManager = new TestShaderManager());
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
}
@@ -135,5 +142,23 @@ namespace osu.Game.Tests.Rulesets
public int PlaybackConcurrency { get; set; }
}
+
+ private class TestShaderManager : ShaderManager
+ {
+ public TestShaderManager()
+ : base(new ResourceStore())
+ {
+ }
+
+ public override byte[] LoadRaw(string name) => null;
+
+ public bool IsDisposed { get; private set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ IsDisposed = true;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
index 25619de323..28ad7ed6a7 100644
--- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
@@ -2,14 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Skinning;
@@ -18,14 +19,21 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Rulesets
{
+ [HeadlessTest]
public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
{
private SkinRequester requester;
protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
- [Cached(typeof(ISkinSource))]
- private readonly ISkinSource testSource = new TestSkinProvider();
+ [Test]
+ public void TestRulesetResources()
+ {
+ setupProviderStep();
+
+ AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null);
+ AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null);
+ }
[Test]
public void TestEarlyAddedSkinRequester()
@@ -38,7 +46,7 @@ namespace osu.Game.Tests.Rulesets
rulesetSkinProvider.Add(requester = new SkinRequester());
- requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME);
+ requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
Child = rulesetSkinProvider;
});
@@ -46,6 +54,15 @@ namespace osu.Game.Tests.Rulesets
AddAssert("requester got correct initial texture", () => textureOnLoad != null);
}
+ private void setupProviderStep()
+ {
+ AddStep("setup provider", () =>
+ {
+ Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
+ .WithChild(requester = new SkinRequester());
+ });
+ }
+
private class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
@@ -68,28 +85,5 @@ namespace osu.Game.Tests.Rulesets
public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup);
}
-
- private class TestSkinProvider : ISkinSource
- {
- public const string TEXTURE_NAME = "some-texture";
-
- public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
-
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null;
-
- public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
-
- public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
-
- public event Action SourceChanged
- {
- add { }
- remove { }
- }
-
- public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
-
- public IEnumerable AllSources => new[] { this };
- }
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
new file mode 100644
index 0000000000..ab47067411
--- /dev/null
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -0,0 +1,94 @@
+// 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.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Skins
+{
+ [HeadlessTest]
+ public class TestSceneSkinProvidingContainer : OsuTestScene
+ {
+ ///
+ /// Ensures that the first inserted skin after resetting (via source change)
+ /// is always prioritised over others when providing the same resource.
+ ///
+ [Test]
+ public void TestPriorityPreservation()
+ {
+ TestSkinProvidingContainer provider = null;
+ TestSkin mostPrioritisedSource = null;
+
+ AddStep("setup sources", () =>
+ {
+ var sources = new List();
+ for (int i = 0; i < 10; i++)
+ sources.Add(new TestSkin());
+
+ mostPrioritisedSource = sources.First();
+
+ Child = provider = new TestSkinProvidingContainer(sources);
+ });
+
+ AddAssert("texture provided by expected skin", () =>
+ {
+ return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
+ });
+
+ AddStep("trigger source change", () => provider.TriggerSourceChanged());
+
+ AddAssert("texture still provided by expected skin", () =>
+ {
+ return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
+ });
+ }
+
+ private class TestSkinProvidingContainer : SkinProvidingContainer
+ {
+ private readonly IEnumerable sources;
+
+ public TestSkinProvidingContainer(IEnumerable sources)
+ {
+ this.sources = sources;
+ }
+
+ public new void TriggerSourceChanged() => base.TriggerSourceChanged();
+
+ protected override void OnSourceChanged()
+ {
+ ResetSources();
+ sources.ForEach(AddSource);
+ }
+ }
+
+ private class TestSkin : ISkin
+ {
+ public const string TEXTURE_NAME = "virtual-texture";
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ {
+ if (componentName == TEXTURE_NAME)
+ return Texture.WhitePixel;
+
+ return null;
+ }
+
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new System.NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup) => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index fb50da32f3..8c6932e792 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking;
+using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
@@ -45,6 +46,14 @@ namespace osu.Game.Tests.Testing
Dependencies.Get().Get(@"test-sample") != null);
}
+ [Test]
+ public void TestRetrieveShader()
+ {
+ AddAssert("ruleset shaders retrieved", () =>
+ Dependencies.Get().LoadRaw(@"sh_TestVertex.vs") != null &&
+ Dependencies.Get().LoadRaw(@"sh_TestFragment.fs") != null);
+ }
+
[Test]
public void TestResolveConfigManager()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index bc7cf8eee2..fdc3916c47 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -11,7 +11,6 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
-using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -36,18 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
- double? time = null;
-
- AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
-
- // test seek via keyboard
- AddStep("seek with right arrow key", () => InputManager.Key(Key.Right));
- AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000);
-
- AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
- AddStep("seek with left arrow key", () => InputManager.Key(Key.Left));
- AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time);
-
seekToBreak(0);
seekToBreak(1);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
index 4ee48fd853..11bd701e19 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -114,11 +114,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public bool ResultsCreated { get; private set; }
- public FakeRankingPushPlayer()
- : base(true, true)
- {
- }
-
protected override ResultsScreen CreateResults(ScoreInfo score)
{
var results = base.CreateResults(score);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
index d69ac665cc..ed40a83831 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddStep("Up arrow", () => InputManager.Key(Key.Up));
- AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value);
+ AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().State == SelectionState.Selected);
}
///
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddStep("Down arrow", () => InputManager.Key(Key.Down));
- AddAssert("First button selected", () => getButton(0).Selected.Value);
+ AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
///
@@ -111,11 +111,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Up arrow", () => InputManager.Key(Key.Up));
- AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
+ AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up));
- AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
+ AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up));
- AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
+ AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
}
///
@@ -127,11 +127,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Down arrow", () => InputManager.Key(Key.Down));
- AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
+ AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down));
- AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
+ AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down));
- AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
+ AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
}
///
@@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
AddStep("Hide overlay", () => failOverlay.Hide());
- AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value));
+ AddAssert("Overlay state is reset", () => failOverlay.Buttons.All(b => b.State == SelectionState.NotSelected));
}
///
@@ -162,11 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hide overlay", () => pauseOverlay.Hide());
showOverlay();
- AddAssert("First button not selected", () => !getButton(0).Selected.Value);
+ AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1)));
- AddAssert("First button selected", () => getButton(0).Selected.Value);
+ AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
///
@@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
- AddAssert("First button not selected", () => !getButton(0).Selected.Value);
- AddAssert("Second button selected", () => getButton(1).Selected.Value);
+ AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
+ AddAssert("Second button selected", () => getButton(1).State == SelectionState.Selected);
}
///
@@ -196,8 +196,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Up arrow", () => InputManager.Key(Key.Up));
- AddAssert("Second button not selected", () => !getButton(1).Selected.Value);
- AddAssert("First button selected", () => getButton(0).Selected.Value);
+ AddAssert("Second button not selected", () => getButton(1).State == SelectionState.NotSelected);
+ AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
///
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
AddStep("Down arrow", () => InputManager.Key(Key.Down));
- AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition
+ AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected); // Initial state condition
}
///
@@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddAssert("No button selected",
- () => pauseOverlay.Buttons.All(button => !button.Selected.Value));
+ () => pauseOverlay.Buttons.All(button => button.State == SelectionState.NotSelected));
}
private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
new file mode 100644
index 0000000000..5ff2e9c439
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -0,0 +1,246 @@
+// 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.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Online.Solo;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestScenePlayerScoreSubmission : PlayerTestScene
+ {
+ protected override bool AllowFail => allowFail;
+
+ private bool allowFail;
+
+ private Func createCustomBeatmap;
+ private Func createCustomRuleset;
+
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ protected override bool HasCustomSteps => true;
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+
+ protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => createCustomBeatmap?.Invoke(ruleset) ?? createTestBeatmap(ruleset);
+
+ private IBeatmap createTestBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = (TestBeatmap)base.CreateBeatmap(ruleset);
+
+ beatmap.HitObjects = beatmap.HitObjects.Take(10).ToList();
+
+ return beatmap;
+ }
+
+ [Test]
+ public void TestNoSubmissionOnResultsWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ createPlayerTest();
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnResults()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest();
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnExitWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ createPlayerTest();
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnEmptyFail()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(true);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnFail()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(true);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnEmptyExit()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest();
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnExit()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest();
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnLocalBeatmap()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(false, r =>
+ {
+ var beatmap = createTestBeatmap(r);
+ beatmap.BeatmapInfo.OnlineBeatmapID = null;
+ return beatmap;
+ });
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnCustomRuleset()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } });
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ private void createPlayerTest(bool allowFail = false, Func createBeatmap = null, Func createRuleset = null)
+ {
+ CreateTest(() => AddStep("set up requirements", () =>
+ {
+ this.allowFail = allowFail;
+ createCustomBeatmap = createBeatmap;
+ createCustomRuleset = createRuleset;
+ }));
+ }
+
+ private void prepareTokenResponse(bool validToken)
+ {
+ AddStep("Prepare test API", () =>
+ {
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case CreateSoloScoreRequest tokenRequest:
+ if (validToken)
+ tokenRequest.TriggerSuccess(new APIScoreToken { ID = 1234 });
+ else
+ tokenRequest.TriggerFailure(new APIException("something went wrong!", null));
+ return true;
+ }
+
+ return false;
+ };
+ });
+ }
+
+ private void addFakeHit()
+ {
+ AddUntilStep("wait for first result", () => Player.Results.Count > 0);
+
+ AddStep("force successfuly hit", () =>
+ {
+ Player.ScoreProcessor.RevertResult(Player.Results.First());
+ Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
+ {
+ Type = HitResult.Great,
+ });
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
new file mode 100644
index 0000000000..fcd65eaff3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
@@ -0,0 +1,87 @@
+// 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.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
+ {
+ protected TestReplayPlayer Player;
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
+ AddStep("Load player", () => LoadScreen(Player));
+ AddUntilStep("player loaded", () => Player.IsLoaded);
+ }
+
+ [Test]
+ public void TestPause()
+ {
+ double? lastTime = null;
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+
+ AddStep("Pause playback", () => InputManager.Key(Key.Space));
+
+ AddUntilStep("Time stopped progressing", () =>
+ {
+ double current = Player.GameplayClockContainer.CurrentTime;
+ bool changed = lastTime != current;
+ lastTime = current;
+
+ return !changed;
+ });
+
+ AddWaitStep("wait some", 10);
+
+ AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
+ }
+
+ [Test]
+ public void TestSeekBackwards()
+ {
+ double? lastTime = null;
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+
+ AddStep("Seek backwards", () =>
+ {
+ lastTime = Player.GameplayClockContainer.CurrentTime;
+ InputManager.Key(Key.Left);
+ });
+
+ AddAssert("Jumped backwards", () => Player.GameplayClockContainer.CurrentTime - lastTime < 0);
+ }
+
+ [Test]
+ public void TestSeekForwards()
+ {
+ double? lastTime = null;
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+
+ AddStep("Seek forwards", () =>
+ {
+ lastTime = Player.GameplayClockContainer.CurrentTime;
+ InputManager.Key(Key.Right);
+ });
+
+ AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
+ }
+
+ protected TestReplayPlayer CreatePlayer(Ruleset ruleset)
+ {
+ Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
+ SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
+
+ return new TestReplayPlayer(false);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index f29fbbf52b..3e8ba69e01 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new[]
{
new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
- new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.ScaleToFit),
+ new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
}
},
@@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Disable()
{
allow = false;
- OnSourceChanged();
+ TriggerSourceChanged();
}
public SwitchableSkinProvidingContainer(ISkin skin)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 6eeb3596a8..7584d67afe 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private OsuGameBase game { get; set; }
- private int nextFrame;
-
private BeatmapSetInfo importedBeatmap;
private int importedBeatmapId;
@@ -51,8 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
base.SetUpSteps();
- AddStep("reset sent frames", () => nextFrame = 0);
-
AddStep("import beatmap", () =>
{
importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
@@ -105,7 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
checkPaused(true);
- sendFrames(1000); // send enough frames to ensure play won't be paused
+ // send enough frames to ensure play won't be paused
+ sendFrames(100);
checkPaused(false);
}
@@ -114,12 +109,12 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestSpectatingDuringGameplay()
{
start();
+ sendFrames(300);
loadSpectatingScreen();
waitForPlayer();
- AddStep("advance frame count", () => nextFrame = 300);
- sendFrames();
+ sendFrames(300);
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000);
}
@@ -220,11 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void sendFrames(int count = 10)
{
- AddStep("send frames", () =>
- {
- testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
- nextFrame += count;
- });
+ AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count));
}
private void loadSpectatingScreen()
@@ -232,14 +223,5 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
}
-
- internal class TestUserLookupCache : UserLookupCache
- {
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
- {
- Id = lookup,
- Username = $"User {lookup}"
- });
- }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 469f594fdc..bb577886cc 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var legacyFrame in frames.Frames)
{
var frame = new TestReplayFrame();
- frame.FromLegacy(legacyFrame, null, null);
+ frame.FromLegacy(legacyFrame, null);
replay.Frames.Add(frame);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
deleted file mode 100644
index c665a57452..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Online.Rooms;
-using osu.Game.Rulesets;
-using osu.Game.Screens.OnlinePlay;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public abstract class RoomManagerTestScene : RoomTestScene
- {
- [Cached(Type = typeof(IRoomManager))]
- protected TestRoomManager RoomManager { get; } = new TestRoomManager();
-
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("clear rooms", () => RoomManager.Rooms.Clear());
- }
-
- protected void AddRooms(int count, RulesetInfo ruleset = null)
- {
- AddStep("add rooms", () =>
- {
- for (int i = 0; i < count; i++)
- {
- var room = new Room
- {
- RoomID = { Value = i },
- Name = { Value = $"Room {i}" },
- Host = { Value = new User { Username = "Host" } },
- EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
- Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
- };
-
- if (ruleset != null)
- {
- room.Playlist.Add(new PlaylistItem
- {
- Ruleset = { Value = ruleset },
- Beatmap =
- {
- Value = new BeatmapInfo
- {
- Metadata = new BeatmapMetadata()
- }
- }
- });
- }
-
- RoomManager.Rooms.Add(room);
- }
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
deleted file mode 100644
index 1785c99784..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Bindables;
-using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestRoomManager : IRoomManager
- {
- public event Action RoomsUpdated
- {
- add { }
- remove { }
- }
-
- public readonly BindableList Rooms = new BindableList();
-
- public IBindable InitialRoomsReceived { get; } = new Bindable(true);
-
- IBindableList IRoomManager.Rooms => Rooms;
-
- public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room);
-
- public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
- {
- }
-
- public void PartRoom()
- {
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
index 9f24347ae9..471d0b6c98 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
@@ -4,17 +4,21 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneLoungeRoomInfo : RoomTestScene
+ public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
+ SelectedRoom.Value = new Room();
+
Child = new RoomInfo
{
Anchor = Anchor.Centre,
@@ -23,15 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
});
- public override void SetUpSteps()
- {
- // Todo: Temp
- }
-
[Test]
public void TestNonSelectedRoom()
{
- AddStep("set null room", () => Room.RoomID.Value = null);
+ AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
}
[Test]
@@ -39,11 +38,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set open room", () =>
{
- Room.RoomID.Value = 0;
- Room.Name.Value = "Room 0";
- Room.Host.Value = new User { Username = "peppy", Id = 2 };
- Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
- Room.Status.Value = new RoomStatusOpen();
+ SelectedRoom.Value.RoomID.Value = 0;
+ SelectedRoom.Value.Name.Value = "Room 0";
+ SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
+ SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
+ SelectedRoom.Value.Status.Value = new RoomStatusOpen();
});
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 5682fd5c3c..75cc687ee8 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -3,24 +3,26 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneLoungeRoomsContainer : RoomManagerTestScene
+ public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
+ protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
+
private RoomsContainer container;
- [BackgroundDependencyLoader]
- private void load()
+ [SetUp]
+ public new void Setup() => Schedule(() =>
{
Child = container = new RoomsContainer
{
@@ -29,12 +31,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 0.5f,
JoinRequested = joinRequested
};
- }
+ });
[Test]
public void TestBasicListChanges()
{
- AddRooms(3);
+ AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
@@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestKeyboardNavigation()
{
- AddRooms(3);
+ AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("no selection", () => checkRoomSelected(null));
@@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestClickDeselection()
{
- AddRooms(1);
+ AddStep("add room", () => RoomManager.AddRooms(1));
AddAssert("no selection", () => checkRoomSelected(null));
@@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestStringFiltering()
{
- AddRooms(4);
+ AddStep("add rooms", () => RoomManager.AddRooms(4));
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
@@ -107,21 +109,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
- AddRooms(2, new OsuRuleset().RulesetInfo);
- AddRooms(3, new CatchRuleset().RulesetInfo);
+ AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo));
+ AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
+ // Todo: What even is this case...?
+ AddStep("set empty filter criteria", () => container.Filter(null));
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }));
-
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }));
-
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
- private bool checkRoomSelected(Room room) => Room == room;
+ private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
index 9ad9f2c883..d66603a448 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual.OnlinePlay;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMatchBeatmapDetailArea : RoomTestScene
+ public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@@ -26,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
+ SelectedRoom.Value = new Room();
+
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,
@@ -37,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewItem()
{
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
- ID = Room.Playlist.Count,
+ ID = SelectedRoom.Value.Playlist.Count,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
index 7cdc6b1a7d..71ba5db481 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
@@ -7,46 +7,49 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Match.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMatchHeader : RoomTestScene
+ public class TestSceneMatchHeader : OnlinePlayTestScene
{
- public TestSceneMatchHeader()
- {
- Child = new Header();
- }
-
[SetUp]
public new void Setup() => Schedule(() =>
{
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value = new Room
{
- Beatmap =
+ Name = { Value = "A very awesome room" },
+ Host = { Value = new User { Id = 2, Username = "peppy" } },
+ Playlist =
{
- Value = new BeatmapInfo
+ new PlaylistItem
{
- Metadata = new BeatmapMetadata
+ Beatmap =
{
- Title = "Title",
- Artist = "Artist",
- AuthorString = "Author",
+ Value = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title",
+ Artist = "Artist",
+ AuthorString = "Author",
+ },
+ Version = "Version",
+ Ruleset = new OsuRuleset().RulesetInfo
+ }
},
- Version = "Version",
- Ruleset = new OsuRuleset().RulesetInfo
+ RequiredMods =
+ {
+ new OsuModDoubleTime(),
+ new OsuModNoFail(),
+ new OsuModRelax(),
+ }
}
- },
- RequiredMods =
- {
- new OsuModDoubleTime(),
- new OsuModNoFail(),
- new OsuModRelax(),
}
- });
+ };
- Room.Name.Value = "A very awesome room";
- Room.Host.Value = new User { Id = 2, Username = "peppy" };
+ Child = new Header();
});
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
index 64eaf0556b..a7a5f3af39 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
@@ -2,72 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMatchLeaderboard : RoomTestScene
+ public class TestSceneMatchLeaderboard : OnlinePlayTestScene
{
- protected override bool UseOnlineAPI => true;
-
- public TestSceneMatchLeaderboard()
- {
- Add(new MatchLeaderboard
- {
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Size = new Vector2(550f, 450f),
- Scope = MatchLeaderboardScope.Overall,
- });
- }
-
[BackgroundDependencyLoader]
- private void load(IAPIProvider api)
+ private void load()
{
- var req = new GetRoomScoresRequest();
- req.Success += v => { };
- req.Failure += _ => { };
+ ((DummyAPIAccess)API).HandleRequest = r =>
+ {
+ switch (r)
+ {
+ case GetRoomLeaderboardRequest leaderboardRequest:
+ leaderboardRequest.TriggerSuccess(new APILeaderboard
+ {
+ Leaderboard = new List
+ {
+ new APIUserScoreAggregate
+ {
+ UserID = 2,
+ User = new User { Id = 2, Username = "peppy" },
+ TotalScore = 995533,
+ RoomID = 3,
+ CompletedBeatmaps = 1,
+ TotalAttempts = 6,
+ Accuracy = 0.9851
+ },
+ new APIUserScoreAggregate
+ {
+ UserID = 1040328,
+ User = new User { Id = 1040328, Username = "smoogipoo" },
+ TotalScore = 981100,
+ RoomID = 3,
+ CompletedBeatmaps = 1,
+ TotalAttempts = 9,
+ Accuracy = 0.937
+ }
+ }
+ });
+ return true;
+ }
- api.Queue(req);
+ return false;
+ };
}
[SetUp]
public new void Setup() => Schedule(() =>
{
- Room.RoomID.Value = 3;
+ SelectedRoom.Value = new Room { RoomID = { Value = 3 } };
+
+ Child = new MatchLeaderboard
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Size = new Vector2(550f, 450f),
+ Scope = MatchLeaderboardScope.Overall,
+ };
});
-
- private class GetRoomScoresRequest : APIRequest>
- {
- protected override string Target => "rooms/3/leaderboard";
- }
-
- private class RoomScore
- {
- [JsonProperty("user")]
- public User User { get; set; }
-
- [JsonProperty("accuracy")]
- public double Accuracy { get; set; }
-
- [JsonProperty("total_score")]
- public int TotalScore { get; set; }
-
- [JsonProperty("pp")]
- public double PP { get; set; }
-
- [JsonProperty("attempts")]
- public int TotalAttempts { get; set; }
-
- [JsonProperty("completed")]
- public int CompletedAttempts { get; set; }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index 5ad35be0ec..e14df62af1 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -3,74 +3,40 @@
using System.Collections.Generic;
using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
-using osu.Game.Database;
-using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
-using osu.Game.Tests.Visual.Spectator;
-using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{
- [Cached(typeof(SpectatorClient))]
- private TestSpectatorClient spectatorClient = new TestSpectatorClient();
-
- [Cached(typeof(UserLookupCache))]
- private UserLookupCache lookupCache = new TestUserLookupCache();
-
- protected override Container Content => content;
- private readonly Container content;
-
- private readonly Dictionary clocks = new Dictionary
- {
- { PLAYER_1_ID, new ManualClock() },
- { PLAYER_2_ID, new ManualClock() }
- };
-
- public TestSceneMultiSpectatorLeaderboard()
- {
- base.Content.AddRange(new Drawable[]
- {
- spectatorClient,
- lookupCache,
- content = new Container { RelativeSizeAxes = Axes.Both }
- });
- }
+ private Dictionary clocks;
+ private MultiSpectatorLeaderboard leaderboard;
[SetUpSteps]
public new void SetUpSteps()
{
- MultiSpectatorLeaderboard leaderboard = null;
-
AddStep("reset", () =>
{
Clear();
- foreach (var (userId, clock) in clocks)
+ clocks = new Dictionary
{
- spectatorClient.EndPlay(userId);
- clock.CurrentTime = 0;
- }
+ { PLAYER_1_ID, new ManualClock() },
+ { PLAYER_2_ID, new ManualClock() }
+ };
+
+ foreach (var (userId, _) in clocks)
+ SpectatorClient.StartPlay(userId, 0);
});
AddStep("create leaderboard", () =>
{
- foreach (var (userId, _) in clocks)
- spectatorClient.StartPlay(userId, 0);
-
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
-
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
@@ -96,10 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
// For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++)
{
- spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
+ SpectatorClient.SendFrames(PLAYER_1_ID, 1);
if (i % 10 == 0)
- spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
+ SpectatorClient.SendFrames(PLAYER_2_ID, 10);
}
});
@@ -145,17 +111,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
-
- private class TestUserLookupCache : UserLookupCache
- {
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
- {
- return Task.FromResult(new User
- {
- Id = lookup,
- Username = $"User {lookup}"
- });
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index b91391c409..072e32370d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -3,31 +3,20 @@
using System.Collections.Generic;
using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Online.Spectator;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
-using osu.Game.Tests.Visual.Spectator;
-using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
- [Cached(typeof(SpectatorClient))]
- private TestSpectatorClient spectatorClient = new TestSpectatorClient();
-
- [Cached(typeof(UserLookupCache))]
- private UserLookupCache lookupCache = new TestUserLookupCache();
-
[Resolved]
private OsuGameBase game { get; set; }
@@ -37,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen;
private readonly List playingUserIds = new List();
- private readonly Dictionary nextFrame = new Dictionary();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
@@ -51,25 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
}
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("reset sent frames", () => nextFrame.Clear());
-
- AddStep("add streaming client", () =>
- {
- Remove(spectatorClient);
- Add(spectatorClient);
- });
-
- AddStep("finish previous gameplay", () =>
- {
- foreach (var id in playingUserIds)
- spectatorClient.EndPlay(id);
- playingUserIds.Clear();
- });
- }
+ [SetUp]
+ public new void Setup() => Schedule(() => playingUserIds.Clear());
[Test]
public void TestDelayedStart()
@@ -80,18 +51,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
- nextFrame[PLAYER_1_ID] = 0;
- nextFrame[PLAYER_2_ID] = 0;
});
loadSpectateScreen(false);
AddWaitStep("wait a bit", 10);
- AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
+ AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1);
AddWaitStep("wait a bit", 10);
- AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
+ AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2);
}
@@ -107,6 +76,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20);
}
+ [Test]
+ public void TestTimeDoesNotProgressWhileAllPlayersPaused()
+ {
+ start(new[] { PLAYER_1_ID, PLAYER_2_ID });
+ loadSpectateScreen();
+
+ sendFrames(PLAYER_1_ID, 40);
+ sendFrames(PLAYER_2_ID, 20);
+
+ checkPaused(PLAYER_2_ID, true);
+ checkPausedInstant(PLAYER_1_ID, false);
+ AddAssert("master clock still running", () => this.ChildrenOfType().Single().IsRunning);
+
+ checkPaused(PLAYER_1_ID, true);
+ AddUntilStep("master clock paused", () => !this.ChildrenOfType().Single().IsRunning);
+ }
+
[Test]
public void TestPlayersMustStartSimultaneously()
{
@@ -151,7 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 20);
- sendFrames(PLAYER_2_ID, 10);
+ sendFrames(PLAYER_2_ID);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
@@ -182,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 1000);
- sendFrames(PLAYER_2_ID, 10);
+ sendFrames(PLAYER_2_ID, 30);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
@@ -208,10 +194,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, true);
- sendFrames(PLAYER_1_ID, 10);
+ sendFrames(PLAYER_1_ID);
sendFrames(PLAYER_2_ID, 20);
- assertMuted(PLAYER_1_ID, false);
- assertMuted(PLAYER_2_ID, true);
+ checkPaused(PLAYER_1_ID, false);
+ assertOneNotMuted();
checkPaused(PLAYER_1_ID, true);
assertMuted(PLAYER_1_ID, true);
@@ -229,6 +215,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertMuted(PLAYER_2_ID, true);
}
+ [Test]
+ public void TestSpectatingDuringGameplay()
+ {
+ var players = new[] { PLAYER_1_ID, PLAYER_2_ID };
+
+ start(players);
+ sendFrames(players, 300);
+
+ loadSpectateScreen();
+ sendFrames(players, 300);
+
+ AddUntilStep("playing from correct point in time", () => this.ChildrenOfType().All(r => r.FrameStableClock.CurrentTime > 30000));
+ }
+
+ [Test]
+ public void TestSpectatingDuringGameplayWithLateFrames()
+ {
+ start(new[] { PLAYER_1_ID, PLAYER_2_ID });
+ sendFrames(new[] { PLAYER_1_ID, PLAYER_2_ID }, 300);
+
+ loadSpectateScreen();
+ sendFrames(PLAYER_1_ID, 300);
+
+ AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
+ checkPaused(PLAYER_1_ID, false);
+
+ sendFrames(PLAYER_2_ID, 300);
+ AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000);
+ }
+
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@@ -242,8 +258,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
}
- private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
-
private void start(int[] userIds, int? beatmapId = null)
{
AddStep("start play", () =>
@@ -251,23 +265,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int id in userIds)
{
Client.CurrentMatchPlayingUserIds.Add(id);
- spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
+ SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
- nextFrame[id] = 0;
}
});
}
- private void finish(int userId)
- {
- AddStep("end play", () =>
- {
- spectatorClient.EndPlay(userId);
- playingUserIds.Remove(userId);
- nextFrame.Remove(userId);
- });
- }
-
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@@ -275,10 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("send frames", () =>
{
foreach (int id in userIds)
- {
- spectatorClient.SendFrames(id, nextFrame[id], count);
- nextFrame[id] += count;
- }
+ SpectatorClient.SendFrames(id, count);
});
}
@@ -286,7 +286,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state);
private void checkPausedInstant(int userId, bool state)
- => AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state);
+ {
+ checkPaused(userId, state);
+
+ // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time.
+ // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state);
+ }
+
+ private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1);
private void assertMuted(int userId, bool muted)
=> AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted);
@@ -297,17 +304,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId);
-
- internal class TestUserLookupCache : UserLookupCache
- {
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
- {
- return Task.FromResult(new User
- {
- Id = lookup,
- Username = $"User {lookup}"
- });
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 599dfb082b..7673efb78f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@@ -18,7 +19,9 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@@ -30,19 +33,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayer : ScreenTestScene
{
- private TestMultiplayer multiplayerScreen;
-
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
- private TestMultiplayerClient client => multiplayerScreen.Client;
- private Room room => client.APIRoom;
+ private DependenciesScreen dependenciesScreen;
+ private TestMultiplayer multiplayerScreen;
+ private TestMultiplayerClient client;
- public TestSceneMultiplayer()
- {
- loadMultiplayer();
- }
+ [Cached(typeof(UserLookupCache))]
+ private UserLookupCache lookupCache = new TestUserLookupCache();
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -51,18 +51,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
}
- [SetUp]
- public void Setup() => Schedule(() =>
+ public override void SetUpSteps()
{
- beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
- importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
- });
+ base.SetUpSteps();
+
+ AddStep("import beatmap", () =>
+ {
+ beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
+ importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
+ });
+
+ AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer());
+
+ AddStep("load dependencies", () =>
+ {
+ client = new TestMultiplayerClient(multiplayerScreen.RoomManager);
+
+ // The screen gets suspended so it stops receiving updates.
+ Child = client;
+
+ LoadScreen(dependenciesScreen = new DependenciesScreen(client));
+ });
+
+ AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
+
+ AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
+ AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded);
+ }
+
+ [Test]
+ public void TestEmpty()
+ {
+ // used to test the flow of multiplayer from visual tests.
+ }
[Test]
public void TestUserSetToIdleWhenBeatmapDeleted()
{
- loadMultiplayer();
-
createRoom(() => new Room
{
Name = { Value = "Test Room" },
@@ -85,8 +110,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
{
- loadMultiplayer();
-
createRoom(() => new Room
{
Name = { Value = "Test Room" },
@@ -123,8 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable()
{
- loadMultiplayer();
-
createRoom(() => new Room
{
Name = { Value = "Test Room" },
@@ -164,11 +185,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
}
+ [Test]
+ public void TestSubScreenExitedWhenDisconnectedFromMultiplayerServer()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddStep("disconnect", () => client.Disconnect());
+ AddUntilStep("back in lounge", () => this.ChildrenOfType().FirstOrDefault()?.IsCurrentScreen() == true);
+ }
+
[Test]
public void TestLeaveNavigation()
{
- loadMultiplayer();
-
createRoom(() => new Room
{
Name = { Value = "Test Room" },
@@ -227,32 +266,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => client.Room != null);
}
- private void loadMultiplayer()
- {
- AddStep("show", () =>
- {
- multiplayerScreen = new TestMultiplayer();
-
- // Needs to be added at a higher level since the multiplayer screen becomes non-current.
- Child = multiplayerScreen.Client;
-
- LoadScreen(multiplayerScreen);
- });
-
- AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded);
- }
-
- private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
+ ///
+ /// Used for the sole purpose of adding as a resolvable dependency.
+ ///
+ private class DependenciesScreen : OsuScreen
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
- public TestMultiplayer()
+ public DependenciesScreen(TestMultiplayerClient client)
{
- Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
+ Client = client;
}
+ }
- protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
+ private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
+ {
+ public new TestMultiplayerRoomManager RoomManager { get; private set; }
+
+ protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager();
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index af2f6fa5fe..0e368b59dd 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -6,12 +6,11 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
-using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
@@ -19,37 +18,20 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
-using osu.Game.Tests.Visual.Online;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene
{
- private const int users = 16;
+ private static IEnumerable users => Enumerable.Range(0, 16);
- [Cached(typeof(SpectatorClient))]
- private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient();
-
- [Cached(typeof(UserLookupCache))]
- private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
+ public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
private MultiplayerGameplayLeaderboard leaderboard;
-
- protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
-
private OsuConfigManager config;
- public TestSceneMultiplayerGameplayLeaderboard()
- {
- base.Content.Children = new Drawable[]
- {
- spectatorClient,
- lookupCache,
- Content
- };
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -59,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public override void SetUpSteps()
{
- AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result);
+ AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () =>
{
@@ -70,14 +52,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- for (int i = 0; i < users; i++)
- spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
+ foreach (var user in users)
+ SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
- spectatorClient.Schedule(() =>
- {
- Client.CurrentMatchPlayingUserIds.Clear();
- Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
- });
+ // Todo: This is REALLY bad.
+ Client.CurrentMatchPlayingUserIds.AddRange(users);
Children = new Drawable[]
{
@@ -86,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playable);
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray())
+ LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -100,24 +79,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestScoreUpdates()
{
- AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100);
+ AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
}
[Test]
public void TestUserQuit()
{
- AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
+ foreach (var user in users)
+ AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).Result.AsNonNull()));
}
[Test]
public void TestChangeScoringMode()
{
- AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5);
+ AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
}
+ protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
+
+ protected class TestDependencies : MultiplayerTestSceneDependencies
+ {
+ protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient();
+ }
+
public class TestMultiplayerSpectatorClient : TestSpectatorClient
{
private readonly Dictionary lastHeaders = new Dictionary();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
index 6b03b53b4b..4e08ffef17 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
@@ -1,7 +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 osu.Framework.Allocation;
+using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@@ -10,18 +10,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
{
- [Cached]
- private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
-
- [BackgroundDependencyLoader]
- private void load()
+ [SetUp]
+ public new void Setup() => Schedule(() =>
{
+ SelectedRoom.Value = new Room();
+
Child = new MultiplayerMatchFooter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 50
};
- }
+ });
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 5b059c06f5..8bcb9cebbc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -29,7 +29,7 @@ using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneMultiplayerMatchSongSelect : RoomTestScene
+ public class TestSceneMultiplayerMatchSongSelect : MultiplayerTestScene
{
private BeatmapManager manager;
private RulesetStore rulesets;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index e8ebc0c426..955be6ca21 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -49,13 +49,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
- Room.Name.Value = "Test Room";
+ SelectedRoom.Value = new Room { Name = { Value = "Test Room" } };
});
[SetUpSteps]
public void SetupSteps()
{
- AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room)));
+ AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value)));
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
}
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set playlist", () =>
{
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
@@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 7f8f04b718..6526f7eea7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -22,8 +22,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
- [SetUp]
- public new void Setup() => Schedule(createNewParticipantsList);
+ [SetUpSteps]
+ public void SetupSteps()
+ {
+ createNewParticipantsList();
+ }
[Test]
public void TestAddUser()
@@ -88,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestCorrectInitialState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
- AddStep("recreate list", createNewParticipantsList);
+ createNewParticipantsList();
checkProgressBarVisibility(true);
}
@@ -233,7 +236,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewParticipantsList()
{
- Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
+ ParticipantsList participantsList = null;
+
+ AddStep("create new list", () => Child = participantsList = new ParticipantsList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Size = new Vector2(380, 0.7f)
+ });
+
+ AddUntilStep("wait for list to load", () => participantsList.IsLoaded);
}
private void checkProgressBarVisibility(bool visible) =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
index 929cd6ca80..820b403a10 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -27,7 +28,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
- private OnlinePlayBeatmapAvailabilityTracker beatmapTracker;
private BeatmapSetInfo importedSet;
private readonly Bindable selectedItem = new Bindable();
@@ -43,18 +43,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
-
- Add(beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker
- {
- SelectedItem = { BindTarget = selectedItem }
- });
-
- Dependencies.Cache(beatmapTracker);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
+ AvailabilityTracker.SelectedItem.BindTo(selectedItem);
+
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem
@@ -71,18 +66,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnReadyClick = async () =>
+ OnReadyClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ Task.Run(async () =>
{
- await Client.StartMatch();
- return;
- }
+ if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ {
+ await Client.StartMatch();
+ return;
+ }
- await Client.ToggleReady();
- readyClickOperation.Dispose();
+ await Client.ToggleReady();
+
+ readyClickOperation.Dispose();
+ });
}
});
});
@@ -114,10 +113,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
addClickButtonStep();
- AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
+ AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
- AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
+ AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
@@ -133,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
addClickButtonStep();
- AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
+ AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
@@ -207,8 +206,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void verifyGameplayStartFlow()
{
+ AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
- AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
+ AddUntilStep("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value);
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
@@ -219,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
- AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value);
+ AddUntilStep("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
index c008771fd9..b17427a30b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
@@ -3,37 +3,38 @@
using System;
using NUnit.Framework;
-using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
[HeadlessTest]
- public class TestSceneMultiplayerRoomManager : RoomTestScene
+ public class TestSceneMultiplayerRoomManager : MultiplayerTestScene
{
- private TestMultiplayerRoomContainer roomContainer;
- private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager;
+ protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
+
+ public TestSceneMultiplayerRoomManager()
+ : base(false)
+ {
+ }
[Test]
public void TestPollsInitially()
{
AddStep("create room manager with a few rooms", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
- roomManager.PartRoom();
- roomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
- roomManager.PartRoom();
- roomManager.ClearRooms();
- });
+ RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
+ RoomManager.PartRoom();
+ RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
+ RoomManager.PartRoom();
+ RoomManager.ClearRooms();
});
- AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
- AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
+ AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
+ AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
@@ -41,19 +42,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a few rooms", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom());
- roomManager.PartRoom();
- roomManager.CreateRoom(createRoom());
- roomManager.PartRoom();
- });
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.PartRoom();
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.PartRoom();
});
- AddStep("disconnect", () => roomContainer.Client.Disconnect());
+ AddStep("disconnect", () => Client.Disconnect());
- AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0);
- AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
+ AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0);
+ AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
@@ -61,20 +59,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a few rooms", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom());
- roomManager.PartRoom();
- roomManager.CreateRoom(createRoom());
- roomManager.PartRoom();
- });
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.PartRoom();
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.PartRoom();
});
- AddStep("disconnect", () => roomContainer.Client.Disconnect());
- AddStep("connect", () => roomContainer.Client.Connect());
+ AddStep("disconnect", () => Client.Disconnect());
+ AddStep("connect", () => Client.Connect());
- AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
- AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
+ AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
+ AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
@@ -82,15 +77,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom());
- roomManager.ClearRooms();
- });
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.ClearRooms();
});
- AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0);
- AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
+ AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0);
+ AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
@@ -98,13 +90,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom());
- });
+ RoomManager.CreateRoom(createRoom());
});
- AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
+ AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
[Test]
@@ -112,14 +101,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- roomManager.CreateRoom(createRoom());
- roomManager.PartRoom();
- });
+ RoomManager.CreateRoom(createRoom());
+ RoomManager.PartRoom();
});
- AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null);
+ AddAssert("multiplayer room parted", () => Client.Room == null);
}
[Test]
@@ -127,16 +113,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
- createRoomManager().With(d => d.OnLoadComplete += _ =>
- {
- var r = createRoom();
- roomManager.CreateRoom(r);
- roomManager.PartRoom();
- roomManager.JoinRoom(r);
- });
+ var r = createRoom();
+ RoomManager.CreateRoom(r);
+ RoomManager.PartRoom();
+ RoomManager.JoinRoom(r);
});
- AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
+ AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
private Room createRoom(Action initFunc = null)
@@ -161,18 +144,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
return room;
}
- private TestMultiplayerRoomManager createRoomManager()
+ private class TestDependencies : MultiplayerTestSceneDependencies
{
- Child = roomContainer = new TestMultiplayerRoomContainer
+ public TestDependencies()
{
- RoomManager =
- {
- TimeBetweenListingPolls = { Value = 1 },
- TimeBetweenSelectionPolls = { Value = 1 }
- }
- };
-
- return roomManager;
+ // Need to set these values as early as possible.
+ RoomManager.TimeBetweenListingPolls.Value = 1;
+ RoomManager.TimeBetweenSelectionPolls.Value = 1;
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index d00404102c..3d08d5da9e 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -37,40 +38,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
private IDisposable readyClickOperation;
- protected override Container Content => content;
- private readonly Container content;
-
- public TestSceneMultiplayerSpectateButton()
- {
- base.Content.Add(content = new Container
- {
- RelativeSizeAxes = Axes.Both
- });
- }
-
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- {
- var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
-
- return dependencies;
- }
-
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
-
- var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } };
- base.Content.Add(beatmapTracker);
- Dependencies.Cache(beatmapTracker);
-
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
+ AvailabilityTracker.SelectedItem.BindTo(selectedItem);
+
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem
@@ -90,11 +70,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnSpectateClick = async () =>
+ OnSpectateClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- await Client.ToggleSpectate();
- readyClickOperation.Dispose();
+
+ Task.Run(async () =>
+ {
+ await Client.ToggleSpectate();
+ readyClickOperation.Dispose();
+ });
}
},
readyButton = new MultiplayerReadyButton
@@ -102,18 +86,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- OnReadyClick = async () =>
+ OnReadyClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
- if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ Task.Run(async () =>
{
- await Client.StartMatch();
- return;
- }
+ if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
+ {
+ await Client.StartMatch();
+ return;
+ }
- await Client.ToggleReady();
- readyClickOperation.Dispose();
+ await Client.ToggleReady();
+
+ readyClickOperation.Dispose();
+ });
}
}
}
@@ -134,10 +122,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestToggleWhenIdle(MultiplayerUserState initialState)
{
addClickSpectateButtonStep();
- AddAssert("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating);
+ AddUntilStep("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating);
addClickSpectateButtonStep();
- AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
+ AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(MultiplayerRoomState.Closed)]
@@ -186,9 +174,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
private void assertSpectateButtonEnablement(bool shouldBeEnabled)
- => AddAssert($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled);
+ => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled);
private void assertReadyButtonEnablement(bool shouldBeEnabled)
- => AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled);
+ => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index d95a95ebe5..e4bf9b36ed 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -14,16 +14,18 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
+using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestScenePlaylistsSongSelect : RoomTestScene
+ public class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@@ -85,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("reset", () =>
{
+ SelectedRoom.Value = new Room();
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.Value = Array.Empty();
@@ -98,14 +101,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestItemAddedIfEmptyOnStart()
{
AddStep("finalise selection", () => songSelect.FinaliseSelection());
- AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
public void TestItemAddedWhenCreateNewItemClicked()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
- AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
@@ -113,7 +116,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("finalise selection", () => songSelect.FinaliseSelection());
- AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
@@ -121,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
- AddAssert("playlist has 2 items", () => Room.Playlist.Count == 2);
+ AddAssert("playlist has 2 items", () => SelectedRoom.Value.Playlist.Count == 2);
}
[Test]
@@ -131,13 +134,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("rearrange", () =>
{
- var item = Room.Playlist[0];
- Room.Playlist.RemoveAt(0);
- Room.Playlist.Add(item);
+ var item = SelectedRoom.Value.Playlist[0];
+ SelectedRoom.Value.Playlist.RemoveAt(0);
+ SelectedRoom.Value.Playlist.Add(item);
});
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
- AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
+ AddAssert("new item has id 2", () => SelectedRoom.Value.Playlist.Last().ID == 2);
}
///
@@ -151,8 +154,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
- AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
- AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
+ AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value));
+ AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
}
///
@@ -174,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
- AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
+ AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value));
}
private class TestPlaylistsSongSelect : PlaylistsSongSelect
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs
new file mode 100644
index 0000000000..cdeafdc9a3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Rooms;
+using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene
+ {
+ [SetUp]
+ public new void Setup() => Schedule(() =>
+ {
+ SelectedRoom.Value = new Room();
+
+ Child = new StarRatingRangeDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ });
+
+ [Test]
+ public void TestRange([Values(0, 2, 3, 4, 6, 7)] double min, [Values(0, 2, 3, 4, 6, 7)] double max)
+ {
+ AddStep("set playlist", () =>
+ {
+ SelectedRoom.Value.Playlist.AddRange(new[]
+ {
+ new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = min } } },
+ new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = max } } },
+ });
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 3cedaf9d45..4ec76e1e4b 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -11,6 +11,7 @@ using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
+using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
@@ -57,8 +58,10 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtSongSelectFromPlayerLoader()
{
- PushAndConfirm(() => new TestPlaySongSelect());
- PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
+ importAndWaitForSongSelect();
+
+ AddStep("Press enter", () => InputManager.Key(Key.Enter));
+ AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
@@ -68,8 +71,10 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestPerformAtMenuFromPlayerLoader()
{
- PushAndConfirm(() => new TestPlaySongSelect());
- PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
+ importAndWaitForSongSelect();
+
+ AddStep("Press enter", () => InputManager.Key(Key.Enter));
+ AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -165,6 +170,13 @@ namespace osu.Game.Tests.Visual.Navigation
}
}
+ private void importAndWaitForSongSelect()
+ {
+ AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
+ PushAndConfirm(() => new TestPlaySongSelect());
+ AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineBeatmapSetID == 241526);
+ }
+
public class DialogBlockingScreen : OsuScreen
{
[Resolved]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index b6dce2c398..af2e4fc91a 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -9,6 +10,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
@@ -102,7 +106,7 @@ needs_cleanup: true
{
AddStep("Add absolute image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "";
});
}
@@ -112,8 +116,7 @@ needs_cleanup: true
{
AddStep("Add relative image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
- markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "";
});
}
@@ -123,8 +126,7 @@ needs_cleanup: true
{
AddStep("Add paragraph with block image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
- markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Interface/";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image

@@ -138,7 +140,7 @@ Line after image";
{
AddStep("Add inline image", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = " osu!";
});
}
@@ -148,7 +150,7 @@ Line after image";
{
AddStep("Add Table", () =>
{
- markdownContainer.DocumentUrl = "https://dev.ppy.sh";
+ markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = @"
| Image | Name | Effect |
| :-: | :-: | :-- |
@@ -162,15 +164,33 @@ Line after image";
});
}
+ [Test]
+ public void TestWideImageNotExceedContainer()
+ {
+ AddStep("Add image", () =>
+ {
+ markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
+ markdownContainer.Text = "";
+ });
+
+ AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType().First().DelayedLoadCompleted);
+
+ AddStep("Change container width", () =>
+ {
+ markdownContainer.Width = 0.5f;
+ });
+
+ AddAssert("Image not exceed container width", () =>
+ {
+ var spriteImage = markdownContainer.ChildrenOfType().First();
+ return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth);
+ });
+ }
+
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
- public new string DocumentUrl
- {
- set => base.DocumentUrl = value;
- }
-
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 618447eae2..b16b61c5c7 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -3,25 +3,21 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
-using osu.Game.Tests.Visual.Multiplayer;
+using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
- public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene
+ public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{
- private LoungeSubScreen loungeScreen;
+ protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
- [BackgroundDependencyLoader]
- private void load()
- {
- }
+ private LoungeSubScreen loungeScreen;
public override void SetUpSteps()
{
@@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestScrollSelectedIntoView()
{
- AddRooms(30);
+ AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
index 44a79b6598..a320cb240f 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
@@ -3,7 +3,6 @@
using System;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -12,26 +11,28 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
+using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
- public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene
+ public class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene
{
- [Cached(Type = typeof(IRoomManager))]
- private TestRoomManager roomManager = new TestRoomManager();
+ protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private TestRoomSettings settings;
+ protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
+
[SetUp]
public new void Setup() => Schedule(() =>
{
- settings = new TestRoomSettings
+ SelectedRoom.Value = new Room();
+
+ Child = settings = new TestRoomSettings
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }
};
-
- Child = settings;
});
[Test]
@@ -39,19 +40,19 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("clear name and beatmap", () =>
{
- Room.Name.Value = "";
- Room.Playlist.Clear();
+ SelectedRoom.Value.Name.Value = "";
+ SelectedRoom.Value.Playlist.Clear();
});
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
- AddStep("set name", () => Room.Name.Value = "Room name");
+ AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
- AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
+ AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
- AddStep("clear name", () => Room.Name.Value = "");
+ AddStep("clear name", () => SelectedRoom.Value.Name.Value = "");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
}
@@ -67,9 +68,9 @@ namespace osu.Game.Tests.Visual.Playlists
{
settings.NameField.Current.Value = expected_name;
settings.DurationField.Current.Value = expectedDuration;
- Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
- roomManager.CreateRequested = r =>
+ RoomManager.CreateRequested = r =>
{
createdRoom = r;
return true;
@@ -88,11 +89,11 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("setup", () =>
{
- Room.Name.Value = "Test Room";
- Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
+ SelectedRoom.Value.Name.Value = "Test Room";
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
fail = true;
- roomManager.CreateRequested = _ => !fail;
+ RoomManager.CreateRequested = _ => !fail;
});
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
@@ -119,7 +120,12 @@ namespace osu.Game.Tests.Visual.Playlists
public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText;
}
- private class TestRoomManager : IRoomManager
+ private class TestDependencies : OnlinePlayTestSceneDependencies
+ {
+ protected override IRoomManager CreateRoomManager() => new TestRoomManager();
+ }
+
+ protected class TestRoomManager : IRoomManager
{
public const string FAILED_TEXT = "failed";
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
index 255f147ec9..76a78c0a3c 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
@@ -3,21 +3,23 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Playlists
{
- public class TestScenePlaylistsParticipantsList : RoomTestScene
+ public class TestScenePlaylistsParticipantsList : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
- Room.RoomID.Value = 7;
+ SelectedRoom.Value = new Room { RoomID = { Value = 7 } };
for (int i = 0; i < 50; i++)
{
- Room.RecentParticipants.Add(new User
+ SelectedRoom.Value.RecentParticipants.Add(new User
{
Username = "peppy",
Statistics = new UserStatistics { GlobalRank = 1234 },
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
index 6d7a254ab9..f2bfb80beb 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs
@@ -15,20 +15,17 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Playlists
{
- public class TestScenePlaylistsRoomSubScreen : RoomTestScene
+ public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
- [Cached(typeof(IRoomManager))]
- private readonly TestRoomManager roomManager = new TestRoomManager();
-
private BeatmapManager manager;
private RulesetStore rulesets;
@@ -56,8 +53,9 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUpSteps]
public void SetupSteps()
{
+ AddStep("set room", () => SelectedRoom.Value = new Room());
AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait());
- AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room)));
+ AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@@ -66,12 +64,12 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("set room properties", () =>
{
- Room.RoomID.Value = 1;
- Room.Name.Value = "my awesome room";
- Room.Host.Value = new User { Id = 2, Username = "peppy" };
- Room.RecentParticipants.Add(Room.Host.Value);
- Room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.RoomID.Value = 1;
+ SelectedRoom.Value.Name.Value = "my awesome room";
+ SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
+ SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value);
+ SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -87,9 +85,9 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("set room properties", () =>
{
- Room.Name.Value = "my awesome room";
- Room.Host.Value = new User { Id = 2, Username = "peppy" };
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Name.Value = "my awesome room";
+ SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -103,7 +101,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("click", () => InputManager.Click(MouseButton.Left));
- AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]);
+ AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
}
[Test]
@@ -138,9 +136,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("load room", () =>
{
- Room.Name.Value = "my awesome room";
- Room.Host.Value = new User { Id = 2, Username = "peppy" };
- Room.Playlist.Add(new PlaylistItem
+ SelectedRoom.Value.Name.Value = "my awesome room";
+ SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
+ SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = importedSet.Beatmaps[0] },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -171,30 +169,5 @@ namespace osu.Game.Tests.Visual.Playlists
{
}
}
-
- private class TestRoomManager : IRoomManager
- {
- public event Action RoomsUpdated
- {
- add => throw new NotImplementedException();
- remove => throw new NotImplementedException();
- }
-
- public IBindable InitialRoomsReceived { get; } = new Bindable(true);
-
- public IBindableList Rooms { get; } = new BindableList();
-
- public void CreateRoom(Room room, Action onSuccess = null, Action onError = null)
- {
- room.RoomID.Value = 1;
- onSuccess?.Invoke(room);
- }
-
- public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room);
-
- public void PartRoom()
- {
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
index 082d85603e..227bce0c60 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Settings
[BackgroundDependencyLoader]
private void load()
{
- Add(new DirectorySelector { RelativeSizeAxes = Axes.Both });
+ Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both });
}
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
index 311e4c3362..84a0fc6e4c 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
@@ -12,13 +12,13 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestAllFiles()
{
- AddStep("create", () => Child = new FileSelector { RelativeSizeAxes = Axes.Both });
+ AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
}
[Test]
public void TestJpgFilesOnly()
{
- AddStep("create", () => Child = new FileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
+ AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
}
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
index f63145f534..df59b9284b 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
@@ -17,28 +16,65 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestRestoreDefaultValueButtonVisibility()
{
- TestSettingsTextBox textBox = null;
+ SettingsTextBox textBox = null;
+ RestoreDefaultValueButton restoreDefaultValueButton = null;
- AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
+ AddStep("create settings item", () =>
{
- Current = new Bindable
+ Child = textBox = new SettingsTextBox
{
- Default = "test",
- Value = "test"
- }
+ Current = new Bindable
+ {
+ Default = "test",
+ Value = "test"
+ }
+ };
+
+ restoreDefaultValueButton = textBox.ChildrenOfType>().Single();
});
- AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+ AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default");
- AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
+ AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddStep("restore default", () => textBox.Current.SetDefault());
- AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+ AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
- private class TestSettingsTextBox : SettingsTextBox
+ ///
+ /// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
+ /// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
+ ///
+ [TestCase(4.2f)]
+ [TestCase(9.9f)]
+ public void TestRestoreDefaultValueButtonPrecision(float initialValue)
{
- public Drawable RestoreDefaultValueButton => this.ChildrenOfType>().Single();
+ BindableFloat current = null;
+ SettingsSlider sliderBar = null;
+ RestoreDefaultValueButton restoreDefaultValueButton = null;
+
+ AddStep("create settings item", () =>
+ {
+ Child = sliderBar = new SettingsSlider
+ {
+ Current = current = new BindableFloat(initialValue)
+ {
+ MinValue = 0f,
+ MaxValue = 10f,
+ Precision = 0.1f,
+ }
+ };
+
+ restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single();
+ });
+
+ AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
+
+ AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f);
+ AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
+
+ AddStep("restore default", () => sliderBar.Current.SetDefault());
+ AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
}
-}
\ No newline at end of file
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
index 06572f66bf..b4544fbc85 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "All Metrics",
Metadata = new BeatmapMetadata
{
- Source = "osu!lazer",
+ Source = "osu!",
Tags = "this beatmap has all the metrics",
},
BaseDifficulty = new BeatmapDifficulty
@@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "Only Ratings",
Metadata = new BeatmapMetadata
{
- Source = "osu!lazer",
+ Source = "osu!",
Tags = "this beatmap has ratings metrics but not retries or fails",
},
BaseDifficulty = new BeatmapDifficulty
@@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "Only Retries and Fails",
Metadata = new BeatmapMetadata
{
- Source = "osu!lazer",
+ Source = "osu!",
Tags = "this beatmap has retries and fails but no ratings",
},
BaseDifficulty = new BeatmapDifficulty
@@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "No Metrics",
Metadata = new BeatmapMetadata
{
- Source = "osu!lazer",
+ Source = "osu!",
Tags = "this beatmap has no metrics",
},
BaseDifficulty = new BeatmapDifficulty
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
index 271fbde5c3..449401c0bf 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
@@ -31,10 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
[Test]
- public void TestLocal([Values("Beatmap", "Some long title and stuff")]
- string title,
- [Values("Trial", "Some1's very hardest difficulty")]
- string version)
+ public void TestLocal(
+ [Values("Beatmap", "Some long title and stuff")]
+ string title,
+ [Values("Trial", "Some1's very hardest difficulty")]
+ string version)
{
showMetadataForBeatmap(() => CreateWorkingBeatmap(new Beatmap
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs
new file mode 100644
index 0000000000..fa9179443d
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneColourPicker.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneColourPicker : OsuTestScene
+ {
+ private readonly Bindable colour = new Bindable(Colour4.Aquamarine);
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create pickers", () => Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = @"No OverlayColourProvider",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ new OsuColourPicker
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = { BindTarget = colour },
+ }
+ }
+ },
+ new ColourProvidingContainer(OverlayColourScheme.Blue)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = @"With blue OverlayColourProvider",
+ Font = OsuFont.Default.With(size: 40)
+ },
+ new OsuColourPicker
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Current = { BindTarget = colour },
+ }
+ }
+ }
+ }
+ }
+ });
+
+ AddStep("set green", () => colour.Value = Colour4.LimeGreen);
+ AddStep("set white", () => colour.Value = Colour4.White);
+ AddStep("set red", () => colour.Value = Colour4.Red);
+ }
+
+ private class ColourProvidingContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider provider { get; }
+
+ public ColourProvidingContainer(OverlayColourScheme colourScheme)
+ {
+ provider = new OverlayColourProvider(colourScheme);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
new file mode 100644
index 0000000000..e0d76b3e4a
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -0,0 +1,225 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneModDifficultyAdjustSettings : OsuManualInputManagerTestScene
+ {
+ private OsuModDifficultyAdjust modDifficultyAdjust;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create control", () =>
+ {
+ modDifficultyAdjust = new OsuModDifficultyAdjust();
+
+ Child = new Container
+ {
+ Size = new Vector2(300),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ChildrenEnumerable = modDifficultyAdjust.CreateSettingsControls(),
+ },
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestFollowsBeatmapDefaultsVisually()
+ {
+ setBeatmapWithDifficultyParameters(5);
+
+ checkSliderAtValue("Circle Size", 5);
+ checkBindableAtValue("Circle Size", null);
+
+ setBeatmapWithDifficultyParameters(8);
+
+ checkSliderAtValue("Circle Size", 8);
+ checkBindableAtValue("Circle Size", null);
+ }
+
+ [Test]
+ public void TestOutOfRangeValueStillApplied()
+ {
+ AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+
+ // this is a no-op, just showing that it won't reset the value during deserialisation.
+ setExtendedLimits(false);
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+
+ // setting extended limits will reset the serialisation exception.
+ // this should be fine as the goal is to allow, at most, the value of extended limits.
+ setExtendedLimits(true);
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+ }
+
+ [Test]
+ public void TestExtendedLimits()
+ {
+ setSliderValue("Circle Size", 99);
+
+ checkSliderAtValue("Circle Size", 10);
+ checkBindableAtValue("Circle Size", 10);
+
+ setExtendedLimits(true);
+
+ checkSliderAtValue("Circle Size", 10);
+ checkBindableAtValue("Circle Size", 10);
+
+ setSliderValue("Circle Size", 99);
+
+ checkSliderAtValue("Circle Size", 11);
+ checkBindableAtValue("Circle Size", 11);
+
+ setExtendedLimits(false);
+
+ checkSliderAtValue("Circle Size", 10);
+ checkBindableAtValue("Circle Size", 10);
+ }
+
+ [Test]
+ public void TestUserOverrideMaintainedOnBeatmapChange()
+ {
+ setSliderValue("Circle Size", 9);
+
+ setBeatmapWithDifficultyParameters(2);
+
+ checkSliderAtValue("Circle Size", 9);
+ checkBindableAtValue("Circle Size", 9);
+ }
+
+ [Test]
+ public void TestResetToDefault()
+ {
+ setBeatmapWithDifficultyParameters(2);
+
+ setSliderValue("Circle Size", 9);
+ checkSliderAtValue("Circle Size", 9);
+ checkBindableAtValue("Circle Size", 9);
+
+ resetToDefault("Circle Size");
+ checkSliderAtValue("Circle Size", 2);
+ checkBindableAtValue("Circle Size", null);
+ }
+
+ [Test]
+ public void TestUserOverrideMaintainedOnMatchingBeatmapValue()
+ {
+ setBeatmapWithDifficultyParameters(3);
+
+ checkSliderAtValue("Circle Size", 3);
+ checkBindableAtValue("Circle Size", null);
+
+ // need to initially change it away from the current beatmap value to trigger an override.
+ setSliderValue("Circle Size", 4);
+ setSliderValue("Circle Size", 3);
+
+ checkSliderAtValue("Circle Size", 3);
+ checkBindableAtValue("Circle Size", 3);
+
+ setBeatmapWithDifficultyParameters(4);
+
+ checkSliderAtValue("Circle Size", 3);
+ checkBindableAtValue("Circle Size", 3);
+ }
+
+ [Test]
+ public void TestResetToDefaults()
+ {
+ setBeatmapWithDifficultyParameters(5);
+
+ setSliderValue("Circle Size", 3);
+ setExtendedLimits(true);
+
+ checkSliderAtValue("Circle Size", 3);
+ checkBindableAtValue("Circle Size", 3);
+
+ AddStep("reset mod settings", () => modDifficultyAdjust.ResetSettingsToDefaults());
+
+ checkSliderAtValue("Circle Size", 5);
+ checkBindableAtValue("Circle Size", null);
+ }
+
+ private void resetToDefault(string name)
+ {
+ AddStep($"Reset {name} to default", () =>
+ this.ChildrenOfType().First(c => c.LabelText == name)
+ .Current.SetDefault());
+ }
+
+ private void setExtendedLimits(bool status) =>
+ AddStep($"Set extended limits {status}", () => modDifficultyAdjust.ExtendedLimits.Value = status);
+
+ private void setSliderValue(string name, float value)
+ {
+ AddStep($"Set {name} slider to {value}", () =>
+ this.ChildrenOfType().First(c => c.LabelText == name)
+ .ChildrenOfType>().First().Current.Value = value);
+ }
+
+ private void checkBindableAtValue(string name, float? expectedValue)
+ {
+ AddAssert($"Bindable {name} is {(expectedValue?.ToString() ?? "null")}", () =>
+ this.ChildrenOfType().First(c => c.LabelText == name)
+ .Current.Value == expectedValue);
+ }
+
+ private void checkSliderAtValue(string name, float expectedValue)
+ {
+ AddAssert($"Slider {name} at {expectedValue}", () =>
+ this.ChildrenOfType().First(c => c.LabelText == name)
+ .ChildrenOfType>().First().Current.Value == expectedValue);
+ }
+
+ private void setBeatmapWithDifficultyParameters(float value)
+ {
+ AddStep($"set beatmap with all {value}", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ OverallDifficulty = value,
+ CircleSize = value,
+ DrainRate = value,
+ ApproachRate = value,
+ }
+ }
+ }));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index df8ef92a05..3485d7fbc3 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
+using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
@@ -50,6 +51,38 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show());
}
+ ///
+ /// Ensure that two mod overlays are not cross polluting via central settings instances.
+ ///
+ [Test]
+ public void TestSettingsNotCrossPolluting()
+ {
+ Bindable> selectedMods2 = null;
+
+ AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
+
+ AddStep("set setting", () => modSelect.ChildrenOfType>().First().Current.Value = 8);
+
+ AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
+
+ AddStep("create second bindable", () => selectedMods2 = new Bindable>(new Mod[] { new OsuModDifficultyAdjust() }));
+
+ AddStep("create second overlay", () =>
+ {
+ Add(modSelect = new TestModSelectOverlay().With(d =>
+ {
+ d.Origin = Anchor.TopCentre;
+ d.Anchor = Anchor.TopCentre;
+ d.SelectedMods.BindTarget = selectedMods2;
+ }));
+ });
+
+ AddStep("show", () => modSelect.Show());
+
+ AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
+ AddAssert("ensure second is default", () => selectedMods2.Value.OfType