diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index a92191a439..e34626a59e 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -4,3 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
+T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
+T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
diff --git a/osu.Android.props b/osu.Android.props
index aaac6ec427..73fbe3ab2e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index f2e1c0ec3b..88fe8f1150 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
new file mode 100644
index 0000000000..a48ecb9b79
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+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 osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneHyperDashColouring : OsuTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Test]
+ public void TestDefaultFruitColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomFruitColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestCustomFruitColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestFruitColourFallback()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashColour);
+ }
+
+ private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
+ {
+ DrawableFruit drawableFruit = null;
+
+ AddStep("create hyper-dash fruit", () =>
+ {
+ var fruit = new Fruit { HyperDashTarget = new Banana() };
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+ });
+
+ AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
+ }
+
+ private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
+ {
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var testSkinProvider = new SkinProvidingContainer(skin);
+ var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
+
+ return legacySkinProvider
+ .WithChild(testSkinProvider
+ .WithChild(legacySkinTransformer
+ .WithChild(child)));
+ }
+
+ private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
+ fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour);
+
+ private class TestSkin : LegacySkin
+ {
+ public Color4 HyperDashColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
+ }
+
+ public Color4 HyperDashAfterImageColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
+ }
+
+ public Color4 HyperDashFruitColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
+ }
+
+ public TestSkin()
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ }
+ }
+ }
+}
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 8c371db257..cbd3dc5518 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
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 4d9dbbbc5f..d99325ff87 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -71,8 +71,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap)
{
- using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
- halfCatcherWidth = catcher.CatchWidth * 0.5f;
+ halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
return new Skill[]
{
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 1ef235f764..16414261a5 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -9,17 +9,26 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset
+ public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"Use the mouse to control the catcher.";
+ private DrawableRuleset drawableRuleset;
+
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ this.drawableRuleset = drawableRuleset;
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ if (!drawableRuleset.HasReplayLoaded.Value)
+ drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
}
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 6844be5941..b12cdd4ccb 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -70,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
+ protected override float SamplePlaybackPosition => HitObject.X;
+
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
index 5797588ded..7ac9f11ad6 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
@@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- BorderColour = Color4.Red,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 12f * RADIUS_ADJUST,
Children = new Drawable[]
{
@@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Red,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
});
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index 65e6e6f209..4a87eb95e7 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
- public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case CatchSkinColour colour:
+ return source.GetConfig(new SkinCustomColourLookup(colour));
+ }
+
+ return source.GetConfig(lookup);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
new file mode 100644
index 0000000000..4506111498
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public enum CatchSkinColour
+ {
+ ///
+ /// The colour to be used for the catcher while in hyper-dashing state.
+ ///
+ HyperDash,
+
+ ///
+ /// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
+ ///
+ HyperDashFruit,
+
+ ///
+ /// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
+ ///
+ HyperDashAfterImage,
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
index 25ee0811d0..5be54d3882 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
@@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
var hyperDash = new Sprite
{
- Texture = skin.GetTexture(lookupName),
- Colour = Color4.Red,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = 1,
Alpha = 0.7f,
- Scale = new Vector2(1.2f)
+ Scale = new Vector2(1.2f),
+ Texture = skin.GetTexture(lookupName),
+ Colour = skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ??
+ skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
+ Catcher.DEFAULT_HYPER_DASH_COLOUR,
};
AddInternal(hyperDash);
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 7c815370c8..daf9456919 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class Catcher : Container, IKeyBindingHandler
{
+ public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
+
///
/// Whether we are hyper-dashing or not.
///
@@ -42,11 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
private const float allowed_catch_range = 0.8f;
- ///
- /// Width of the area that can be used to attempt catches during gameplay.
- ///
- internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range;
-
protected bool Dashing
{
get => dashing;
@@ -77,6 +74,11 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
+ ///
+ /// Width of the area that can be used to attempt catches during gameplay.
+ ///
+ private readonly float catchWidth;
+
private Container caughtFruit;
private CatcherSprite catcherIdle;
@@ -104,7 +106,9 @@ namespace osu.Game.Rulesets.Catch.UI
Size = new Vector2(CatcherArea.CATCHER_SIZE);
if (difficulty != null)
- Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ Scale = calculateScale(difficulty);
+
+ catchWidth = CalculateCatchWidth(Scale);
}
[BackgroundDependencyLoader]
@@ -137,6 +141,26 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher();
}
+ ///
+ /// Calculates the scale of the catcher based off the provided beatmap difficulty.
+ ///
+ private static Vector2 calculateScale(BeatmapDifficulty difficulty)
+ => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+
+ ///
+ /// Calculates the width of the area used for attempting catches in gameplay.
+ ///
+ /// The scale of the catcher.
+ internal static float CalculateCatchWidth(Vector2 scale)
+ => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
+
+ ///
+ /// Calculates the width of the area used for attempting catches in gameplay.
+ ///
+ /// The beatmap difficulty.
+ internal static float CalculateCatchWidth(BeatmapDifficulty difficulty)
+ => CalculateCatchWidth(calculateScale(difficulty));
+
///
/// Add a caught fruit to the catcher's stack.
///
@@ -175,7 +199,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// Whether the catch is possible.
public bool AttemptCatch(CatchHitObject fruit)
{
- var halfCatchWidth = CatchWidth * 0.5f;
+ var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
new file mode 100644
index 0000000000..40bb83aece
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Replays;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaLegacyReplayTest
+ {
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Key5)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
+ beatmap.Stages.Add(new StageDefinition { Columns = 5 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index afde1c9521..aac77c9c1c 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
-
- AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png
new file mode 100644
index 0000000000..aa681f6f22
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
new file mode 100644
index 0000000000..56564776b3
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
@@ -0,0 +1,6 @@
+[General]
+Version: 2.4
+
+[Mania]
+Keys: 4
+ColumnLineWidth: 3,1,3,1,1
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
index d6bacbe59e..bde323f187 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 7b0cf40d45..0d13b85901 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
private const double time_after_tail = 5250;
private List judgementResults;
- private bool allJudgedFired;
///
/// -----[ ]-----
@@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
{
if (currentPlayer == p) judgementResults.Add(result);
};
- p.ScoreProcessor.AllJudged += () =>
- {
- if (currentPlayer == p) allJudgedFired = true;
- };
};
LoadScreen(currentPlayer = p);
- allJudgedFired = false;
judgementResults = new List();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
- AddUntilStep("Wait for all judged", () => allJudgedFired);
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
index d7b539a2a0..2d97e61aa5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
@@ -1,17 +1,59 @@
// 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.Extensions.IEnumerableExtensions;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ this.ChildrenOfType().ForEach(c => c.Clear());
+
+ ResetPlacement();
+
+ ((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
+ });
+
+ [Test]
+ public void TestPlaceBeforeCurrentTimeDownwards()
+ {
+ AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time < 0", () => getNote().StartTime < 0);
+ }
+
+ [Test]
+ public void TestPlaceAfterCurrentTimeDownwards()
+ {
+ AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time > 0", () => getNote().StartTime > 0);
+ }
+
+ private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject;
+
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
}
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 6855b99f28..77c871718b 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
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index d904474815..1c8116754f 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osuTK;
-using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps
{
@@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
TargetColumns = (int)Math.Max(1, roundedCircleSize);
- if (TargetColumns >= 10)
+ if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
TargetColumns /= 2;
Dual = true;
@@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
- public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap ConvertBeatmap(IBeatmap original)
{
@@ -239,8 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
- Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
- Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
});
}
else if (HitObject is IHasXPosition)
@@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
-
- ///
- /// Retrieves the sample info list at a point in time.
- ///
- /// The time to retrieve the sample info list from.
- ///
- private IList sampleInfoListAt(double time)
- {
- if (!(HitObject is IHasCurve curveData))
- return HitObject.Samples;
-
- double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
-
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.NodeSamples[index];
- }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 315ef96e49..d8d5b67c0e 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
else
{
- var holdNote = new HoldNote
+ newObject = new HoldNote
{
StartTime = startTime,
- Column = column,
Duration = endTime - startTime,
- Head = { Samples = sampleInfoListAt(startTime) },
- Tail = { Samples = sampleInfoListAt(endTime) }
+ Column = column,
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
};
-
- newObject = holdNote;
}
pattern.Add(newObject);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index b3be08e1f7..907bed0d65 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (holdNote)
{
- var hold = new HoldNote
+ newObject = new HoldNote
{
StartTime = HitObject.StartTime,
+ Duration = endTime - HitObject.StartTime,
Column = column,
- Duration = endTime - HitObject.StartTime
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
};
-
- if (hold.Head.Samples == null)
- hold.Head.Samples = new List();
-
- hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
-
- hold.Tail.Samples = HitObject.Samples;
-
- newObject = hold;
}
else
{
diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
new file mode 100644
index 0000000000..8d39e08b26
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
@@ -0,0 +1,64 @@
+// 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.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class DualStageVariantGenerator
+ {
+ private readonly int singleStageVariant;
+ private readonly InputKey[] stage1LeftKeys;
+ private readonly InputKey[] stage1RightKeys;
+ private readonly InputKey[] stage2LeftKeys;
+ private readonly InputKey[] stage2RightKeys;
+
+ public DualStageVariantGenerator(int singleStageVariant)
+ {
+ this.singleStageVariant = singleStageVariant;
+
+ // 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard.
+ if (singleStageVariant == 10)
+ {
+ stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V };
+ stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
+
+ stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B };
+ stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ else
+ {
+ stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R };
+ stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
+
+ stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G };
+ stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ }
+
+ public IEnumerable GenerateMappings()
+ {
+ var stage1Bindings = new VariantMappingGenerator
+ {
+ LeftKeys = stage1LeftKeys,
+ RightKeys = stage1RightKeys,
+ SpecialKey = InputKey.V,
+ SpecialAction = ManiaAction.Special1,
+ NormalActionStart = ManiaAction.Key1
+ }.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
+
+ var stage2Bindings = new VariantMappingGenerator
+ {
+ LeftKeys = stage2LeftKeys,
+ RightKeys = stage2RightKeys,
+ SpecialKey = InputKey.B,
+ SpecialAction = ManiaAction.Special2,
+ NormalActionStart = nextNormal
+ }.GenerateKeyBindingsFor(singleStageVariant, out _);
+
+ return stage1Bindings.Concat(stage2Bindings);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 7bbde400ea..b3dd392202 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
@@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
bodyPiece.Height = (bottomPosition - topPosition).Y;
}
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ base.OnMouseUp(e);
+ EndPlacement(true);
+ }
+
private double originalStartTime;
public override void UpdatePosition(Vector2 screenSpacePosition)
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index f1750f4a01..d569d68b59 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
new Container
{
RelativeSizeAxes = Axes.Both,
+ Masking = true,
BorderThickness = 1,
BorderColour = colours.Yellow,
Child = new Box
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 6ddf212266..400abb6380 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -50,16 +50,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return base.OnMouseDown(e);
HitObject.Column = Column.Index;
- BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
+ BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
return true;
}
- protected override void OnMouseUp(MouseUpEvent e)
- {
- EndPlacement(true);
- base.OnMouseUp(e);
- }
-
public override void UpdatePosition(Vector2 screenSpacePosition)
{
if (!PlacementActive)
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index 32c6a6fd07..2b7b383dbe 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -26,5 +27,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Width = SnappedWidth;
Position = SnappedMousePosition;
}
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ base.OnMouseDown(e);
+
+ // Place the note immediately.
+ EndPlacement(true);
+
+ return true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
index 292990fd7e..186fc4b15d 100644
--- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs
+++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
@@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania
[Description("Key 18")]
Key18,
+
+ [Description("Key 19")]
+ Key19,
+
+ [Description("Key 20")]
+ Key20,
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 2bd88fee90..a37aaa8cc4 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
+ ///
+ /// The maximum number of supported keys in a single stage.
+ ///
+ public const int MAX_STAGE_KEYS = 10;
+
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
@@ -202,6 +207,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
+ new ManiaModKey10(),
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3()),
@@ -250,9 +256,9 @@ namespace osu.Game.Rulesets.Mania
{
get
{
- for (int i = 1; i <= 9; i++)
+ for (int i = 1; i <= MAX_STAGE_KEYS; i++)
yield return (int)PlayfieldType.Single + i;
- for (int i = 2; i <= 18; i += 2)
+ for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2)
yield return (int)PlayfieldType.Dual + i;
}
}
@@ -262,73 +268,10 @@ namespace osu.Game.Rulesets.Mania
switch (getPlayfieldType(variant))
{
case PlayfieldType.Single:
- return new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.A,
- InputKey.S,
- InputKey.D,
- InputKey.F
- },
- RightKeys = new[]
- {
- InputKey.J,
- InputKey.K,
- InputKey.L,
- InputKey.Semicolon
- },
- SpecialKey = InputKey.Space,
- SpecialAction = ManiaAction.Special1,
- NormalActionStart = ManiaAction.Key1,
- }.GenerateKeyBindingsFor(variant, out _);
+ return new SingleStageVariantGenerator(variant).GenerateMappings();
case PlayfieldType.Dual:
- int keys = getDualStageKeyCount(variant);
-
- var stage1Bindings = new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.Q,
- InputKey.W,
- InputKey.E,
- InputKey.R,
- },
- RightKeys = new[]
- {
- InputKey.X,
- InputKey.C,
- InputKey.V,
- InputKey.B
- },
- SpecialKey = InputKey.S,
- SpecialAction = ManiaAction.Special1,
- NormalActionStart = ManiaAction.Key1
- }.GenerateKeyBindingsFor(keys, out var nextNormal);
-
- var stage2Bindings = new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.Number7,
- InputKey.Number8,
- InputKey.Number9,
- InputKey.Number0
- },
- RightKeys = new[]
- {
- InputKey.K,
- InputKey.L,
- InputKey.Semicolon,
- InputKey.Quote
- },
- SpecialKey = InputKey.I,
- SpecialAction = ManiaAction.Special2,
- NormalActionStart = nextNormal
- }.GenerateKeyBindingsFor(keys, out _);
-
- return stage1Bindings.Concat(stage2Bindings);
+ return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
}
return Array.Empty();
@@ -364,59 +307,6 @@ namespace osu.Game.Rulesets.Mania
{
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
-
- private class VariantMappingGenerator
- {
- ///
- /// All the s available to the left hand.
- ///
- public InputKey[] LeftKeys;
-
- ///
- /// All the s available to the right hand.
- ///
- public InputKey[] RightKeys;
-
- ///
- /// The for the special key.
- ///
- public InputKey SpecialKey;
-
- ///
- /// The at which the normal columns should begin.
- ///
- public ManiaAction NormalActionStart;
-
- ///
- /// The for the special column.
- ///
- public ManiaAction SpecialAction;
-
- ///
- /// Generates a list of s for a specific number of columns.
- ///
- /// The number of columns that need to be bound.
- /// The next to use for normal columns.
- /// The keybindings.
- public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
- {
- ManiaAction currentNormalAction = NormalActionStart;
-
- var bindings = new List();
-
- for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
- bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
-
- if (columns % 2 == 1)
- bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
-
- for (int i = 0; i < columns / 2; i++)
- bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
-
- nextNormalAction = currentNormalAction;
- return bindings;
- }
- }
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
new file mode 100644
index 0000000000..684370fc3d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.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.Mania.Mods
+{
+ public class ManiaModKey10 : ManiaKeyMod
+ {
+ public override int KeyCount => 10;
+ public override string Name => "Ten Keys";
+ public override string Acronym => "10K";
+ public override string Description => @"Play with ten keys.";
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index a9ef661aaa..2262bd2b7d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddRangeInternal(new[]
{
- bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece())
+ bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
+ {
+ RelativeSizeAxes = Axes.Both
+ })
{
RelativeSizeAxes = Axes.X
},
@@ -127,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
+ public override void PlaySamples()
+ {
+ // Samples are played by the head/tail notes.
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 5bfa07bd14..88888001b4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -24,6 +25,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
+ [Resolved(canBeNull: true)]
+ private ManiaPlayfield playfield { get; set; }
+
+ protected override float SamplePlaybackPosition
+ {
+ get
+ {
+ if (playfield == null)
+ return base.SamplePlaybackPosition;
+
+ return (float)HitObject.Column / playfield.TotalColumns;
+ }
+ }
+
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
index 0ee0a14df3..bc4a095395 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
@@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public DefaultBodyPiece()
{
- RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
AddLayout(subtractionCache);
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 049bf55f90..eea2c31260 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -28,7 +30,9 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
duration = value;
- Tail.StartTime = EndTime;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -38,8 +42,12 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.StartTime = value;
- Head.StartTime = value;
- Tail.StartTime = EndTime;
+
+ if (Head != null)
+ Head.StartTime = value;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.Column = value;
- Head.Column = value;
- Tail.Column = value;
+
+ if (Head != null)
+ Head.Column = value;
+
+ if (Tail != null)
+ Tail.Column = value;
}
}
+ public List> NodeSamples { get; set; }
+
///
/// The head note of the hold.
///
- public readonly Note Head = new Note();
+ public Note Head { get; private set; }
///
/// The tail note of the hold.
///
- public readonly TailNote Tail = new TailNote();
+ public TailNote Tail { get; private set; }
///
/// The time between ticks of this hold.
@@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks();
- AddNested(Head);
- AddNested(Tail);
+ AddNested(Head = new Note
+ {
+ StartTime = StartTime,
+ Column = Column,
+ Samples = getNodeSamples(0),
+ });
+
+ AddNested(Tail = new TailNote
+ {
+ StartTime = EndTime,
+ Column = Column,
+ Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ });
}
private void createTicks()
@@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ private IList getNodeSamples(int nodeIndex) =>
+ nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 995e1516cb..27bf50493d 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -5,11 +5,12 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
- public abstract class ManiaHitObject : HitObject, IHasColumn
+ public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{
public readonly Bindable ColumnBindable = new Bindable();
@@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
+
+ #region LegacyBeatmapEncoder
+
+ float IHasXPosition.X => Column;
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index 8c73c36e99..dbab54d1d0 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
@@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
-using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
while (activeColumns > 0)
{
- var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
+ bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction);
@@ -58,33 +58,87 @@ namespace osu.Game.Rulesets.Mania.Replays
int keys = 0;
- var specialColumns = new List();
-
- for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
- {
- if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
- specialColumns.Add(i);
- }
-
foreach (var action in Actions)
{
switch (action)
{
case ManiaAction.Special1:
- keys |= 1 << specialColumns[0];
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
break;
case ManiaAction.Special2:
- keys |= 1 << specialColumns[1];
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
break;
default:
- keys |= 1 << (action - ManiaAction.Key1);
+ // the index in lazer, which doesn't include special keys.
+ int nonSpecialKeyIndex = action - ManiaAction.Key1;
+
+ // the index inclusive of special keys.
+ int overallIndex = 0;
+
+ // iterate to find the index including special keys.
+ for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
+ {
+ // skip over special columns.
+ if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
+ continue;
+ // found a non-special column to use.
+ if (nonSpecialKeyIndex == 0)
+ break;
+ // found a non-special column but not ours.
+ nonSpecialKeyIndex--;
+ }
+
+ keys |= 1 << overallIndex;
break;
}
}
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
+
+ ///
+ /// Find the overall index (across all stages) for a specified special key.
+ ///
+ /// The beatmap.
+ /// The special key offset (0 is S1).
+ /// The overall index for the special column.
+ private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
+ {
+ for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
+ {
+ if (isColumnAtIndexSpecial(maniaBeatmap, i))
+ {
+ if (specialOffset == 0)
+ return i;
+
+ specialOffset--;
+ }
+ }
+
+ throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
+ }
+
+ ///
+ /// Check whether the column at an overall index (across all stages) is a special column.
+ ///
+ /// The beatmap.
+ /// The overall index to check.
+ private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
+ {
+ foreach (var stage in beatmap.Stages)
+ {
+ if (index >= stage.Columns)
+ {
+ index -= stage.Columns;
+ continue;
+ }
+
+ return stage.IsSpecialColumn(index);
+ }
+
+ throw new ArgumentException("Column index is too high.", nameof(index));
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
new file mode 100644
index 0000000000..2069329d9a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.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 System.Collections.Generic;
+using osu.Framework.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class SingleStageVariantGenerator
+ {
+ private readonly int variant;
+ private readonly InputKey[] leftKeys;
+ private readonly InputKey[] rightKeys;
+
+ public SingleStageVariantGenerator(int variant)
+ {
+ this.variant = variant;
+
+ // 10K is special because it expands towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard.
+ if (variant == 10)
+ {
+ leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V };
+ rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ else
+ {
+ leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F };
+ rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ }
+
+ public IEnumerable GenerateMappings() => new VariantMappingGenerator
+ {
+ LeftKeys = leftKeys,
+ RightKeys = rightKeys,
+ SpecialKey = InputKey.Space,
+ SpecialAction = ManiaAction.Special1,
+ NormalActionStart = ManiaAction.Key1,
+ }.GenerateKeyBindingsFor(variant, out _);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
index 6504321bb2..1a097405ac 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -67,6 +67,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
RelativeSizeAxes = Axes.Y,
Width = leftLineWidth,
+ Scale = new Vector2(0.740f, 1),
Colour = lineColour,
Alpha = hasLeftLine ? 1 : 0
},
@@ -76,6 +77,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = rightLineWidth,
+ Scale = new Vector2(0.740f, 1),
Colour = lineColour,
Alpha = hasRightLine ? 1 : 0
},
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index c2eb48b774..2dec468654 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -14,6 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
+ [Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
private readonly List stages = new List();
diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
new file mode 100644
index 0000000000..878d1088a6
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.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.Framework.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class VariantMappingGenerator
+ {
+ ///
+ /// All the s available to the left hand.
+ ///
+ public InputKey[] LeftKeys;
+
+ ///
+ /// All the s available to the right hand.
+ ///
+ public InputKey[] RightKeys;
+
+ ///
+ /// The for the special key.
+ ///
+ public InputKey SpecialKey;
+
+ ///
+ /// The at which the normal columns should begin.
+ ///
+ public ManiaAction NormalActionStart;
+
+ ///
+ /// The for the special column.
+ ///
+ public ManiaAction SpecialAction;
+
+ ///
+ /// Generates a list of s for a specific number of columns.
+ ///
+ /// The number of columns that need to be bound.
+ /// The next to use for normal columns.
+ /// The keybindings.
+ public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
+ {
+ ManiaAction currentNormalAction = NormalActionStart;
+
+ var bindings = new List();
+
+ for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
+ bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
+
+ if (columns % 2 == 1)
+ bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
+
+ for (int i = 0; i < columns / 2; i++)
+ bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
+
+ nextNormalAction = currentNormalAction;
+ return bindings;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
new file mode 100644
index 0000000000..8bd3d3c7cc
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -0,0 +1,106 @@
+// 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.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModHidden : ModTestScene
+ {
+ public TestSceneOsuModHidden()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ EndTime = 1000,
+ },
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1200,
+ EndTime = 2200,
+ },
+ new HitCircle
+ {
+ Position = new Vector2(300, 192),
+ StartTime = 3200,
+ },
+ new HitCircle
+ {
+ Position = new Vector2(384, 192),
+ StartTime = 4200,
+ }
+ }
+ },
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ EndTime = 1000,
+ },
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1200,
+ EndTime = 2200,
+ },
+ new Slider
+ {
+ StartTime = 3200,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ new Slider
+ {
+ StartTime = 5200,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ }
+ }
+ },
+ PassCondition = checkSomeHit
+ });
+
+ private bool checkSomeHit()
+ {
+ return Player.ScoreProcessor.JudgedHits >= 4;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
new file mode 100644
index 0000000000..a6c3be7e5a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -0,0 +1,447 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
+ {
+ private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
+ private const double late_miss_window = 500; // time after +500 is considered a miss
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAtFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], 0);
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], 100);
+ }
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
+ }
+
+ ///
+ /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
+ ///
+ [Test]
+ public void TestMissSliderHeadAndHitAllSliderTicks()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ }
+
+ ///
+ /// Tests clicking hitting future slider ticks before a circle.
+ ///
+ [Test]
+ public void TestHitSliderTicksBeforeCircle()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ }
+
+ ///
+ /// Tests clicking a future circle before a spinner.
+ ///
+ [Test]
+ public void TestHitCircleBeforeSpinner()
+ {
+ const double time_spinner = 1500;
+ const double time_circle = 1800;
+ Vector2 positionCircle = Vector2.Zero;
+
+ var hitObjects = new List
+ {
+ new TestSpinner
+ {
+ StartTime = time_spinner,
+ Position = new Vector2(256, 192),
+ EndTime = time_spinner + 1000,
+ },
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ [Test]
+ public void TestHitSliderHeadBeforeHitCircle()
+ {
+ const double time_circle = 1000;
+ const double time_slider = 1200;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementAssert(string name, Func hitObject, HitResult result)
+ {
+ AddAssert($"{name} judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+ Ruleset = new OsuRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private class TestHitCircle : HitCircle
+ {
+ protected override HitWindows CreateHitWindows() => new TestHitWindows();
+ }
+
+ private class TestSlider : Slider
+ {
+ public TestSlider()
+ {
+ DefaultsApplied += () =>
+ {
+ HeadCircle.HitWindows = new TestHitWindows();
+ TailCircle.HitWindows = new TestHitWindows();
+
+ HeadCircle.HitWindows.SetDifficulty(0);
+ TailCircle.HitWindows.SetDifficulty(0);
+ };
+ }
+ }
+
+ private class TestSpinner : Spinner
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ SpinsRequired = 1;
+ }
+ }
+
+ private class TestHitWindows : HitWindows
+ {
+ private static readonly DifficultyRange[] ranges =
+ {
+ new DifficultyRange(HitResult.Great, 500, 500, 500),
+ new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
+ };
+
+ public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
+
+ protected override DifficultyRange[] GetRanges() => ranges;
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs
new file mode 100644
index 0000000000..cbe14ff4d2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs
@@ -0,0 +1,64 @@
+// 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 Humanizer;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestScenePathControlPointVisualiser : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(StringHumanizeExtensions),
+ typeof(PathControlPointPiece),
+ typeof(PathControlPointConnectionPiece)
+ };
+
+ private Slider slider;
+ private PathControlPointVisualiser visualiser;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ slider = new Slider();
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ });
+
+ [Test]
+ public void TestAddOverlappingControlPoints()
+ {
+ createVisualiser(true);
+
+ addControlPointStep(new Vector2(200));
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(300));
+ addControlPointStep(new Vector2(500, 300));
+
+ AddAssert("last connection displayed", () =>
+ {
+ var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300));
+ return lastConnection.DrawWidth > 50;
+ });
+ }
+
+ private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+
+ private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 67e1b77770..b0c2e56c3e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double time_slider_end = 4000;
private List judgementResults;
- private bool allJudgedFired;
///
/// Scenario:
@@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (currentPlayer == p) judgementResults.Add(result);
};
- p.ScoreProcessor.AllJudged += () =>
- {
- if (currentPlayer == p) allJudgedFired = true;
- };
};
LoadScreen(currentPlayer = p);
- allJudgedFired = false;
judgementResults = new List();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
- AddUntilStep("Wait for all judged", () => allJudgedFired);
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
index 0522260150..fe9973f4d8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
@@ -1,18 +1,302 @@
// 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.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
{
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ HitObjectContainer.Clear();
+ ResetPlacement();
+ });
+
+ [Test]
+ public void TestBeginPlacementWithoutFinishing()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ assertPlaced(false);
+ }
+
+ [Test]
+ public void TestPlaceWithoutMovingMouse()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertLength(0);
+ assertControlPointType(0, PathType.Linear);
+ }
+
+ [Test]
+ public void TestPlaceWithMouseMovement()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 200));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertLength(200);
+ assertControlPointCount(2);
+ assertControlPointType(0, PathType.Linear);
+ }
+
+ [Test]
+ public void TestPlaceNormalControlPoint()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestPlaceTwoNormalControlPoints()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(4);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointPosition(2, new Vector2(100, 100));
+ assertControlPointType(0, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestPlaceSegmentControlPoint()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointType(0, PathType.Linear);
+ assertControlPointType(1, PathType.Linear);
+ }
+
+ [Test]
+ public void TestMoveToPerfectCurveThenPlaceLinear()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(2);
+ assertControlPointType(0, PathType.Linear);
+ assertLength(100);
+ }
+
+ [Test]
+ public void TestMoveToBezierThenPlacePerfectCurve()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 300));
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointType(0, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestMoveToFourthOrderBezierThenPlaceThirdOrderBezier()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400));
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(4);
+ assertControlPointType(0, PathType.Bezier);
+ }
+
+ [Test]
+ public void TestPlaceLinearSegmentThenPlaceLinearSegment()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(3);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointPosition(2, new Vector2(100));
+ assertControlPointType(0, PathType.Linear);
+ assertControlPointType(1, PathType.Linear);
+ }
+
+ [Test]
+ public void TestPlaceLinearSegmentThenPlacePerfectCurveSegment()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(4);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointPosition(2, new Vector2(100));
+ assertControlPointType(0, PathType.Linear);
+ assertControlPointType(1, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestPlacePerfectCurveSegmentThenPlacePerfectCurveSegment()
+ {
+ addMovementStep(new Vector2(200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 200));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(300, 300));
+ addClickStep(MouseButton.Left);
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400, 300));
+ addClickStep(MouseButton.Left);
+
+ addMovementStep(new Vector2(400));
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertControlPointCount(5);
+ assertControlPointPosition(1, new Vector2(100, 0));
+ assertControlPointPosition(2, new Vector2(100));
+ assertControlPointPosition(3, new Vector2(200, 100));
+ assertControlPointPosition(4, new Vector2(200));
+ assertControlPointType(0, PathType.PerfectCurve);
+ assertControlPointType(2, PathType.PerfectCurve);
+ }
+
+ [Test]
+ public void TestBeginPlacementWithoutReleasingMouse()
+ {
+ addMovementStep(new Vector2(200));
+ AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
+
+ addMovementStep(new Vector2(400, 200));
+ AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
+
+ addClickStep(MouseButton.Right);
+
+ assertPlaced(true);
+ assertLength(200);
+ assertControlPointCount(2);
+ assertControlPointType(0, PathType.Linear);
+ }
+
+ private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
+
+ private void addClickStep(MouseButton button)
+ {
+ AddStep($"press {button}", () => InputManager.PressButton(button));
+ AddStep($"release {button}", () => InputManager.ReleaseButton(button));
+ }
+
+ private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected);
+
+ private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1));
+
+ private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
+
+ private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type);
+
+ private void assertControlPointPosition(int index, Vector2 position) =>
+ AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1));
+
+ private Slider getSlider() => HitObjectContainer.Count > 0 ? (Slider)((DrawableSlider)HitObjectContainer[0]).HitObject : null;
+
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
}
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 217707b180..2fcfa1deb7 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
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index 407f5f540e..dad199715e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{
@@ -28,16 +29,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
circlePiece.UpdateFrom(HitObject);
}
- protected override bool OnClick(ClickEvent e)
+ protected override bool OnMouseDown(MouseDownEvent e)
{
- EndPlacement(true);
- return true;
+ if (e.Button == MouseButton.Left)
+ {
+ EndPlacement(true);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
- BeginPlacement();
- HitObject.Position = ToLocalSpace(screenSpacePosition);
- }
+ public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
index 0fc441fec6..ba1d35c35c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -16,22 +16,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
public class PathControlPointConnectionPiece : CompositeDrawable
{
- public PathControlPoint ControlPoint;
+ public readonly PathControlPoint ControlPoint;
private readonly Path path;
private readonly Slider slider;
+ private readonly int controlPointIndex;
private IBindable sliderPosition;
private IBindable pathVersion;
- public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint)
+ public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
- ControlPoint = controlPoint;
+ this.controlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
+ ControlPoint = slider.Path.ControlPoints[controlPointIndex];
+
InternalChild = path = new SmoothPath
{
Anchor = Anchor.Centre,
@@ -61,13 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
- int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1;
-
- if (index == 0 || index == slider.Path.ControlPoints.Count)
+ int nextIndex = controlPointIndex + 1;
+ if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;
path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
+ path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index af4da5e853..d0c1eb5317 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -12,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -26,13 +28,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action RequestSelection;
public readonly BindableBool IsSelected = new BindableBool();
-
public readonly PathControlPoint ControlPoint;
private readonly Slider slider;
private readonly Container marker;
private readonly Drawable markerRing;
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
@@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
+ controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
+
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -137,7 +143,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
- protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (RequestSelection == null)
+ return false;
+
+ if (e.Button == MouseButton.Left)
+ {
+ changeHandler?.BeginChange();
+ return true;
+ }
+
+ return false;
+ }
protected override void OnDrag(DragEvent e)
{
@@ -158,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
ControlPoint.Position.Value += e.Delta;
}
+ protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
+
///
/// Updates the state of the circular control point marker.
///
@@ -168,8 +188,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+
if (IsHovered || IsSelected.Value)
- colour = Color4.White;
+ colour = colour.Lighten(1);
+
marker.Colour = colour;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index e293eba9d7..f6354bc612 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
@@ -24,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
internal readonly Container Pieces;
+ internal readonly Container Connections;
- private readonly Container connections;
-
+ private readonly IBindableList controlPoints = new BindableList();
private readonly Slider slider;
-
private readonly bool allowSelection;
private InputManager inputManager;
- private IBindableList controlPoints;
-
public Action> RemoveControlPointsRequested;
public PathControlPointVisualiser(Slider slider, bool allowSelection)
@@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[]
{
- connections = new Container { RelativeSizeAxes = Axes.Both },
+ Connections = new Container { RelativeSizeAxes = Axes.Both },
Pieces = new Container { RelativeSizeAxes = Axes.Both }
};
}
@@ -57,33 +55,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
inputManager = GetContainingInputManager();
- controlPoints = slider.Path.ControlPoints.GetBoundCopy();
- controlPoints.ItemsAdded += addControlPoints;
- controlPoints.ItemsRemoved += removeControlPoints;
-
- addControlPoints(controlPoints);
+ controlPoints.CollectionChanged += onControlPointsChanged;
+ controlPoints.BindTo(slider.Path.ControlPoints);
}
- private void addControlPoints(IEnumerable controlPoints)
+ private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- foreach (var point in controlPoints)
+ switch (e.Action)
{
- Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
- {
- if (allowSelection)
- d.RequestSelection = selectPiece;
- }));
+ case NotifyCollectionChangedAction.Add:
+ for (int i = 0; i < e.NewItems.Count; i++)
+ {
+ var point = (PathControlPoint)e.NewItems[i];
- connections.Add(new PathControlPointConnectionPiece(slider, point));
- }
- }
+ Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
+ {
+ if (allowSelection)
+ d.RequestSelection = selectPiece;
+ }));
- private void removeControlPoints(IEnumerable controlPoints)
- {
- foreach (var point in controlPoints)
- {
- Pieces.RemoveAll(p => p.ControlPoint == point);
- connections.RemoveAll(c => c.ControlPoint == point);
+ Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var point in e.OldItems.Cast())
+ {
+ Pieces.RemoveAll(p => p.ControlPoint == point);
+ Connections.RemoveAll(c => c.ControlPoint == point);
+ }
+
+ break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index a780653796..ac30f5a762 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
+using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
+ private PathControlPointVisualiser controlPointVisualiser;
private InputManager inputManager;
@@ -51,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
- new PathControlPointVisualiser(HitObject, false)
+ controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
};
setState(PlacementState.Initial);
@@ -73,17 +77,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
- ensureCursor();
-
- // The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
- // is used instead since snapping control points doesn't make much sense
- cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ updateCursor();
break;
}
}
- protected override bool OnClick(ClickEvent e)
+ protected override bool OnMouseDown(MouseDownEvent e)
{
+ if (e.Button != MouseButton.Left)
+ return base.OnMouseDown(e);
+
switch (state)
{
case PlacementState.Initial:
@@ -91,14 +94,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
- switch (e.Button)
+ if (canPlaceNewControlPoint(out var lastPoint))
{
- case MouseButton.Left:
- ensureCursor();
+ // Place a new point by detatching the current cursor.
+ updateCursor();
+ cursor = null;
+ }
+ else
+ {
+ // Transform the last point into a new segment.
+ Debug.Assert(lastPoint != null);
- // Detatch the cursor
- cursor = null;
- break;
+ segmentStart = lastPoint;
+ segmentStart.Type.Value = PathType.Linear;
+
+ currentSegmentLength = 1;
}
break;
@@ -114,16 +124,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnMouseUp(e);
}
- protected override bool OnDoubleClick(DoubleClickEvent e)
- {
- // Todo: This should all not occur on double click, but rather if the previous control point is hovered.
- segmentStart = HitObject.Path.ControlPoints[^1];
- segmentStart.Type.Value = PathType.Linear;
-
- currentSegmentLength = 1;
- return true;
- }
-
private void beginCurve()
{
BeginPlacement(commitStart: true);
@@ -161,17 +161,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
- private void ensureCursor()
+ private void updateCursor()
{
- if (cursor == null)
+ if (canPlaceNewControlPoint(out _))
{
- HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
- currentSegmentLength++;
+ // The cursor does not overlap a previous control point, so it can be added if not already existing.
+ if (cursor == null)
+ {
+ HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
+ // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
+ currentSegmentLength++;
+ updatePathType();
+ }
+
+ // Update the cursor position.
+ cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ }
+ else if (cursor != null)
+ {
+ // The cursor overlaps a previous control point, so it's removed.
+ HitObject.Path.ControlPoints.Remove(cursor);
+ cursor = null;
+
+ // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear).
+ currentSegmentLength--;
updatePathType();
}
}
+ ///
+ /// Whether a new control point can be placed at the current mouse position.
+ ///
+ /// The last-placed control point. May be null, but is not null if false is returned.
+ /// Whether a new control point can be placed at the current position.
+ private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
+ {
+ // We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
+ var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
+ var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
+
+ lastPoint = last;
+ return lastPiece?.IsHovered != true;
+ }
+
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 001100d3ce..b7074b7ee5 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -92,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int? placementControlPointIndex;
- protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null;
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (placementControlPointIndex != null)
+ {
+ changeHandler?.BeginChange();
+ return true;
+ }
+
+ return false;
+ }
protected override void OnDrag(DragEvent e)
{
@@ -103,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnDragEnd(DragEndEvent e)
{
- placementControlPointIndex = null;
+ if (placementControlPointIndex != null)
+ {
+ placementControlPointIndex = null;
+ changeHandler?.EndChange();
+ }
}
private BindableList controlPoints => HitObject.Path.ControlPoints;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 91a4e049e3..fdba03f260 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
+ protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
+
public override void ApplyToDrawableHitObjects(IEnumerable drawables)
{
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 9b0759d9d2..7b1941b7f9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
@@ -33,15 +34,30 @@ namespace osu.Game.Rulesets.Osu.Mods
private ReplayState state;
private double lastStateChangeTime;
+ private bool hasReplay;
+
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ if (osuInputManager.ReplayInputHandler != null)
+ {
+ hasReplay = true;
+ return;
+ }
+
osuInputManager.AllowUserPresses = false;
}
public void Update(Playfield playfield)
{
+ if (hasReplay)
+ return;
+
bool requiresHold = false;
bool requiresHit = false;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 5202327245..d73ad888f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset);
- if (result == HitResult.None)
+ if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
return;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index a677cb6a72..8308c0c576 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -1,11 +1,14 @@
// 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.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -16,6 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
public override bool HandlePositionalInput => true;
+ protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
+
+ ///
+ /// Whether this can be hit.
+ /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
+ ///
+ public Func CheckHittable;
+
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@@ -54,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ ///
+ /// Causes this to get missed, disregarding all conditions in implementations of .
+ ///
+ public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9b6f39d91d..72502c02cd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -124,8 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case SliderTailCircle tail:
return new DrawableSliderTail(slider, tail);
- case HitCircle head:
- return new DrawableSliderHead(slider, head) { OnShake = Shake };
+ case SliderHeadCircle head:
+ return new DrawableSliderHead(slider, head)
+ {
+ OnShake = Shake,
+ CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true
+ };
case SliderTick tick:
return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index a360071f26..04f563eeec 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Slider slider;
- public DrawableSliderHead(Slider slider, HitCircle h)
+ public DrawableSliderHead(Slider slider, SliderHeadCircle h)
: base(h)
{
this.slider = slider;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index db1f46d8e2..e5d6c20738 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Head:
- AddNested(HeadCircle = new SliderCircle
+ AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
new file mode 100644
index 0000000000..f6d46aeef5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
@@ -0,0 +1,9 @@
+// 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.Objects
+{
+ public class SliderHeadCircle : HitCircle
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
new file mode 100644
index 0000000000..8e4f81347d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
@@ -0,0 +1,106 @@
+// 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 osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Ensures that s are hit in-order. Affectionately known as "note lock".
+ /// If a is hit out of order:
+ ///
+ /// - The hit is blocked if it occurred earlier than the previous 's start time.
+ /// - The hit causes all previous s to missed otherwise.
+ ///
+ ///
+ public class OrderedHitPolicy
+ {
+ private readonly HitObjectContainer hitObjectContainer;
+
+ public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+ }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ public bool IsHittable(DrawableHitObject hitObject, double time)
+ {
+ DrawableHitObject blockingObject = null;
+
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
+ {
+ if (hitObjectCanBlockFutureHits(obj))
+ blockingObject = obj;
+ }
+
+ // If there is no previous hitobject, allow the hit.
+ if (blockingObject == null)
+ return true;
+
+ // A hit is allowed if:
+ // 1. The last blocking hitobject has been judged.
+ // 2. The current time is after the last hitobject's start time.
+ // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
+ return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
+ }
+
+ ///
+ /// Handles a being hit to potentially miss all earlier s.
+ ///
+ /// The that was hit.
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
+ if (!hitObjectCanBlockFutureHits(hitObject))
+ return;
+
+ if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
+ throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
+
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
+ {
+ if (obj.Judged)
+ continue;
+
+ if (hitObjectCanBlockFutureHits(obj))
+ ((DrawableOsuHitObject)obj).MissForcefully();
+ }
+ }
+
+ ///
+ /// Whether a blocks hits on future s until its start time is reached.
+ ///
+ /// The to test.
+ private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
+ => hitObject is DrawableHitCircle;
+
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.StartTime >= targetTime)
+ yield break;
+
+ yield return obj;
+
+ foreach (var nestedObj in obj.NestedHitObjects)
+ {
+ if (nestedObj.HitObject.StartTime >= targetTime)
+ break;
+
+ yield return nestedObj;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 6d1ea4bbfc..4b1a2ce43c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
+ private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -51,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI
Depth = -1,
},
};
+
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
}
public override void Add(DrawableHitObject h)
@@ -64,7 +67,10 @@ namespace osu.Game.Rulesets.Osu.UI
base.Add(h);
- followPoints.AddFollowPoints((DrawableOsuHitObject)h);
+ DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
+ osuHitObject.CheckHittable = hitPolicy.IsHittable;
+
+ followPoints.AddFollowPoints(osuHitObject);
}
public override bool Remove(DrawableHitObject h)
@@ -79,6 +85,9 @@ namespace osu.Game.Rulesets.Osu.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
+ // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
+ hitPolicy.HandleHit(judgedObject);
+
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
new file mode 100644
index 0000000000..1db07b3244
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ internal class DrawableTestHit : DrawableTaikoHitObject
+ {
+ private readonly HitResult type;
+
+ public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
+ : base(hit)
+ {
+ this.type = type;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Result.Type = type;
+ }
+
+ public override bool OnPressed(TaikoAction action) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png
new file mode 100644
index 0000000000..72ef665478
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png
new file mode 100644
index 0000000000..5ca8a40d88
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png
new file mode 100644
index 0000000000..3e44f33095
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png
new file mode 100644
index 0000000000..440e5b55e5
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png
new file mode 100644
index 0000000000..043bfbfae1
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png
new file mode 100644
index 0000000000..4233d9bb6e
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png
new file mode 100755
index 0000000000..5aba688756
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png
new file mode 100644
index 0000000000..63504dd52d
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png
new file mode 100644
index 0000000000..490c196fba
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png
new file mode 100644
index 0000000000..99cd589a10
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png
new file mode 100644
index 0000000000..26eec54d07
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png
new file mode 100644
index 0000000000..272c6bcaf7
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png
new file mode 100644
index 0000000000..e49e82a71f
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png
new file mode 100644
index 0000000000..56d6d34c1a
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png
new file mode 100644
index 0000000000..5d8b60da9e
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs
similarity index 92%
rename from osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs
rename to osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs
index 6db2a6907f..161154b1a7 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Taiko.Tests
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public abstract class TaikoSkinnableTestScene : SkinnableTestScene
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
new file mode 100644
index 0000000000..70493aa69a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -0,0 +1,111 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableBarLine : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DrawableBarLine),
+ typeof(LegacyBarLine),
+ typeof(BarLine),
+ }).ToList();
+
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
+ {
+ Direction = { Value = ScrollingDirection.Left },
+ TimeRange = { Value = 5000 },
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Bar line", () => SetContents(() =>
+ {
+ ScrollingHitObjectContainer hoc;
+
+ var cont = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.8f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new TaikoPlayfield(new ControlPointInfo()),
+ hoc = new ScrollingHitObjectContainer()
+ }
+ };
+
+ hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ return cont;
+ }));
+
+ AddStep("Bar line (major)", () => SetContents(() =>
+ {
+ ScrollingHitObjectContainer hoc;
+
+ var cont = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.8f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new TaikoPlayfield(new ControlPointInfo()),
+ hoc = new ScrollingHitObjectContainer()
+ }
+ };
+
+ hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
+ return cont;
+ }));
+ }
+
+ private BarLine createBarLineAtCurrentTime(bool major = false)
+ {
+ var barline = new BarLine
+ {
+ Major = major,
+ StartTime = Time.Current + 2000,
+ };
+
+ var cpi = new ControlPointInfo();
+ cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ barline.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ return barline;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
new file mode 100644
index 0000000000..554894bf68
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -0,0 +1,86 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableDrumRoll : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DrawableDrumRoll),
+ typeof(DrawableDrumRollTick),
+ typeof(LegacyDrumRoll),
+ }).ToList();
+
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
+ {
+ Direction = { Value = ScrollingDirection.Left },
+ TimeRange = { Value = 5000 },
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Drum roll", () => SetContents(() =>
+ {
+ var hoc = new ScrollingHitObjectContainer();
+
+ hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ });
+
+ return hoc;
+ }));
+
+ AddStep("Drum roll (strong)", () => SetContents(() =>
+ {
+ var hoc = new ScrollingHitObjectContainer();
+
+ hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ });
+
+ return hoc;
+ }));
+ }
+
+ private DrumRoll createDrumRollAtCurrentTime(bool strong = false)
+ {
+ var drumroll = new DrumRoll
+ {
+ IsStrong = strong,
+ StartTime = Time.Current + 1000,
+ Duration = 4000,
+ };
+
+ var cpi = new ControlPointInfo();
+ cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ drumroll.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ return drumroll;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
new file mode 100644
index 0000000000..6a3c98a514
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -0,0 +1,69 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableHit : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DrawableHit),
+ typeof(LegacyHit),
+ typeof(LegacyCirclePiece),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+
+ private Hit createHitAtCurrentTime(bool strong = false)
+ {
+ var hit = new Hit
+ {
+ IsStrong = strong,
+ StartTime = Time.Current + 3000,
+ };
+
+ hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ return hit;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
new file mode 100644
index 0000000000..791c438c94
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -0,0 +1,58 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Rulesets.Taiko.UI;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneHitExplosion : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(HitExplosion),
+ typeof(LegacyHitExplosion),
+ typeof(DefaultHitExplosion),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
+ AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
+ AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
+ }
+
+ private Drawable getContentFor(HitResult type)
+ {
+ DrawableTaikoHitObject hit;
+
+ return new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ hit = createHit(type),
+ new HitExplosion(hit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
+ };
+ }
+
+ private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
similarity index 96%
rename from osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs
rename to osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
index 1928e9f66f..412027ca61 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
@@ -6,14 +6,14 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
-namespace osu.Game.Rulesets.Taiko.Tests
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneInputDrum : TaikoSkinnableTestScene
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
new file mode 100644
index 0000000000..16b3c036a3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -0,0 +1,46 @@
+// 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.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ public class TestSceneTaikoPlayfield : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(TaikoHitTarget),
+ typeof(TaikoLegacyHitTarget),
+ typeof(PlayfieldBackgroundRight),
+ }).ToList();
+
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
+ {
+ Direction = { Value = ScrollingDirection.Left },
+ TimeRange = { Value = 5000 },
+ };
+
+ public TestSceneTaikoPlayfield()
+ {
+ AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo())
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ }));
+
+ AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
similarity index 93%
rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
rename to osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index 0d9e813c60..44452d70c1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -24,9 +24,9 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
- public class TestSceneTaikoPlayfield : OsuTestScene
+ public class TestSceneHits : OsuTestScene
{
- private const double default_duration = 1000;
+ private const double default_duration = 3000;
private const float scroll_time = 1000;
protected override double TimePerAction => default_duration * 2;
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Miss :(", addMissJudgement);
AddStep("DrumRoll", () => addDrumRoll(false));
AddStep("Strong DrumRoll", () => addDrumRoll(true));
+ AddStep("Kiai DrumRoll", () => addDrumRoll(true, kiai: true));
AddStep("Swell", () => addSwell());
AddStep("Centre", () => addCentreHit(false));
AddStep("Strong Centre", () => addCentreHit(true));
@@ -148,6 +149,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ Add(h);
+
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
}
@@ -163,6 +166,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ Add(h);
+
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
@@ -192,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
drawableRuleset.Playfield.Add(new DrawableSwell(swell));
}
- private void addDrumRoll(bool strong, double duration = default_duration)
+ private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
{
addBarLine(true);
addBarLine(true, scroll_time + duration);
@@ -202,9 +207,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
+ TickRate = 8,
};
- d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ var cpi = new ControlPointInfo();
+ cpi.Add(-10000, new EffectControlPoint { KiaiMode = kiai });
+
+ d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
@@ -219,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableCentreHit(h));
+ drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private void addRimHit(bool strong)
@@ -232,7 +241,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableRimHit(h));
+ drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
@@ -244,13 +253,5 @@ namespace osu.Game.Rulesets.Taiko.Tests
public override bool OnPressed(TaikoAction action) => false;
}
-
- private class DrawableTestHit : DrawableHitObject
- {
- public DrawableTestHit(TaikoHitObject hitObject)
- : base(hitObject)
- {
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
index 303f0163b1..923e28a45e 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[Test]
public void TestZeroTickTimeOffsets()
{
- AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted);
+ AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value);
AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
}
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 f6054a5d6f..28b8476a22 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
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 695ada3a00..caf645d5a2 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
///
- private const float legacy_velocity_multiplier = 1.4f;
+ public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
///
/// Because swells are easier in taiko than spinners are in osu!,
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
- original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
+ original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
Beatmap converted = base.ConvertBeatmap(original);
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
- double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
+ double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
index e9caabbcc8..1e08e921a6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osuTK;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
/// The visual line tracker.
///
- protected Box Tracker;
+ protected SkinnableDrawable Line;
///
/// The bar line.
@@ -45,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
RelativeSizeAxes = Axes.Y;
Width = tracker_width;
- AddInternal(Tracker = new Box
+ AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ EdgeSmoothness = new Vector2(0.5f, 0),
+ })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- EdgeSmoothness = new Vector2(0.5f, 0),
- Alpha = 0.75f
+ Alpha = 0.75f,
});
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
index 4d3a1a3f8a..62aab3524b 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
});
- Tracker.Alpha = 1f;
+ Line.Alpha = 1f;
}
protected override void LoadComplete()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
deleted file mode 100644
index 4979135f50..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables
-{
- public class DrawableCentreHit : DrawableHit
- {
- public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre };
-
- public DrawableCentreHit(Hit hit)
- : base(hit)
- {
- MainPiece.Add(new CentreHitSymbolPiece());
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- MainPiece.AccentColour = colours.PinkDarker;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 5806c90115..5e731e5ad6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -29,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private int rollingHits;
- private readonly Container tickContainer;
+ private Container tickContainer;
private Color4 colourIdle;
private Color4 colourEngaged;
@@ -38,14 +40,20 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
- MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- MainPiece.AccentColour = colourIdle = colours.YellowDark;
+ colourIdle = colours.YellowDark;
colourEngaged = colours.YellowDarker;
+
+ updateColour();
+
+ Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
+
+ if (MainPiece.Drawable is IHasAccentColour accentMain)
+ accentMain.AccentColour = colourIdle;
}
protected override void LoadComplete()
@@ -84,7 +92,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
+ protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody),
+ _ => new ElongatedCirclePiece());
public override bool OnPressed(TaikoAction action) => false;
@@ -100,8 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
- Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
- MainPiece.FadeAccent(newColour, 100);
+ updateColour();
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@@ -113,8 +121,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return;
int countHit = NestedHitObjects.Count(o => o.IsHit);
+
if (countHit >= HitObject.RequiredGoodHits)
+ {
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good);
+ }
else
ApplyResult(r => r.Type = HitResult.Miss);
}
@@ -130,8 +141,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
+ protected override void Update()
+ {
+ base.Update();
+
+ OriginPosition = new Vector2(DrawHeight);
+ Content.X = DrawHeight / 2;
+ }
+
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
+ private void updateColour()
+ {
+ Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
+ (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100);
+ }
+
private class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 25b6141a0e..62405cf047 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -6,23 +6,28 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRollTick : DrawableTaikoHitObject
{
+ ///
+ /// The hit type corresponding to the that the user pressed to hit this .
+ ///
+ public HitType JudgementType;
+
public DrawableDrumRollTick(DrumRollTick tick)
: base(tick)
{
FillMode = FillMode.Fit;
}
- public override bool DisplayResult => false;
-
- protected override TaikoPiece CreateMainPiece() => new TickPiece
- {
- Filled = HitObject.FirstTick
- };
+ protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
+ _ => new TickPiece
+ {
+ Filled = HitObject.FirstTick
+ });
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
@@ -49,7 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
- public override bool OnPressed(TaikoAction action) => UpdateResult(true);
+ public override bool OnPressed(TaikoAction action)
+ {
+ JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
+ return UpdateResult(true);
+ }
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs
new file mode 100644
index 0000000000..460e760629
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace osu.Game.Rulesets.Taiko.Objects.Drawables
+{
+ ///
+ /// A hit used specifically for drum rolls, where spawning flying hits is required.
+ ///
+ public class DrawableFlyingHit : DrawableHit
+ {
+ public DrawableFlyingHit(DrawableDrumRollTick drumRollTick)
+ : base(new IgnoreHit
+ {
+ StartTime = drumRollTick.HitObject.StartTime + drumRollTick.Result.TimeOffset,
+ IsStrong = drumRollTick.HitObject.IsStrong,
+ Type = drumRollTick.JudgementType
+ })
+ {
+ HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 85dfc8d5e0..d2671eadda 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -8,31 +8,45 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
- public abstract class DrawableHit : DrawableTaikoHitObject
+ public class DrawableHit : DrawableTaikoHitObject
{
///
/// A list of keys which can result in hits for this HitObject.
///
- public abstract TaikoAction[] HitActions { get; }
+ public TaikoAction[] HitActions { get; }
///
/// The action that caused this to be hit.
///
- public TaikoAction? HitAction { get; private set; }
+ public TaikoAction? HitAction
+ {
+ get;
+ private set;
+ }
private bool validActionPressed;
private bool pressHandledThisFrame;
- protected DrawableHit(Hit hit)
+ public DrawableHit(Hit hit)
: base(hit)
{
FillMode = FillMode.Fit;
+
+ HitActions =
+ HitObject.Type == HitType.Centre
+ ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
+ : new[] { TaikoAction.LeftRim, TaikoAction.RightRim };
}
+ protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre
+ ? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
+ : new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@@ -58,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
if (pressHandledThisFrame)
return true;
-
if (Judged)
return false;
@@ -66,14 +79,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Only count this as handled if the new judgement is a hit
var result = UpdateResult(true);
-
if (IsHit)
HitAction = action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true;
-
return result;
}
@@ -81,7 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
if (action == HitAction)
HitAction = null;
-
base.OnReleased(action);
}
@@ -92,8 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
-
- Size = BaseSize * Parent.RelativeChildSize;
}
protected override void UpdateStateTransforms(ArmedState state)
@@ -116,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// If we're far enough away from the left stage, we should bring outselves in front of it
ProxyContent();
- var flash = (MainPiece as CirclePiece)?.FlashBox;
+ var flash = (MainPiece.Drawable as CirclePiece)?.FlashBox;
flash?.FadeTo(0.9f).FadeOut(300);
const float gravity_time = 300;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
deleted file mode 100644
index 5a12d71cea..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables
-{
- public class DrawableRimHit : DrawableHit
- {
- public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim };
-
- public DrawableRimHit(Hit hit)
- : base(hit)
- {
- MainPiece.Add(new RimHitSymbolPiece());
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- MainPiece.AccentColour = colours.BlueDarker;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index fa39819199..32f7acadc8 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -9,11 +9,12 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -34,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
- private readonly SwellSymbolPiece symbol;
-
public DrawableSwell(Swell swell)
: base(swell)
{
@@ -107,18 +106,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
});
AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both });
-
- MainPiece.Add(symbol = new SwellSymbolPiece());
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- MainPiece.AccentColour = colours.YellowDark;
expandingRing.Colour = colours.YellowLight;
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
}
+ protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Swell),
+ _ => new SwellCirclePiece
+ {
+ // to allow for rotation transform
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -182,7 +186,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
.Then()
.FadeTo(completion / 8, 2000, Easing.OutQuint);
- symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
+ MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index ce875ebba8..1685576f0d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -3,6 +3,8 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -28,5 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
public override bool OnPressed(TaikoAction action) => false;
+
+ protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
+ _ => new TickPiece());
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 5f892dd2fa..1be04f1760 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -4,7 +4,6 @@
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osuTK;
using System.Linq;
using osu.Game.Audio;
@@ -12,6 +11,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Objects;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
/// Moves to a layer proxied above the playfield.
- /// Does nothing is content is already proxied.
+ /// Does nothing if content is already proxied.
///
protected void ProxyContent()
{
@@ -108,19 +108,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
- public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject
- where TTaikoHit : TaikoHitObject
+ public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject
+ where TObject : TaikoHitObject
{
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
- public new TTaikoHit HitObject;
+ public new TObject HitObject;
protected readonly Vector2 BaseSize;
- protected readonly TaikoPiece MainPiece;
+ protected readonly SkinnableDrawable MainPiece;
private readonly Container strongHitContainer;
- protected DrawableTaikoHitObject(TTaikoHit hitObject)
+ protected DrawableTaikoHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
@@ -132,7 +132,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
Content.Add(MainPiece = CreateMainPiece());
- MainPiece.KiaiMode = HitObject.Kiai;
AddInternal(strongHitContainer = new Container());
}
@@ -169,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Normal and clap samples are handled by the drum
protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
- protected virtual TaikoPiece CreateMainPiece() => new CirclePiece();
+ protected abstract SkinnableDrawable CreateMainPiece();
///
/// Creates the handler for this 's .
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs
new file mode 100644
index 0000000000..0509841ba8
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs
@@ -0,0 +1,52 @@
+// 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 osuTK;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
+{
+ public class CentreHitCirclePiece : CirclePiece
+ {
+ public CentreHitCirclePiece()
+ {
+ Add(new CentreHitSymbolPiece());
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AccentColour = colours.PinkDarker;
+ }
+
+ ///
+ /// The symbol used for centre hit pieces.
+ ///
+ public class CentreHitSymbolPiece : Container
+ {
+ public CentreHitSymbolPiece()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ Size = new Vector2(SYMBOL_SIZE);
+ Padding = new MarginPadding(SYMBOL_BORDER);
+
+ Children = new[]
+ {
+ new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new[] { new Box { RelativeSizeAxes = Axes.Both } }
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs
deleted file mode 100644
index 7ed61ede96..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osuTK;
-using osu.Framework.Graphics.Shapes;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
-{
- ///
- /// The symbol used for centre hit pieces.
- ///
- public class CentreHitSymbolPiece : Container
- {
- public CentreHitSymbolPiece()
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- RelativeSizeAxes = Axes.Both;
- Size = new Vector2(CirclePiece.SYMBOL_SIZE);
- Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER);
-
- Children = new[]
- {
- new CircularContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new[] { new Box { RelativeSizeAxes = Axes.Both } }
- }
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
index d9c0664ecd..b5471e6976 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
@@ -10,6 +10,8 @@ using osuTK.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Effects;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
@@ -20,21 +22,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
/// for a usage example.
///
///
- public class CirclePiece : TaikoPiece
+ public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{
public const float SYMBOL_SIZE = 0.45f;
public const float SYMBOL_BORDER = 8;
private const double pre_beat_transition_time = 80;
+ private Color4 accentColour;
+
///
/// The colour of the inner circle and outer glows.
///
- public override Color4 AccentColour
+ public Color4 AccentColour
{
- get => base.AccentColour;
+ get => accentColour;
set
{
- base.AccentColour = value;
+ accentColour = value;
background.Colour = AccentColour;
@@ -42,15 +46,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
}
}
+ private bool kiaiMode;
+
///
/// Whether Kiai mode effects are enabled for this circle piece.
///
- public override bool KiaiMode
+ public bool KiaiMode
{
- get => base.KiaiMode;
+ get => kiaiMode;
set
{
- base.KiaiMode = value;
+ kiaiMode = value;
resetEdgeEffects();
}
@@ -64,8 +70,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
public Box FlashBox;
- public CirclePiece()
+ protected CirclePiece()
{
+ RelativeSizeAxes = Axes.Both;
+
EarlyActivationMilliseconds = pre_beat_transition_time;
AddRangeInternal(new Drawable[]
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs
index 7e3272e42b..034ab6dd21 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Graphics;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
@@ -12,18 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
RelativeSizeAxes = Axes.Y;
}
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AccentColour = colours.YellowDark;
+ }
+
protected override void Update()
{
base.Update();
-
- var padding = Content.DrawHeight * Content.Width / 2;
-
- Content.Padding = new MarginPadding
- {
- Left = padding,
- Right = padding,
- };
-
Width = Parent.DrawSize.X + DrawHeight;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs
new file mode 100644
index 0000000000..3273ab7fa7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs
@@ -0,0 +1,55 @@
+// 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.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
+{
+ public class RimHitCirclePiece : CirclePiece
+ {
+ public RimHitCirclePiece()
+ {
+ Add(new RimHitSymbolPiece());
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AccentColour = colours.BlueDarker;
+ }
+
+ ///
+ /// The symbol used for rim hit pieces.
+ ///
+ public class RimHitSymbolPiece : CircularContainer
+ {
+ public RimHitSymbolPiece()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ Size = new Vector2(SYMBOL_SIZE);
+
+ BorderThickness = SYMBOL_BORDER;
+ BorderColour = Color4.White;
+ Masking = true;
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs
deleted file mode 100644
index e4c964a884..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osuTK;
-using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
-{
- ///
- /// The symbol used for rim hit pieces.
- ///
- public class RimHitSymbolPiece : CircularContainer
- {
- public RimHitSymbolPiece()
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- RelativeSizeAxes = Axes.Both;
- Size = new Vector2(CirclePiece.SYMBOL_SIZE);
-
- BorderThickness = CirclePiece.SYMBOL_BORDER;
- BorderColour = Color4.White;
- Masking = true;
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
- };
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs
index 0ed9923924..a8f9f0b94d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs
@@ -1,36 +1,52 @@
// 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 osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
- ///
- /// The symbol used for swell pieces.
- ///
- public class SwellSymbolPiece : Container
+ public class SwellCirclePiece : CirclePiece
{
- public SwellSymbolPiece()
+ public SwellCirclePiece()
{
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
+ Add(new SwellSymbolPiece());
+ }
- RelativeSizeAxes = Axes.Both;
- Size = new Vector2(CirclePiece.SYMBOL_SIZE);
- Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER);
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AccentColour = colours.YellowDark;
+ }
- Children = new[]
+ ///
+ /// The symbol used for swell pieces.
+ ///
+ public class SwellSymbolPiece : Container
+ {
+ public SwellSymbolPiece()
{
- new SpriteIcon
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ Size = new Vector2(SYMBOL_SIZE);
+ Padding = new MarginPadding(SYMBOL_BORDER);
+
+ Children = new[]
{
- RelativeSizeAxes = Axes.Both,
- Icon = FontAwesome.Solid.Asterisk,
- Shadow = false
- }
- };
+ new SpriteIcon
+ {
+ RelativeSizeAxes = Axes.Both,
+ Icon = FontAwesome.Solid.Asterisk,
+ Shadow = false
+ }
+ };
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs
deleted file mode 100644
index 8067054f8f..0000000000
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Graphics;
-using osuTK.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Framework.Graphics;
-
-namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
-{
- public class TaikoPiece : BeatSyncedContainer, IHasAccentColour
- {
- ///
- /// The colour of the inner circle and outer glows.
- ///
- public virtual Color4 AccentColour { get; set; }
-
- ///
- /// Whether Kiai mode effects are enabled for this circle piece.
- ///
- public virtual bool KiaiMode { get; set; }
-
- public TaikoPiece()
- {
- RelativeSizeAxes = Axes.Both;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs
index 83cf7a64ec..0648bcebcd 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
- public class TickPiece : TaikoPiece
+ public class TickPiece : CompositeDrawable
{
///
/// Any tick that is not the first for a drumroll is not filled, but is instead displayed
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
FillMode = FillMode.Fit;
Size = new Vector2(tick_size);
- Add(new CircularContainer
+ InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
AlwaysPresent = true
}
}
- });
+ };
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index aacd78f176..dc2f277e58 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -3,15 +3,20 @@
using osu.Game.Rulesets.Objects.Types;
using System;
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
- public class DrumRoll : TaikoHitObject, IHasEndTime
+ public class DrumRoll : TaikoHitObject, IHasCurve
{
///
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@@ -26,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public double Duration { get; set; }
+ ///
+ /// Velocity of this .
+ ///
+ public double Velocity { get; private set; }
+
///
/// Numer of ticks per beat length.
///
@@ -54,6 +64,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
+ DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
+
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
overallDifficulty = difficulty.OverallDifficulty;
@@ -93,5 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ #region LegacyBeatmapEncoder
+
+ double IHasDistance.Distance => Duration * Velocity;
+
+ int IHasRepeats.RepeatCount { get => 0; set { } }
+
+ List> IHasRepeats.NodeSamples => new List>();
+
+ SliderPath IHasCurve.Path
+ => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs
new file mode 100644
index 0000000000..302f940ef4
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs
@@ -0,0 +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 osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Taiko.Objects
+{
+ public class IgnoreHit : Hit
+ {
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs
new file mode 100644
index 0000000000..7d08a21ab1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs
@@ -0,0 +1,27 @@
+// 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.Sprites;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyBarLine : Sprite
+ {
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ Texture = skin.GetTexture("taiko-barline");
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ Size = new Vector2(1, 0.88f);
+ FillMode = FillMode.Fill;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
new file mode 100644
index 0000000000..bfcf268c3d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
@@ -0,0 +1,96 @@
+// 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.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
+ {
+ private Drawable backgroundLayer;
+
+ public LegacyCirclePiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
+ {
+ Drawable getDrawableFor(string lookup)
+ {
+ const string normal_hit = "taikohit";
+ const string big_hit = "taikobig";
+
+ string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
+
+ return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
+ // fallback to regular size if "big" version doesn't exist.
+ skin.GetAnimation($"{normal_hit}{lookup}", true, false);
+ }
+
+ // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
+ AddInternal(backgroundLayer = getDrawableFor("circle"));
+
+ var foregroundLayer = getDrawableFor("circleoverlay");
+ if (foregroundLayer != null)
+ AddInternal(foregroundLayer);
+
+ // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
+ // For now just stop at first frame for sanity.
+ foreach (var c in InternalChildren)
+ {
+ (c as IFramedAnimation)?.Stop();
+
+ c.Anchor = Anchor.Centre;
+ c.Origin = Anchor.Centre;
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateAccentColour();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
+ // This ensures they are scaled relative to each other but also match the expected DrawableHit size.
+ foreach (var c in InternalChildren)
+ c.Scale = new Vector2(DrawHeight / 128);
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ if (value == accentColour)
+ return;
+
+ accentColour = value;
+ if (IsLoaded)
+ updateAccentColour();
+ }
+ }
+
+ private void updateAccentColour()
+ {
+ backgroundLayer.Colour = accentColour;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
new file mode 100644
index 0000000000..8531f3cefd
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour
+ {
+ private LegacyCirclePiece headCircle;
+
+ private Sprite body;
+
+ private Sprite end;
+
+ public LegacyDrumRoll()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, OsuColour colours)
+ {
+ InternalChildren = new Drawable[]
+ {
+ end = new Sprite
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ Texture = skin.GetTexture("taiko-roll-end"),
+ FillMode = FillMode.Fit,
+ },
+ body = new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = skin.GetTexture("taiko-roll-middle"),
+ },
+ headCircle = new LegacyCirclePiece
+ {
+ RelativeSizeAxes = Axes.Y,
+ },
+ };
+
+ AccentColour = colours.YellowDark;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateAccentColour();
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ if (value == accentColour)
+ return;
+
+ accentColour = value;
+ if (IsLoaded)
+ updateAccentColour();
+ }
+ }
+
+ private void updateAccentColour()
+ {
+ headCircle.AccentColour = accentColour;
+ body.Colour = accentColour;
+ end.Colour = accentColour;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
new file mode 100644
index 0000000000..656728f6e4
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyHit : LegacyCirclePiece
+ {
+ private readonly TaikoSkinComponents component;
+
+ public LegacyHit(TaikoSkinComponents component)
+ {
+ this.component = component;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AccentColour = component == TaikoSkinComponents.CentreHit
+ ? new Color4(235, 69, 44, 255)
+ : new Color4(67, 142, 172, 255);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
new file mode 100644
index 0000000000..d29b574866
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class LegacyHitExplosion : CompositeDrawable
+ {
+ public LegacyHitExplosion(Drawable sprite)
+ {
+ InternalChild = sprite;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeIn(120);
+ this.ScaleTo(0.6f).Then().ScaleTo(1, 240, Easing.OutElastic);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
index c61e35692b..81d645e294 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
@@ -20,36 +20,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
private LegacyHalfDrum left;
private LegacyHalfDrum right;
+ private Container content;
public LegacyInputDrum()
{
- Size = new Vector2(180, 200);
+ RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- Children = new Drawable[]
+ Child = content = new Container
{
- new Sprite
+ Size = new Vector2(180, 200),
+ Children = new Drawable[]
{
- Texture = skin.GetTexture("taiko-bar-left")
- },
- left = new LegacyHalfDrum(false)
- {
- Name = "Left Half",
- RelativeSizeAxes = Axes.Both,
- RimAction = TaikoAction.LeftRim,
- CentreAction = TaikoAction.LeftCentre
- },
- right = new LegacyHalfDrum(true)
- {
- Name = "Right Half",
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.TopRight,
- Scale = new Vector2(-1, 1),
- RimAction = TaikoAction.RightRim,
- CentreAction = TaikoAction.RightCentre
+ new Sprite
+ {
+ Texture = skin.GetTexture("taiko-bar-left")
+ },
+ left = new LegacyHalfDrum(false)
+ {
+ Name = "Left Half",
+ RelativeSizeAxes = Axes.Both,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ right = new LegacyHalfDrum(true)
+ {
+ Name = "Right Half",
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(-1, 1),
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ }
}
};
@@ -60,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
const float ratio = 1.6f;
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
- float negativeScaleAdjust = Width / ratio;
+ float negativeScaleAdjust = content.Width / ratio;
if (skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{
@@ -78,6 +83,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning
}
}
+ protected override void Update()
+ {
+ base.Update();
+
+ // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
+ // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
+ content.Scale = new Vector2(DrawHeight / content.Size.Y);
+ }
+
///
/// A half-drum. Contains one centre and one rim hit.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs
new file mode 100644
index 0000000000..e522fb7c10
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs
@@ -0,0 +1,59 @@
+// 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.Graphics.Sprites;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning
+{
+ public class TaikoLegacyHitTarget : CompositeDrawable
+ {
+ private Container content;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture("approachcircle"),
+ Scale = new Vector2(0.73f),
+ Alpha = 0.47f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture("taikobigcircle"),
+ Scale = new Vector2(0.7f),
+ Alpha = 0.22f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
+ // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
+ content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 78eec94590..f0df612e18 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
@@ -8,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
@@ -27,16 +29,91 @@ namespace osu.Game.Rulesets.Taiko.Skinning
switch (taikoComponent.Component)
{
+ case TaikoSkinComponents.DrumRollBody:
+ if (GetTexture("taiko-roll-middle") != null)
+ return new LegacyDrumRoll();
+
+ return null;
+
case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null)
return new LegacyInputDrum();
return null;
+
+ case TaikoSkinComponents.CentreHit:
+ case TaikoSkinComponents.RimHit:
+
+ if (GetTexture("taikohitcircle") != null)
+ return new LegacyHit(taikoComponent.Component);
+
+ return null;
+
+ case TaikoSkinComponents.DrumRollTick:
+ return this.GetAnimation("sliderscorepoint", false, false);
+
+ case TaikoSkinComponents.HitTarget:
+ if (GetTexture("taikobigcircle") != null)
+ return new TaikoLegacyHitTarget();
+
+ return null;
+
+ case TaikoSkinComponents.PlayfieldBackgroundRight:
+ if (GetTexture("taiko-bar-right") != null)
+ {
+ return this.GetAnimation("taiko-bar-right", false, false).With(d =>
+ {
+ d.RelativeSizeAxes = Axes.Both;
+ d.Size = Vector2.One;
+ });
+ }
+
+ return null;
+
+ case TaikoSkinComponents.PlayfieldBackgroundLeft:
+ // This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
+ if (GetTexture("taiko-bar-right") != null)
+ return Drawable.Empty();
+
+ return null;
+
+ case TaikoSkinComponents.BarLine:
+ if (GetTexture("taiko-barline") != null)
+ return new LegacyBarLine();
+
+ return null;
+
+ case TaikoSkinComponents.TaikoExplosionGood:
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ case TaikoSkinComponents.TaikoExplosionMiss:
+
+ var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
+ if (sprite != null)
+ return new LegacyHitExplosion(sprite);
+
+ return null;
}
return source.GetDrawableComponent(component);
}
+ private string getHitName(TaikoSkinComponents component)
+ {
+ switch (component)
+ {
+ case TaikoSkinComponents.TaikoExplosionMiss:
+ return "taiko-hit0";
+
+ case TaikoSkinComponents.TaikoExplosionGood:
+ return "taiko-hit100";
+
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ return "taiko-hit300";
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
+ }
+
public Texture GetTexture(string componentName) => source.GetTexture(componentName);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 6d4581db80..fd091f97d0 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -6,5 +6,17 @@ namespace osu.Game.Rulesets.Taiko
public enum TaikoSkinComponents
{
InputDrum,
+ CentreHit,
+ RimHit,
+ DrumRollBody,
+ DrumRollTick,
+ Swell,
+ HitTarget,
+ PlayfieldBackgroundLeft,
+ PlayfieldBackgroundRight,
+ BarLine,
+ TaikoExplosionMiss,
+ TaikoExplosionGood,
+ TaikoExplosionGreat,
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
new file mode 100644
index 0000000000..aa444d0494
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class DefaultHitExplosion : CircularContainer
+ {
+ [Resolved]
+ private DrawableHitObject judgedObject { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ BorderColour = Color4.White;
+ BorderThickness = 1;
+
+ Alpha = 0.15f;
+ Masking = true;
+
+ if (judgedObject.Result.Type == HitResult.Miss)
+ return;
+
+ bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
+
+ InternalChildren = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.ScaleTo(3f, 1000, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index e4a4b555a7..a6a00fe242 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -49,10 +49,7 @@ namespace osu.Game.Rulesets.Taiko.UI
switch (h)
{
case Hit hit:
- if (hit.Type == HitType.Centre)
- return new DrawableCentreHit(hit);
- else
- return new DrawableRimHit(hit);
+ return new DrawableHit(hit);
case DrumRoll drumRoll:
return new DrawableDrumRoll(drumRoll);
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
new file mode 100644
index 0000000000..fde42bec04
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class DrumRollHitContainer : ScrollingHitObjectContainer
+ {
+ protected override void Update()
+ {
+ base.Update();
+
+ // Remove any auxiliary hit notes that were spawned during a drum roll but subsequently rewound.
+ for (var i = AliveInternalChildren.Count - 1; i >= 0; i--)
+ {
+ var flyingHit = (DrawableFlyingHit)AliveInternalChildren[i];
+ if (Time.Current <= flyingHit.HitObject.StartTime)
+ Remove(flyingHit);
+ }
+ }
+
+ protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
+ {
+ base.OnChildLifetimeBoundaryCrossed(e);
+
+ // ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above).
+ if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)
+ Remove((DrawableHitObject)e.Child);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index 404960c26f..35a54d6ea7 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -1,15 +1,15 @@
// 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 osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -20,54 +20,50 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public override bool RemoveWhenNotAlive => true;
+ [Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject;
- private readonly Box innerFill;
-
- private readonly bool isRim;
-
- public HitExplosion(DrawableHitObject judgedObject, bool isRim)
+ public HitExplosion(DrawableHitObject judgedObject)
{
- this.isRim = isRim;
-
JudgedObject = judgedObject;
- Anchor = Anchor.CentreLeft;
+ Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
RelativePositionAxes = Axes.Both;
-
- BorderColour = Color4.White;
- BorderThickness = 1;
-
- Alpha = 0.15f;
- Masking = true;
-
- Children = new[]
- {
- innerFill = new Box
- {
- RelativeSizeAxes = Axes.Both,
- }
- };
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- innerFill.Colour = isRim ? colours.BlueDarker : colours.PinkDarker;
+ Child = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
+ }
+
+ private TaikoSkinComponents getComponentName(HitResult resultType)
+ {
+ switch (resultType)
+ {
+ case HitResult.Miss:
+ return TaikoSkinComponents.TaikoExplosionMiss;
+
+ case HitResult.Good:
+ return TaikoSkinComponents.TaikoExplosionGood;
+
+ case HitResult.Great:
+ return TaikoSkinComponents.TaikoExplosionGreat;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
}
protected override void LoadComplete()
{
base.LoadComplete();
- this.ScaleTo(3f, 1000, Easing.OutQuint);
this.FadeOut(500);
-
Expire(true);
}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 422ea2f929..38026517d9 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.UI
sampleMapping = new DrumSampleMapping(controlPoints);
RelativeSizeAxes = Axes.Both;
- FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
@@ -40,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.UI
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{
RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Scale = new Vector2(0.9f),
Children = new Drawable[]
{
new TaikoHalfDrum(false)
diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
index e80b463481..3a307bb3bb 100644
--- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
@@ -18,14 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public override bool RemoveWhenNotAlive => true;
public readonly DrawableHitObject JudgedObject;
+ private readonly HitType type;
- private readonly bool isRim;
-
- public KiaiHitExplosion(DrawableHitObject judgedObject, bool isRim)
+ public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type)
{
- this.isRim = isRim;
-
JudgedObject = judgedObject;
+ this.type = type;
Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre;
@@ -53,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.UI
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
+ Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker,
Radius = 60,
};
}
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
new file mode 100644
index 0000000000..2a8890a95d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ internal class PlayfieldBackgroundLeft : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray1,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = 10,
+ Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
new file mode 100644
index 0000000000..44bfdacf37
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.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 osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ public class PlayfieldBackgroundRight : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Name = "Transparent playfield background";
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+ BorderColour = colours.Gray1;
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Color4.Black.Opacity(0.2f),
+ Radius = 5,
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.Gray0,
+ Alpha = 0.6f
+ },
+ new Container
+ {
+ Name = "Border",
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ MaskingSmoothness = 0,
+ BorderThickness = 2,
+ AlwaysPresent = true,
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
similarity index 95%
rename from osu.Game.Rulesets.Taiko/UI/HitTarget.cs
rename to osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
index 2bb208bd1d..7de1593ab6 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
@@ -13,15 +13,17 @@ namespace osu.Game.Rulesets.Taiko.UI
///
/// A component that is displayed at the hit position in the taiko playfield.
///
- internal class HitTarget : Container
+ internal class TaikoHitTarget : Container
{
///
/// Thickness of all drawn line pieces.
///
private const float border_thickness = 2.5f;
- public HitTarget()
+ public TaikoHitTarget()
{
+ RelativeSizeAxes = Axes.Both;
+
Children = new Drawable[]
{
new Box
@@ -39,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true,
BorderColour = Color4.White,
@@ -61,7 +62,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true,
BorderColour = Color4.White,
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index bde9085c23..6a78c0a1fb 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -3,11 +3,8 @@
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@@ -17,193 +14,142 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
-using osuTK;
-using osuTK.Graphics;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoPlayfield : ScrollingPlayfield
{
+ private readonly ControlPointInfo controlPoints;
+
///
/// Default height of a when inside a .
///
public const float DEFAULT_HEIGHT = 178;
- ///
- /// The offset from which the center of the hit target lies at.
- ///
- public const float HIT_TARGET_OFFSET = 100;
+ private Container hitExplosionContainer;
+ private Container kiaiExplosionContainer;
+ private JudgementContainer judgementContainer;
+ private ScrollingHitObjectContainer drumRollHitContainer;
+ internal Drawable HitTarget;
- ///
- /// The size of the left area of the playfield. This area contains the input drum.
- ///
- private const float left_area_size = 240;
+ private ProxyContainer topLevelHitContainer;
+ private ProxyContainer barlineContainer;
+ private Container rightArea;
+ private Container leftArea;
- private readonly Container hitExplosionContainer;
- private readonly Container kiaiExplosionContainer;
- private readonly JudgementContainer judgementContainer;
- internal readonly HitTarget HitTarget;
-
- private readonly ProxyContainer topLevelHitContainer;
- private readonly ProxyContainer barlineContainer;
-
- private readonly Container overlayBackgroundContainer;
- private readonly Container backgroundContainer;
-
- private readonly Box overlayBackground;
- private readonly Box background;
+ private Container hitTargetOffsetContent;
public TaikoPlayfield(ControlPointInfo controlPoints)
{
- InternalChildren = new Drawable[]
+ this.controlPoints = controlPoints;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new[]
{
- backgroundContainer = new Container
- {
- Name = "Transparent playfield background",
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Colour = Color4.Black.Opacity(0.2f),
- Radius = 5,
- },
- Children = new Drawable[]
- {
- background = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.6f
- },
- }
- },
- new Container
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()),
+ rightArea = new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = left_area_size },
+ RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Masking = true,
- Children = new Drawable[]
+ FillMode = FillMode.Fit,
+ Children = new[]
{
hitExplosionContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Blending = BlendingParameters.Additive,
},
- HitTarget = new HitTarget
+ HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget())
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit
}
}
},
- barlineContainer = new ProxyContainer
+ hitTargetOffsetContent = new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
- },
- new Container
- {
- Name = "Hit objects",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Masking = true,
- Child = HitObjectContainer
- },
- kiaiExplosionContainer = new Container
- {
- Name = "Kiai hit explosions",
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Blending = BlendingParameters.Additive
- },
- judgementContainer = new JudgementContainer
- {
- Name = "Judgements",
- RelativeSizeAxes = Axes.Y,
- Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
- Blending = BlendingParameters.Additive
+ Children = new Drawable[]
+ {
+ barlineContainer = new ProxyContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Name = "Hit objects",
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ HitObjectContainer,
+ drumRollHitContainer = new DrumRollHitContainer()
+ }
+ },
+ kiaiExplosionContainer = new Container
+ {
+ Name = "Kiai hit explosions",
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Blending = BlendingParameters.Additive
+ },
+ judgementContainer = new JudgementContainer
+ {
+ Name = "Judgements",
+ RelativeSizeAxes = Axes.Y,
+ Blending = BlendingParameters.Additive
+ },
+ }
},
}
},
- overlayBackgroundContainer = new Container
+ leftArea = new Container
{
Name = "Left overlay",
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(left_area_size, 1),
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ BorderColour = colours.Gray0,
Children = new Drawable[]
{
- overlayBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
new InputDrum(controlPoints)
{
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Scale = new Vector2(0.9f),
- Margin = new MarginPadding { Right = 20 }
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
},
- new Box
- {
- Anchor = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = 10,
- Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
- },
- }
- },
- new Container
- {
- Name = "Border",
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- MaskingSmoothness = 0,
- BorderThickness = 2,
- AlwaysPresent = true,
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
}
},
topLevelHitContainer = new ProxyContainer
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
- }
+ },
+ drumRollHitContainer.CreateProxy()
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ protected override void Update()
{
- overlayBackgroundContainer.BorderColour = colours.Gray0;
- overlayBackground.Colour = colours.Gray1;
+ base.Update();
- backgroundContainer.BorderColour = colours.Gray1;
- background.Colour = colours.Gray0;
+ // Padding is required to be updated for elements which are based on "absolute" X sized elements.
+ // This is basically allowing for correct alignment as relative pieces move around them.
+ rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
+ hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
}
public override void Add(DrawableHitObject h)
{
h.OnNewResult += OnNewResult;
-
base.Add(h);
switch (h)
@@ -222,7 +168,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
if (!DisplayJudgements.Value)
return;
-
if (!judgedObject.DisplayResult)
return;
@@ -233,6 +178,15 @@ namespace osu.Game.Rulesets.Taiko.UI
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit();
break;
+ case TaikoDrumRollTickJudgement _:
+ if (!result.IsHit)
+ break;
+
+ var drawableTick = (DrawableDrumRollTick)judgedObject;
+
+ addDrumRollHit(drawableTick);
+ break;
+
default:
judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
{
@@ -245,17 +199,23 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.IsHit)
break;
- bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
-
- hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim));
-
- if (judgedObject.HitObject.Kiai)
- kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim));
+ var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
+ addExplosion(judgedObject, type);
break;
}
}
+ private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
+ drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
+
+ private void addExplosion(DrawableHitObject drawableObject, HitType type)
+ {
+ hitExplosionContainer.Add(new HitExplosion(drawableObject));
+ if (drawableObject.HitObject.Kiai)
+ kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
+ }
+
private class ProxyContainer : LifetimeManagementContainer
{
public new MarginPadding Padding
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 33f484a9aa..acb30a6277 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -241,6 +241,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
+ Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
+ Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));
+ Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
+ Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
+
Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index f2b3a16f68..bcc873b0b7 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -1,14 +1,23 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Serialization;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
@@ -16,39 +25,91 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyBeatmapEncoderTest
{
- private const string normal = "Soleily - Renatus (Gamu) [Insane].osu";
-
private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu"));
[TestCaseSource(nameof(allBeatmaps))]
- public void TestDecodeEncodedBeatmap(string name)
+ public void TestBeatmap(string name)
{
- var decoded = decode(normal, out var encoded);
+ var decoded = decode(name, out var encoded);
+
+ sort(decoded);
+ sort(encoded);
- Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count));
Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize()));
}
- private Beatmap decode(string filename, out Beatmap encoded)
+ private void sort(IBeatmap beatmap)
{
- using (var stream = TestResources.OpenResource(filename))
+ // Sort control points to ensure a sane ordering, as they may be parsed in different orders. This works because each group contains only uniquely-typed control points.
+ foreach (var g in beatmap.ControlPointInfo.Groups)
+ {
+ ArrayList.Adapter((IList)g.ControlPoints).Sort(
+ Comparer.Create((c1, c2) => string.Compare(c1.GetType().ToString(), c2.GetType().ToString(), StringComparison.Ordinal)));
+ }
+ }
+
+ private IBeatmap decode(string filename, out IBeatmap encoded)
+ {
+ using (var stream = TestResources.GetStore().GetStream(filename))
using (var sr = new LineBufferedReader(stream))
{
- var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr);
+ var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr));
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
- using (var sr2 = new LineBufferedReader(ms))
+ using (var sr2 = new LineBufferedReader(ms, true))
{
new LegacyBeatmapEncoder(legacyDecoded).Encode(sw);
- sw.Flush();
+ sw.Flush();
ms.Position = 0;
- encoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2);
+ encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2));
+
return legacyDecoded;
}
}
}
+
+ private IBeatmap convert(IBeatmap beatmap)
+ {
+ switch (beatmap.BeatmapInfo.RulesetID)
+ {
+ case 0:
+ beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
+ break;
+
+ case 1:
+ beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo;
+ break;
+
+ case 2:
+ beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo;
+ break;
+
+ case 3:
+ beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo;
+ break;
+ }
+
+ return new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset);
+ }
+
+ private class TestWorkingBeatmap : WorkingBeatmap
+ {
+ private readonly IBeatmap beatmap;
+
+ public TestWorkingBeatmap(IBeatmap beatmap)
+ : base(beatmap.BeatmapInfo, null)
+ {
+ this.beatmap = beatmap;
+ }
+
+ protected override IBeatmap GetBeatmap() => beatmap;
+
+ protected override Texture GetBackground() => throw new NotImplementedException();
+
+ protected override Track GetTrack() => throw new NotImplementedException();
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
index d367d9f88b..b7b48ec06a 100644
--- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
@@ -1,8 +1,9 @@
// 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 Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
@@ -136,7 +137,7 @@ namespace osu.Game.Tests.Beatmaps
var hitCircle = new HitCircle { StartTime = 1000 };
editorBeatmap.Add(hitCircle);
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
- Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3));
+ Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(3));
}
///
@@ -160,7 +161,71 @@ namespace osu.Game.Tests.Beatmaps
hitCircle.StartTime = 0;
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
- Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1));
+ Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(1));
+ }
+
+ ///
+ /// Tests that multiple hitobjects are updated simultaneously.
+ ///
+ [Test]
+ public void TestMultipleHitObjectUpdate()
+ {
+ var updatedObjects = new List();
+ var allHitObjects = new List();
+ EditorBeatmap editorBeatmap = null;
+
+ AddStep("add beatmap", () =>
+ {
+ updatedObjects.Clear();
+
+ Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+
+ for (int i = 0; i < 10; i++)
+ {
+ var h = new HitCircle();
+ editorBeatmap.Add(h);
+ allHitObjects.Add(h);
+ }
+ });
+
+ AddStep("change all start times", () =>
+ {
+ editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
+
+ for (int i = 0; i < 10; i++)
+ allHitObjects[i].StartTime += 10;
+ });
+
+ // Distinct ensures that all hitobjects have been updated once, debounce is tested below.
+ AddAssert("all hitobjects updated", () => updatedObjects.Distinct().Count() == 10);
+ }
+
+ ///
+ /// Tests that hitobject updates are debounced when they happen too soon.
+ ///
+ [Test]
+ public void TestDebouncedUpdate()
+ {
+ var updatedObjects = new List();
+ EditorBeatmap editorBeatmap = null;
+
+ AddStep("add beatmap", () =>
+ {
+ updatedObjects.Clear();
+
+ Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap.Add(new HitCircle());
+ });
+
+ AddStep("change start time twice", () =>
+ {
+ editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
+
+ editorBeatmap.HitObjects[0].StartTime = 10;
+ editorBeatmap.HitObjects[0].StartTime = 20;
+ });
+
+ AddAssert("only updated once", () => updatedObjects.Count == 1);
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs
new file mode 100644
index 0000000000..c477bbd9cf
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Beatmaps
+{
+ [TestFixture]
+ public class ToStringFormattingTest
+ {
+ [Test]
+ public void TestArtistTitle()
+ {
+ var beatmap = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "artist",
+ Title = "title"
+ }
+ };
+
+ Assert.That(beatmap.ToString(), Is.EqualTo("artist - title"));
+ }
+
+ [Test]
+ public void TestArtistTitleCreator()
+ {
+ var beatmap = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "artist",
+ Title = "title",
+ Author = new User { Username = "creator" }
+ }
+ };
+
+ Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator)"));
+ }
+
+ [Test]
+ public void TestArtistTitleCreatorDifficulty()
+ {
+ var beatmap = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "artist",
+ Title = "title",
+ Author = new User { Username = "creator" }
+ },
+ Version = "difficulty"
+ };
+
+ Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]"));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
new file mode 100644
index 0000000000..feda1ae0e9
--- /dev/null
+++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
@@ -0,0 +1,74 @@
+// 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;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Tests.Editing
+{
+ [TestFixture]
+ public class EditorChangeHandlerTest
+ {
+ [Test]
+ public void TestSaveRestoreState()
+ {
+ var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.False);
+
+ handler.SaveState();
+
+ Assert.That(handler.CanUndo.Value, Is.True);
+ Assert.That(handler.CanRedo.Value, Is.False);
+
+ handler.RestoreState(-1);
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.True);
+ }
+
+ [Test]
+ public void TestMaxStatesSaved()
+ {
+ var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ handler.SaveState();
+
+ Assert.That(handler.CanUndo.Value, Is.True);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ {
+ Assert.That(handler.CanUndo.Value, Is.True);
+ handler.RestoreState(-1);
+ }
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ }
+
+ [Test]
+ public void TestMaxStatesExceeded()
+ {
+ var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
+ handler.SaveState();
+
+ Assert.That(handler.CanUndo.Value, Is.True);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ {
+ Assert.That(handler.CanUndo.Value, Is.True);
+ handler.RestoreState(-1);
+ }
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
new file mode 100644
index 0000000000..a3ab677d96
--- /dev/null
+++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
@@ -0,0 +1,342 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using System.Text;
+using NUnit.Framework;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osuTK;
+using Decoder = osu.Game.Beatmaps.Formats.Decoder;
+
+namespace osu.Game.Tests.Editing
+{
+ [TestFixture]
+ public class LegacyEditorBeatmapPatcherTest
+ {
+ private LegacyEditorBeatmapPatcher patcher;
+ private EditorBeatmap current;
+
+ [SetUp]
+ public void Setup()
+ {
+ patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap
+ {
+ BeatmapInfo =
+ {
+ Ruleset = new OsuRuleset().RulesetInfo
+ }
+ }));
+ }
+
+ [Test]
+ public void TestAddHitObject()
+ {
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 1000 }
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestInsertHitObject()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[0],
+ new HitCircle { StartTime = 2000 },
+ (OsuHitObject)current.HitObjects[1],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestDeleteHitObject()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[0],
+ (OsuHitObject)current.HitObjects[2],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestChangeStartTime()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 500 },
+ (OsuHitObject)current.HitObjects[1],
+ (OsuHitObject)current.HitObjects[2],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestChangeSample()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[0],
+ new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
+ (OsuHitObject)current.HitObjects[2],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestChangeSliderPath()
+ {
+ current.AddRange(new OsuHitObject[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new Slider
+ {
+ StartTime = 2000,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(Vector2.One),
+ new PathControlPoint(new Vector2(2), PathType.Bezier),
+ new PathControlPoint(new Vector2(3)),
+ }, 50)
+ },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[0],
+ new Slider
+ {
+ StartTime = 2000,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero, PathType.Bezier),
+ new PathControlPoint(new Vector2(4)),
+ new PathControlPoint(new Vector2(5)),
+ }, 100)
+ },
+ (OsuHitObject)current.HitObjects[2],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestAddMultipleHitObjects()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 3000 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 500 },
+ (OsuHitObject)current.HitObjects[0],
+ new HitCircle { StartTime = 1500 },
+ (OsuHitObject)current.HitObjects[1],
+ new HitCircle { StartTime = 2250 },
+ new HitCircle { StartTime = 2500 },
+ (OsuHitObject)current.HitObjects[2],
+ new HitCircle { StartTime = 3500 },
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestDeleteMultipleHitObjects()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 500 },
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 1500 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 2250 },
+ new HitCircle { StartTime = 2500 },
+ new HitCircle { StartTime = 3000 },
+ new HitCircle { StartTime = 3500 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[1],
+ (OsuHitObject)current.HitObjects[3],
+ (OsuHitObject)current.HitObjects[6],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestChangeSamplesOfMultipleHitObjects()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 500 },
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 1500 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 2250 },
+ new HitCircle { StartTime = 2500 },
+ new HitCircle { StartTime = 3000 },
+ new HitCircle { StartTime = 3500 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ (OsuHitObject)current.HitObjects[0],
+ new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
+ (OsuHitObject)current.HitObjects[2],
+ (OsuHitObject)current.HitObjects[3],
+ new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } },
+ (OsuHitObject)current.HitObjects[5],
+ new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } },
+ (OsuHitObject)current.HitObjects[7],
+ }
+ };
+
+ runTest(patch);
+ }
+
+ [Test]
+ public void TestAddAndDeleteHitObjects()
+ {
+ current.AddRange(new[]
+ {
+ new HitCircle { StartTime = 500 },
+ new HitCircle { StartTime = 1000 },
+ new HitCircle { StartTime = 1500 },
+ new HitCircle { StartTime = 2000 },
+ new HitCircle { StartTime = 2250 },
+ new HitCircle { StartTime = 2500 },
+ new HitCircle { StartTime = 3000 },
+ new HitCircle { StartTime = 3500 },
+ });
+
+ var patch = new OsuBeatmap
+ {
+ HitObjects =
+ {
+ new HitCircle { StartTime = 750 },
+ (OsuHitObject)current.HitObjects[1],
+ (OsuHitObject)current.HitObjects[4],
+ (OsuHitObject)current.HitObjects[5],
+ new HitCircle { StartTime = 2650 },
+ new HitCircle { StartTime = 2750 },
+ new HitCircle { StartTime = 4000 },
+ }
+ };
+
+ runTest(patch);
+ }
+
+ private void runTest(IBeatmap patch)
+ {
+ // Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder.
+ // This causes issues because the decoder adds various default properties (e.g. new combo on first object, default samples).
+ // To resolve "patch" into a sane state it is encoded and then re-decoded.
+ patch = decode(encode(patch));
+
+ // Apply the patch.
+ patcher.Patch(encode(current), encode(patch));
+
+ // Convert beatmaps to strings for assertion purposes.
+ string currentStr = Encoding.ASCII.GetString(encode(current));
+ string patchStr = Encoding.ASCII.GetString(encode(patch));
+
+ Assert.That(currentStr, Is.EqualTo(patchStr));
+ }
+
+ private byte[] encode(IBeatmap beatmap)
+ {
+ using (var encoded = new MemoryStream())
+ {
+ using (var sw = new StreamWriter(encoded))
+ new LegacyBeatmapEncoder(beatmap).Encode(sw);
+
+ return encoded.ToArray();
+ }
+ }
+
+ private IBeatmap decode(byte[] state)
+ {
+ using (var stream = new MemoryStream(state))
+ using (var reader = new LineBufferedReader(stream))
+ return Decoder.GetDecoder(reader).Decode(reader);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
similarity index 99%
rename from osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
rename to osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 3cb5909ba9..168ec0f09d 100644
--- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
-namespace osu.Game.Tests.Editor
+namespace osu.Game.Tests.Editing
{
[HeadlessTest]
public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
new file mode 100644
index 0000000000..f611f2717e
--- /dev/null
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -0,0 +1,346 @@
+// 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.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.IO.Stores;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Resources;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneHitObjectSamples : PlayerTestScene
+ {
+ private readonly SkinInfo userSkinInfo = new SkinInfo();
+
+ private readonly BeatmapInfo beatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo(),
+ Metadata = new BeatmapMetadata
+ {
+ Author = User.SYSTEM_USER
+ }
+ };
+
+ private readonly TestResourceStore userSkinResourceStore = new TestResourceStore();
+ private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
+
+ protected override bool HasCustomSteps => true;
+
+ public TestSceneHitObjectSamples()
+ : base(new OsuRuleset())
+ {
+ }
+
+ private SkinSourceDependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
+
+ ///
+ /// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin.
+ ///
+ [Test]
+ public void TestDefaultSampleFromUserSkin()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-skin-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the beatmap skin.
+ ///
+ [Test]
+ public void TestDefaultSampleFromBeatmap()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the user skin when the beatmap does not contain the sample.
+ ///
+ [Test]
+ public void TestDefaultSampleFromUserSkinFallback()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(null, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
+ /// normal-hitnormal2
+ /// normal-hitnormal
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
+ {
+ setupSkins(expectedSample, expectedSample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ assertBeatmapLookup(expectedSample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
+ /// normal-hitnormal2
+ /// normal-hitnormal
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
+ {
+ setupSkins(string.Empty, expectedSample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ assertUserLookup(expectedSample);
+ }
+
+ ///
+ /// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
+ ///
+ [Test]
+ public void TestFileSampleFromBeatmap()
+ {
+ const string expected_sample = "hit_1.wav";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("file-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a default hitobject and control point causes .
+ ///
+ [Test]
+ public void TestControlPointSampleFromSkin()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("controlpoint-skin-sample.osu");
+
+ assertUserLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a control point that provides a custom sample set of 1 causes .
+ ///
+ [Test]
+ public void TestControlPointSampleFromBeatmap()
+ {
+ const string expected_sample = "normal-hitnormal";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("controlpoint-beatmap-sample.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that a control point that provides a custom sample of 2 causes .
+ ///
+ [TestCase("normal-hitnormal2")]
+ [TestCase("normal-hitnormal")]
+ public void TestControlPointCustomSampleFromBeatmap(string sampleName)
+ {
+ setupSkins(sampleName, sampleName);
+
+ createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
+
+ assertBeatmapLookup(sampleName);
+ }
+
+ ///
+ /// Tests that a hitobject's custom sample overrides the control point's.
+ ///
+ [Test]
+ public void TestHitObjectCustomSampleOverride()
+ {
+ const string expected_sample = "normal-hitnormal3";
+
+ setupSkins(expected_sample, expected_sample);
+
+ createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
+
+ assertBeatmapLookup(expected_sample);
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
+
+ private IBeatmap currentTestBeatmap;
+
+ private void createTestWithBeatmap(string filename)
+ {
+ CreateTest(() =>
+ {
+ AddStep("clear performed lookups", () =>
+ {
+ userSkinResourceStore.PerformedLookups.Clear();
+ beatmapSkinResourceStore.PerformedLookups.Clear();
+ });
+
+ AddStep($"load {filename}", () =>
+ {
+ using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}")))
+ currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader);
+ });
+ });
+ }
+
+ private void setupSkins(string beatmapFile, string userFile)
+ {
+ AddStep("setup skins", () =>
+ {
+ userSkinInfo.Files = new List
+ {
+ new SkinFileInfo
+ {
+ Filename = userFile,
+ FileInfo = new IO.FileInfo { Hash = userFile }
+ }
+ };
+
+ beatmapInfo.BeatmapSet.Files = new List
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = beatmapFile,
+ FileInfo = new IO.FileInfo { Hash = beatmapFile }
+ }
+ };
+
+ // Need to refresh the cached skin source to refresh the skin resource store.
+ dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
+ });
+ }
+
+ private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin",
+ () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name));
+
+ private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
+ () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
+
+ private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
+ {
+ public ISkinSource SkinSource;
+
+ private readonly IReadOnlyDependencyContainer fallback;
+
+ public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback)
+ {
+ this.fallback = fallback;
+ }
+
+ public object Get(Type type)
+ {
+ if (type == typeof(ISkinSource))
+ return SkinSource;
+
+ return fallback.Get(type);
+ }
+
+ public object Get(Type type, CacheInfo info)
+ {
+ if (type == typeof(ISkinSource))
+ return SkinSource;
+
+ return fallback.Get(type, info);
+ }
+
+ public void Inject(T instance) where T : class
+ {
+ // Never used directly
+ }
+ }
+
+ private class TestResourceStore : IResourceStore
+ {
+ public readonly List PerformedLookups = new List();
+
+ public byte[] Get(string name)
+ {
+ markLookup(name);
+ return Array.Empty();
+ }
+
+ public Task GetAsync(string name)
+ {
+ markLookup(name);
+ return Task.FromResult(Array.Empty());
+ }
+
+ public Stream GetStream(string name)
+ {
+ markLookup(name);
+ return new MemoryStream();
+ }
+
+ private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1));
+
+ public IEnumerable GetAvailableResources() => Enumerable.Empty();
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly BeatmapInfo skinBeatmapInfo;
+ private readonly IResourceStore resourceStore;
+
+ public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
+ double length = 60000)
+ : base(beatmap, storyboard, referenceClock, audio, length)
+ {
+ this.skinBeatmapInfo = skinBeatmapInfo;
+ this.resourceStore = resourceStore;
+ }
+
+ protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 2782e902fe..158954106d 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
- cpi.Add(1000, new TimingControlPoint()); // is redundant
+ cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+
+ cpi.Add(1000, new TimingControlPoint()); //is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
@@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
+ cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
new file mode 100644
index 0000000000..1e77d50115
--- /dev/null
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -0,0 +1,117 @@
+// 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.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Online
+{
+ [HeadlessTest]
+ public class TestDummyAPIRequestHandling : OsuTestScene
+ {
+ [Test]
+ public void TestGenericRequestHandling()
+ {
+ AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case CommentVoteRequest cRequest:
+ cRequest.TriggerSuccess(new CommentBundle());
+ break;
+ }
+ });
+
+ CommentVoteRequest request = null;
+ CommentBundle response = null;
+
+ AddStep("fire request", () =>
+ {
+ response = null;
+ request = new CommentVoteRequest(1, CommentVoteAction.Vote);
+ request.Success += res => response = res;
+ API.Queue(request);
+ });
+
+ AddAssert("response event fired", () => response != null);
+
+ AddAssert("request has response", () => request.Result == response);
+ }
+
+ [Test]
+ public void TestQueueRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.Queue(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ [Test]
+ public void TestPerformRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.Perform(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ [Test]
+ public void TestPerformAsyncRequestHandling()
+ {
+ registerHandler();
+
+ LeaveChannelRequest request;
+ bool gotResponse = false;
+
+ AddStep("fire request", () =>
+ {
+ gotResponse = false;
+ request = new LeaveChannelRequest(new Channel(), new User());
+ request.Success += () => gotResponse = true;
+ API.PerformAsync(request);
+ });
+
+ AddAssert("response event fired", () => gotResponse);
+ }
+
+ private void registerHandler()
+ {
+ AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case LeaveChannelRequest cRequest:
+ cRequest.TriggerSuccess();
+ break;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu
new file mode 100644
index 0000000000..91dbc6a60e
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu
new file mode 100644
index 0000000000..3274820100
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,1,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu
new file mode 100644
index 0000000000..c53ec465fb
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,0,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu
new file mode 100644
index 0000000000..65b5ea8707
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+255,193,2170,1,0,0:0:0:0:hit_1.wav
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu
new file mode 100644
index 0000000000..13dc2faab1
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu
@@ -0,0 +1,7 @@
+osu file format v14
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:3:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu
new file mode 100644
index 0000000000..4ab672dbb0
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:2:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu
new file mode 100644
index 0000000000..33bc34949a
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:1:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu
new file mode 100644
index 0000000000..47f5b44c90
--- /dev/null
+++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
index c1f0dab8e9..d39a3e8548 100644
--- a/osu.Game.Tests/Resources/hitobject-combo-offset.osu
+++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu
@@ -5,27 +5,27 @@ osu file format v14
255,193,1000,49,0,0:0:0:0:
// Combo index = 4
-// Slider with new combo followed by circle with no new combo
+// Spinner with new combo followed by circle with no new combo
256,192,2000,12,0,2000,0:0:0:0:
255,193,3000,1,0,0:0:0:0:
// Combo index = 5
-// Slider without new combo followed by circle with no new combo
+// Spinner without new combo followed by circle with no new combo
256,192,4000,8,0,5000,0:0:0:0:
255,193,6000,1,0,0:0:0:0:
// Combo index = 5
-// Slider without new combo followed by circle with new combo
+// Spinner without new combo followed by circle with new combo
256,192,7000,8,0,8000,0:0:0:0:
255,193,9000,5,0,0:0:0:0:
// Combo index = 6
-// Slider with new combo and offset (1) followed by circle with new combo and offset (3)
+// Spinner with new combo and offset (1) followed by circle with new combo and offset (3)
256,192,10000,28,0,11000,0:0:0:0:
255,193,12000,53,0,0:0:0:0:
// Combo index = 11
-// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo
+// Spinner with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo
256,192,13000,44,0,14000,0:0:0:0:
256,192,15000,8,0,16000,0:0:0:0:
255,193,17000,1,0,0:0:0:0:
diff --git a/osu.Game.Tests/Resources/sample-beatmap-catch.osu b/osu.Game.Tests/Resources/sample-beatmap-catch.osu
new file mode 100644
index 0000000000..09ef762e3e
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-catch.osu
@@ -0,0 +1,30 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+32,183,24,5,0,0:0:0:0:
+106,123,200,1,10,0:0:0:0:
+199,108,376,1,2,0:0:0:0:
+305,105,553,5,4,0:0:0:0:
+386,112,729,1,14,0:0:0:0:
+486,197,906,5,12,0:0:0:0:
+14,199,1082,2,0,L|473:198,1,449.999988079071
+14,199,1700,6,6,P|248:33|490:222,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:
+10,190,2494,2,8,B|252:29|254:335|468:167,1,449.999988079071,10|12,0:0|0:0,0:0:0:0:
+256,192,3112,12,0,3906,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/sample-beatmap-mania.osu b/osu.Game.Tests/Resources/sample-beatmap-mania.osu
new file mode 100644
index 0000000000..04d6a31ab6
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-mania.osu
@@ -0,0 +1,39 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 3
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+51,192,24,1,0,0:0:0:0:
+153,192,200,1,0,0:0:0:0:
+358,192,376,1,0,0:0:0:0:
+460,192,553,1,0,0:0:0:0:
+460,192,729,128,0,1435:0:0:0:0:
+358,192,906,128,0,1612:0:0:0:0:
+256,192,1082,128,0,1788:0:0:0:0:
+153,192,1259,128,0,1965:0:0:0:0:
+51,192,1435,128,0,2141:0:0:0:0:
+51,192,2318,1,12,0:0:0:0:
+153,192,2318,1,4,0:0:0:0:
+256,192,2318,1,6,0:0:0:0:
+358,192,2318,1,14,0:0:0:0:
+460,192,2318,1,0,0:0:0:0:
+51,192,2494,128,0,2582:0:0:0:0:
+153,192,2494,128,14,2582:0:0:0:0:
+256,192,2494,128,6,2582:0:0:0:0:
+358,192,2494,128,4,2582:0:0:0:0:
+460,192,2494,128,12,2582:0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/sample-beatmap-osu.osu b/osu.Game.Tests/Resources/sample-beatmap-osu.osu
new file mode 100644
index 0000000000..27c96077e6
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-osu.osu
@@ -0,0 +1,32 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 0
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+98,69,24,1,0,0:0:0:0:
+419,72,200,1,2,0:0:0:0:
+81,314,376,1,6,0:0:0:0:
+423,321,553,1,12,0:0:0:0:
+86,192,729,2,0,P|459:193|460:193,1,359.999990463257
+86,192,1259,2,0,P|246:82|453:203,1,449.999988079071
+86,192,1876,2,0,B|256:30|257:313|464:177,1,359.999990463257
+86,55,2406,2,12,B|447:51|447:51|452:348|452:348|78:344,1,989.999973773957,14|2,0:0|0:0,0:0:0:0:
+256,192,3553,12,0,4259,0:0:0:0:
+67,57,4435,5,0,0:0:0:0:
+440,52,4612,5,0,0:0:0:0:
+86,181,4788,6,0,L|492:183,1,359.999990463257
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/sample-beatmap-taiko.osu b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu
new file mode 100644
index 0000000000..94b4288336
--- /dev/null
+++ b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu
@@ -0,0 +1,42 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+StackLeniency: 0.7
+Mode: 1
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:5
+OverallDifficulty:8
+ApproachRate:8
+SliderMultiplier:3.59999990463257
+SliderTickRate:2
+
+[TimingPoints]
+24,352.941176470588,4,1,1,100,1,0
+6376,-50,4,1,1,100,0,0
+
+[HitObjects]
+231,129,24,1,0,0:0:0:0:
+231,129,200,1,0,0:0:0:0:
+231,129,376,1,0,0:0:0:0:
+231,129,553,1,0,0:0:0:0:
+231,129,729,1,0,0:0:0:0:
+373,132,906,1,4,0:0:0:0:
+373,132,1082,1,4,0:0:0:0:
+373,132,1259,1,4,0:0:0:0:
+373,132,1435,1,4,0:0:0:0:
+231,129,1788,1,8,0:0:0:0:
+231,129,1964,1,8,0:0:0:0:
+231,129,2140,1,8,0:0:0:0:
+231,129,2317,1,8,0:0:0:0:
+231,129,2493,1,8,0:0:0:0:
+373,132,2670,1,12,0:0:0:0:
+373,132,2846,1,12,0:0:0:0:
+373,132,3023,1,12,0:0:0:0:
+373,132,3199,1,12,0:0:0:0:
+51,189,3553,2,0,L|150:188,1,89.9999976158143
+52,191,3906,2,0,L|512:189,1,449.999988079071
+26,196,4612,2,4,L|501:195,1,449.999988079071
+17,242,5318,2,10,P|250:69|495:243,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
new file mode 100644
index 0000000000..64d1024efb
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -0,0 +1,57 @@
+// 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.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Rulesets.Scoring
+{
+ public class ScoreProcessorTest
+ {
+ private ScoreProcessor scoreProcessor;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void SetUp()
+ {
+ scoreProcessor = new ScoreProcessor();
+ beatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = new List
+ {
+ new HitCircle()
+ }
+ };
+ }
+
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, 100)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, 300)]
+ public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
+ {
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
+ {
+ Type = hitResult
+ };
+ scoreProcessor.ApplyResult(judgementResult);
+
+ Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index fd7a5980f3..f6e69fd8bf 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -14,7 +14,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index a8830824c0..6f5655006e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index f49256a633..417d16fdb0 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -13,7 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDistanceSnapGrid : EditorClockTestScene
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
new file mode 100644
index 0000000000..20862e9cac
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs
@@ -0,0 +1,168 @@
+// 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.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneEditorChangeStates : EditorTestScene
+ {
+ public TestSceneEditorChangeStates()
+ : base(new OsuRuleset())
+ {
+ }
+
+ private EditorBeatmap editorBeatmap;
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single());
+ }
+
+ [Test]
+ public void TestUndoFromInitialState()
+ {
+ int hitObjectCount = 0;
+
+ AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
+
+ addUndoSteps();
+
+ AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
+ }
+
+ [Test]
+ public void TestRedoFromInitialState()
+ {
+ int hitObjectCount = 0;
+
+ AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
+
+ addRedoSteps();
+
+ AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
+ }
+
+ [Test]
+ public void TestAddObjectAndUndo()
+ {
+ HitObject addedObject = null;
+ HitObject removedObject = null;
+ HitObject expectedObject = null;
+
+ AddStep("bind removal", () =>
+ {
+ editorBeatmap.HitObjectAdded += h => addedObject = h;
+ editorBeatmap.HitObjectRemoved += h => removedObject = h;
+ });
+
+ AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
+ AddAssert("hitobject added", () => addedObject == expectedObject);
+
+ addUndoSteps();
+ AddAssert("hitobject removed", () => removedObject == expectedObject);
+ }
+
+ [Test]
+ public void TestAddObjectThenUndoThenRedo()
+ {
+ HitObject addedObject = null;
+ HitObject removedObject = null;
+ HitObject expectedObject = null;
+
+ AddStep("bind removal", () =>
+ {
+ editorBeatmap.HitObjectAdded += h => addedObject = h;
+ editorBeatmap.HitObjectRemoved += h => removedObject = h;
+ });
+
+ AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
+ addUndoSteps();
+
+ AddStep("reset variables", () =>
+ {
+ addedObject = null;
+ removedObject = null;
+ });
+
+ addRedoSteps();
+ AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
+ AddAssert("no hitobject removed", () => removedObject == null);
+ }
+
+ [Test]
+ public void TestRemoveObjectThenUndo()
+ {
+ HitObject addedObject = null;
+ HitObject removedObject = null;
+ HitObject expectedObject = null;
+
+ AddStep("bind removal", () =>
+ {
+ editorBeatmap.HitObjectAdded += h => addedObject = h;
+ editorBeatmap.HitObjectRemoved += h => removedObject = h;
+ });
+
+ AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
+ AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
+ AddStep("reset variables", () =>
+ {
+ addedObject = null;
+ removedObject = null;
+ });
+
+ addUndoSteps();
+ AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
+ AddAssert("no hitobject removed", () => removedObject == null);
+ }
+
+ [Test]
+ public void TestRemoveObjectThenUndoThenRedo()
+ {
+ HitObject addedObject = null;
+ HitObject removedObject = null;
+ HitObject expectedObject = null;
+
+ AddStep("bind removal", () =>
+ {
+ editorBeatmap.HitObjectAdded += h => addedObject = h;
+ editorBeatmap.HitObjectRemoved += h => removedObject = h;
+ });
+
+ AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
+ AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
+ addUndoSteps();
+
+ AddStep("reset variables", () =>
+ {
+ addedObject = null;
+ removedObject = null;
+ });
+
+ addRedoSteps();
+ AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo)
+ AddAssert("no hitobject added", () => addedObject == null);
+ }
+
+ private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo());
+
+ private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo());
+
+ protected override Editor CreateEditor() => new TestEditor();
+
+ private class TestEditor : Editor
+ {
+ public new void Undo() => base.Undo();
+
+ public new void Redo() => base.Redo();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
similarity index 97%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
index 1709067d5d..2deeaef1f6 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
@@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Components.RadioButtons;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
index 53c2d62067..2cbdacb61c 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
@@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Components.Menus;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorMenuBar : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index 3118e0cabe..41d1459103 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -13,7 +13,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorSeekSnapping : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index 2e04eb50ca..c92423545d 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index e41c2427fb..ddaca26220 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneHitObjectComposer : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
rename to osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
index 0d4fe4366d..3af976cae0 100644
--- a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
@@ -9,7 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Screens.Edit.Components;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestScenePlaybackControl : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
index 4d8f877575..5ab2f49b4a 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs
@@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
similarity index 95%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
index 43a3cd6122..e33040acdc 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
@@ -8,7 +8,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
similarity index 96%
rename from osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index ae09a7fa47..a6dbe9571e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
index e2762f3d5f..0c1296b82c 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
@@ -14,7 +14,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneWaveform : OsuTestScene
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 19d19c2759..082268d824 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -15,7 +15,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
similarity index 99%
rename from osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
rename to osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 7081eb3af5..56b2860e96 100644
--- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -18,7 +18,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
{
public abstract class TimelineTestScene : EditorClockTestScene
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
new file mode 100644
index 0000000000..512584bd42
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -0,0 +1,139 @@
+// 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.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Track;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Storyboards;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneCompletionCancellation : PlayerTestScene
+ {
+ private Track track;
+
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ private int resultsDisplayWaitCount =>
+ (int)((Screens.Play.Player.RESULTS_DISPLAY_DELAY / TimePerAction) * 2);
+
+ protected override bool AllowFail => false;
+
+ public TestSceneCompletionCancellation()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ // Ensure track has actually running before attempting to seek
+ AddUntilStep("wait for track to start running", () => track.IsRunning);
+ }
+
+ [Test]
+ public void TestCancelCompletionOnRewind()
+ {
+ complete();
+ cancel();
+
+ checkNoRanking();
+ }
+
+ [Test]
+ public void TestReCompleteAfterCancellation()
+ {
+ complete();
+ cancel();
+ complete();
+
+ AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked);
+ }
+
+ ///
+ /// Tests whether can still pause after cancelling completion by reverting back to true.
+ ///
+ [Test]
+ public void TestCanPauseAfterCancellation()
+ {
+ complete();
+ cancel();
+
+ AddStep("pause", () => Player.Pause());
+ AddAssert("paused successfully", () => Player.GameplayClockContainer.IsPaused.Value);
+
+ checkNoRanking();
+ }
+
+ private void complete()
+ {
+ AddStep("seek to completion", () => track.Seek(5000));
+ AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private void cancel()
+ {
+ AddStep("rewind to cancel", () => track.Seek(4000));
+ AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private void checkNoRanking()
+ {
+ // wait to ensure there was no attempt of pushing the results screen.
+ AddWaitStep("wait", resultsDisplayWaitCount);
+ AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked);
+ }
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ {
+ var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
+ track = working.Track;
+ return working;
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap();
+
+ for (int i = 1; i <= 19; i++)
+ {
+ beatmap.HitObjects.Add(new HitCircle
+ {
+ Position = new Vector2(256, 192),
+ StartTime = i * 250,
+ });
+ }
+
+ return beatmap;
+ }
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeRankingPushPlayer();
+
+ public class FakeRankingPushPlayer : TestPlayer
+ {
+ public bool GotoRankingInvoked;
+
+ public FakeRankingPushPlayer()
+ : base(true, true)
+ {
+ }
+
+ protected override void GotoRanking()
+ {
+ GotoRankingInvoked = true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
new file mode 100644
index 0000000000..a95e806862
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
@@ -0,0 +1,73 @@
+// 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.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneFailingLayer : OsuTestScene
+ {
+ private FailingLayer layer;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create layer", () =>
+ {
+ Child = layer = new FailingLayer();
+ layer.BindHealthProcessor(new DrainingHealthProcessor(1));
+ });
+
+ AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer is visible", () => layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerFading()
+ {
+ AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
+ {
+ if (layer != null)
+ layer.Current.Value = val;
+ });
+
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f);
+ AddStep("set health to 1", () => layer.Current.Value = 1f);
+ AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerDisabledViaConfig()
+ {
+ AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer is not visible", () => !layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerVisibilityWithAccumulatingProcessor()
+ {
+ AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1)));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddUntilStep("layer is not visible", () => !layer.IsPresent);
+ }
+
+ [Test]
+ public void TestLayerVisibilityWithDrainingProcessor()
+ {
+ AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1)));
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+ AddWaitStep("wait for potential fade", 10);
+ AddAssert("layer is still visible", () => layer.IsPresent);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
new file mode 100644
index 0000000000..db65e91d17
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
@@ -0,0 +1,98 @@
+// 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.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneKeyBindings : OsuManualInputManagerTestScene
+ {
+ private readonly ActionReceiver receiver;
+
+ public TestSceneKeyBindings()
+ {
+ Add(new TestKeyBindingContainer
+ {
+ Child = receiver = new ActionReceiver()
+ });
+ }
+
+ [Test]
+ public void TestDefaultsWhenNotDatabased()
+ {
+ AddStep("fire key", () =>
+ {
+ InputManager.PressKey(Key.A);
+ InputManager.ReleaseKey(Key.A);
+ });
+
+ AddAssert("received key", () => receiver.ReceivedAction);
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override IEnumerable GetModsFor(ModType type) =>
+ throw new System.NotImplementedException();
+
+ 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 IEnumerable GetDefaultKeyBindings(int variant = 0)
+ {
+ return new[]
+ {
+ new KeyBinding(InputKey.A, TestAction.Down),
+ };
+ }
+
+ public override string Description => "test";
+ public override string ShortName => "test";
+ }
+
+ private enum TestAction
+ {
+ Down,
+ }
+
+ private class TestKeyBindingContainer : DatabasedKeyBindingContainer
+ {
+ public TestKeyBindingContainer()
+ : base(new TestRuleset().RulesetInfo, 0)
+ {
+ }
+ }
+
+ private class ActionReceiver : CompositeDrawable, IKeyBindingHandler
+ {
+ public bool ReceivedAction;
+
+ public bool OnPressed(TestAction action)
+ {
+ ReceivedAction = action == TestAction.Down;
+ return true;
+ }
+
+ public void OnReleased(TestAction action)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index f24589ed35..8fbbc8ebd8 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -5,13 +5,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Overlays.Toolbar;
+using osu.Game.Rulesets;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
- public class TestSceneToolbar : OsuTestScene
+ public class TestSceneToolbar : OsuManualInputManagerTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
@@ -21,24 +25,62 @@ namespace osu.Game.Tests.Visual.Menus
typeof(ToolbarNotificationButton),
};
- public TestSceneToolbar()
+ private Toolbar toolbar;
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } };
+ });
+
+ [Test]
+ public void TestNotificationCounter()
{
- var toolbar = new Toolbar { State = { Value = Visibility.Visible } };
ToolbarNotificationButton notificationButton = null;
- AddStep("create toolbar", () =>
- {
- Add(toolbar);
- notificationButton = toolbar.Children.OfType().Last().Children.OfType().First();
- });
-
- void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);
+ AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType().Single());
setNotifications(1);
setNotifications(2);
setNotifications(3);
setNotifications(0);
setNotifications(144);
+
+ void setNotifications(int count)
+ => AddStep($"set notification count to {count}",
+ () => notificationButton.NotificationCount.Value = count);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestRulesetSwitchingShortcut(bool toolbarHidden)
+ {
+ ToolbarRulesetSelector rulesetSelector = null;
+
+ if (toolbarHidden)
+ AddStep("hide toolbar", () => toolbar.Hide());
+
+ AddStep("retrieve ruleset selector", () => rulesetSelector = toolbar.ChildrenOfType().Single());
+
+ for (int i = 0; i < 4; i++)
+ {
+ var expected = rulesets.AvailableRulesets.ElementAt(i);
+ var numberKey = Key.Number1 + i;
+
+ AddStep($"switch to ruleset {i} via shortcut", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.PressKey(numberKey);
+
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(numberKey);
+ });
+
+ AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected));
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
index 909409835c..27f5b29738 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
@@ -20,26 +20,30 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestFromMainMenu()
{
var firstImport = importBeatmap(1);
+ var secondimport = importBeatmap(3);
+
presentAndConfirm(firstImport);
-
- AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
- AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
-
- var secondimport = importBeatmap(2);
+ returnToMenu();
presentAndConfirm(secondimport);
+ returnToMenu();
+ presentSecondDifficultyAndConfirm(firstImport, 1);
+ returnToMenu();
+ presentSecondDifficultyAndConfirm(secondimport, 3);
}
[Test]
public void TestFromMainMenuDifferentRuleset()
{
var firstImport = importBeatmap(1);
+ var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
+
presentAndConfirm(firstImport);
-
- AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
- AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
-
- var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
+ returnToMenu();
presentAndConfirm(secondimport);
+ returnToMenu();
+ presentSecondDifficultyAndConfirm(firstImport, 1);
+ returnToMenu();
+ presentSecondDifficultyAndConfirm(secondimport, 3);
}
[Test]
@@ -48,8 +52,11 @@ namespace osu.Game.Tests.Visual.Navigation
var firstImport = importBeatmap(1);
presentAndConfirm(firstImport);
- var secondimport = importBeatmap(2);
+ var secondimport = importBeatmap(3);
presentAndConfirm(secondimport);
+
+ presentSecondDifficultyAndConfirm(firstImport, 1);
+ presentSecondDifficultyAndConfirm(secondimport, 3);
}
[Test]
@@ -58,8 +65,17 @@ namespace osu.Game.Tests.Visual.Navigation
var firstImport = importBeatmap(1);
presentAndConfirm(firstImport);
- var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo);
+ var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
presentAndConfirm(secondimport);
+
+ presentSecondDifficultyAndConfirm(firstImport, 1);
+ presentSecondDifficultyAndConfirm(secondimport, 3);
+ }
+
+ private void returnToMenu()
+ {
+ AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
+ AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
}
private Func importBeatmap(int i, RulesetInfo ruleset = null)
@@ -89,6 +105,13 @@ namespace osu.Game.Tests.Visual.Navigation
BaseDifficulty = difficulty,
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
},
+ new BeatmapInfo
+ {
+ OnlineBeatmapID = i * 2048,
+ Metadata = metadata,
+ BaseDifficulty = difficulty,
+ Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
+ },
}
}).Result;
});
@@ -106,5 +129,15 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.ID == getImport().ID);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID);
}
+
+ private void presentSecondDifficultyAndConfirm(Func getImport, int importedID)
+ {
+ Predicate pred = b => b.OnlineBeatmapID == importedID * 2048;
+ AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
+
+ AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
+ AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 2048);
+ AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 7c05d99c59..64d1a9ddcd 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using osu.Game.Overlays;
using NUnit.Framework;
+using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Tests.Visual.Online
{
@@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Online
public override IReadOnlyList RequiredTypes => new[]
{
typeof(BeatmapListingOverlay),
+ typeof(BeatmapListingFilterControl)
};
protected override bool UseOnlineAPI => true;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
index 864fd31a0f..22d20f7098 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
@@ -24,7 +24,6 @@ namespace osu.Game.Tests.Visual.Online
typeof(ChangelogListing),
typeof(ChangelogSingleBuild),
typeof(ChangelogBuild),
- typeof(Comments),
};
protected override bool UseOnlineAPI => true;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs
new file mode 100644
index 0000000000..df95f24686
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Dashboard;
+using osu.Game.Overlays.Dashboard.Friends;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneDashboardOverlay : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DashboardOverlay),
+ typeof(DashboardOverlayHeader),
+ typeof(FriendDisplay)
+ };
+
+ protected override bool UseOnlineAPI => true;
+
+ private readonly DashboardOverlay overlay;
+
+ public TestSceneDashboardOverlay()
+ {
+ Add(overlay = new DashboardOverlay());
+ }
+
+ [Test]
+ public void TestShow()
+ {
+ AddStep("Show", overlay.Show);
+ }
+
+ [Test]
+ public void TestHide()
+ {
+ AddStep("Hide", overlay.Hide);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
index 5b0c2d3c67..9fe873cb6a 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
@@ -9,7 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
using osuTK;
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(PanelDownloadButton)
+ typeof(BeatmapPanelDownloadButton)
};
private TestDownloadButton downloadButton;
@@ -143,14 +143,14 @@ namespace osu.Game.Tests.Visual.Online
return beatmap;
}
- private class TestDownloadButton : PanelDownloadButton
+ private class TestDownloadButton : BeatmapPanelDownloadButton
{
public new bool DownloadEnabled => base.DownloadEnabled;
public DownloadState DownloadState => State.Value;
- public TestDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
- : base(beatmapSet, noVideo)
+ public TestDownloadButton(BeatmapSetInfo beatmapSet)
+ : base(beatmapSet)
{
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs
deleted file mode 100644
index d9873ea243..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs
+++ /dev/null
@@ -1,215 +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.Collections.Generic;
-using NUnit.Framework;
-using osu.Game.Beatmaps;
-using osu.Game.Overlays;
-
-namespace osu.Game.Tests.Visual.Online
-{
- [TestFixture]
- public class TestSceneDirectOverlay : OsuTestScene
- {
- private DirectOverlay direct;
-
- protected override bool UseOnlineAPI => true;
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Add(direct = new DirectOverlay());
- newBeatmaps();
-
- AddStep(@"toggle", direct.ToggleVisibility);
- AddStep(@"result counts", () => direct.ResultAmounts = new DirectOverlay.ResultCounts(1, 4, 13));
- AddStep(@"trigger disabled", () => Ruleset.Disabled = !Ruleset.Disabled);
- }
-
- private void newBeatmaps()
- {
- direct.BeatmapSets = new[]
- {
- new BeatmapSetInfo
- {
- OnlineBeatmapSetID = 578332,
- Metadata = new BeatmapMetadata
- {
- Title = @"OrVid",
- Artist = @"An",
- AuthorString = @"RLC",
- Source = @"",
- Tags = @"acuticnotes an-fillnote revid tear tearvid encrpted encryption axi axivid quad her hervid recoll",
- },
- OnlineInfo = new BeatmapSetOnlineInfo
- {
- Covers = new BeatmapSetOnlineCovers
- {
- Card = @"https://assets.ppy.sh/beatmaps/578332/covers/card.jpg?1494591390",
- Cover = @"https://assets.ppy.sh/beatmaps/578332/covers/cover.jpg?1494591390",
- },
- Preview = @"https://b.ppy.sh/preview/578332.mp3",
- PlayCount = 97,
- FavouriteCount = 72,
- },
- Beatmaps = new List
- {
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 5.35f,
- Metadata = new BeatmapMetadata(),
- },
- },
- },
- new BeatmapSetInfo
- {
- OnlineBeatmapSetID = 599627,
- Metadata = new BeatmapMetadata
- {
- Title = @"tiny lamp",
- Artist = @"fhana",
- AuthorString = @"Sotarks",
- Source = @"ぎんぎつね",
- Tags = @"lantis junichi sato yuxuki waga kevin mitsunaga towana gingitsune opening op full ver version kalibe collab collaboration",
- },
- OnlineInfo = new BeatmapSetOnlineInfo
- {
- Covers = new BeatmapSetOnlineCovers
- {
- Card = @"https://assets.ppy.sh/beatmaps/599627/covers/card.jpg?1494539318",
- Cover = @"https://assets.ppy.sh/beatmaps/599627/covers/cover.jpg?1494539318",
- },
- Preview = @"https//b.ppy.sh/preview/599627.mp3",
- PlayCount = 3082,
- FavouriteCount = 14,
- },
- Beatmaps = new List
- {
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 5.81f,
- Metadata = new BeatmapMetadata(),
- },
- },
- },
- new BeatmapSetInfo
- {
- OnlineBeatmapSetID = 513268,
- Metadata = new BeatmapMetadata
- {
- Title = @"At Gwanghwamun",
- Artist = @"KYUHYUN",
- AuthorString = @"Cerulean Veyron",
- Source = @"",
- Tags = @"soul ballad kh super junior sj suju 슈퍼주니어 kt뮤직 sm엔터테인먼트 s.m.entertainment kt music 1st mini album ep",
- },
- OnlineInfo = new BeatmapSetOnlineInfo
- {
- Covers = new BeatmapSetOnlineCovers
- {
- Card = @"https://assets.ppy.sh/beatmaps/513268/covers/card.jpg?1494502863",
- Cover = @"https://assets.ppy.sh/beatmaps/513268/covers/cover.jpg?1494502863",
- },
- Preview = @"https//b.ppy.sh/preview/513268.mp3",
- PlayCount = 2762,
- FavouriteCount = 15,
- },
- Beatmaps = new List
- {
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 0.9f,
- Metadata = new BeatmapMetadata(),
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 1.1f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 2.02f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 3.49f,
- },
- },
- },
- new BeatmapSetInfo
- {
- OnlineBeatmapSetID = 586841,
- Metadata = new BeatmapMetadata
- {
- Title = @"RHAPSODY OF BLUE SKY",
- Artist = @"fhana",
- AuthorString = @"[Kamiya]",
- Source = @"小林さんちのメイドラゴン",
- Tags = @"kobayashi san chi no maidragon aozora no opening anime maid dragon oblivion karen dynamix imoutosan pata-mon gxytcgxytc",
- },
- OnlineInfo = new BeatmapSetOnlineInfo
- {
- Covers = new BeatmapSetOnlineCovers
- {
- Card = @"https://assets.ppy.sh/beatmaps/586841/covers/card.jpg?1494052741",
- Cover = @"https://assets.ppy.sh/beatmaps/586841/covers/cover.jpg?1494052741",
- },
- Preview = @"https//b.ppy.sh/preview/586841.mp3",
- PlayCount = 62317,
- FavouriteCount = 161,
- },
- Beatmaps = new List
- {
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 1.26f,
- Metadata = new BeatmapMetadata(),
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 2.01f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 2.87f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 3.76f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 3.93f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 4.37f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 5.13f,
- },
- new BeatmapInfo
- {
- Ruleset = Ruleset.Value,
- StarDifficulty = 5.42f,
- },
- },
- },
- };
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
index cb08cded37..d6ed654bac 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets;
using osu.Game.Users;
using osuTK;
@@ -20,8 +20,8 @@ namespace osu.Game.Tests.Visual.Online
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(DirectGridPanel),
- typeof(DirectListPanel),
+ typeof(GridBeatmapPanel),
+ typeof(ListBeatmapPanel),
typeof(IconPill)
};
@@ -126,12 +126,12 @@ namespace osu.Game.Tests.Visual.Online
Spacing = new Vector2(5, 20),
Children = new Drawable[]
{
- new DirectGridPanel(normal),
- new DirectGridPanel(undownloadable),
- new DirectGridPanel(manyDifficulties),
- new DirectListPanel(normal),
- new DirectListPanel(undownloadable),
- new DirectListPanel(manyDifficulties),
+ new GridBeatmapPanel(normal),
+ new GridBeatmapPanel(undownloadable),
+ new GridBeatmapPanel(manyDifficulties),
+ new ListBeatmapPanel(normal),
+ new ListBeatmapPanel(undownloadable),
+ new ListBeatmapPanel(manyDifficulties),
},
},
};
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
index cf365a7614..0b5ff1c960 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
@@ -10,6 +10,7 @@ using osu.Game.Users;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using NUnit.Framework;
+using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online
{
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
- private FriendDisplay display;
+ private TestFriendDisplay display;
[SetUp]
public void Setup() => Schedule(() =>
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
Child = new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
- Child = display = new FriendDisplay()
+ Child = display = new TestFriendDisplay()
};
});
@@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online
LastVisit = DateTimeOffset.Now
}
};
+
+ private class TestFriendDisplay : FriendDisplay
+ {
+ public void Fetch()
+ {
+ base.APIStateChanged(API, APIState.Online);
+ }
+
+ public override void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
new file mode 100644
index 0000000000..103308d34d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
@@ -0,0 +1,85 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Chat;
+using osu.Game.Rulesets;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [HeadlessTest]
+ public class TestSceneNowPlayingCommand : OsuTestScene
+ {
+ [Cached(typeof(IChannelPostTarget))]
+ private PostTarget postTarget { get; set; }
+
+ public TestSceneNowPlayingCommand()
+ {
+ Add(postTarget = new PostTarget());
+ }
+
+ [Test]
+ public void TestGenericActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening"));
+ }
+
+ [Test]
+ public void TestEditActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing"));
+ }
+
+ [Test]
+ public void TestPlayActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo()));
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing"));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestLinkPresence(bool hasOnlineId)
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
+
+ AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null)
+ {
+ BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
+ });
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ if (hasOnlineId)
+ AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234"));
+ else
+ AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://"));
+ }
+
+ public class PostTarget : Component, IChannelPostTarget
+ {
+ public void PostMessage(string text, bool isAction = false, Channel target = null)
+ {
+ LastMessage = text;
+ }
+
+ public string LastMessage { get; private set; }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 76a8ee9914..f68ed4154b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -54,6 +54,35 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets;
}
+ [Test]
+ public void TestRecommendedSelection()
+ {
+ loadBeatmaps();
+
+ AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
+
+ // check recommended was selected
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(1, 3);
+
+ // change away from recommended
+ advanceSelection(direction: -1, diff: true);
+ waitForSelection(1, 2);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(2, 3);
+
+ // next set, check recommended
+ advanceSelection(direction: 1, diff: false);
+ waitForSelection(3, 3);
+
+ // go back to first set and ensure user selection was retained
+ advanceSelection(direction: -1, diff: false);
+ advanceSelection(direction: -1, diff: false);
+ waitForSelection(1, 2);
+ }
+
///
/// Test keyboard traversal
///
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 4405c75744..39e04ed39a 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
+ [Test]
+ public void TestPresentNewRulesetNewBeatmap()
+ {
+ createSongSelect();
+ changeRuleset(2);
+
+ addRulesetImportStep(2);
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ BeatmapInfo target = null;
+
+ AddStep("select beatmap/ruleset externally", () =>
+ {
+ target = manager.GetAllUsableBeatmapSets()
+ .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
+
+ Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
+ Beatmap.Value = manager.GetWorkingBeatmap(target);
+ });
+
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
+
+ // this is an important check, to make sure updateComponentFromBeatmap() was actually run
+ AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
+ }
+
+ [Test]
+ public void TestPresentNewBeatmapNewRuleset()
+ {
+ createSongSelect();
+ changeRuleset(2);
+
+ addRulesetImportStep(2);
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ BeatmapInfo target = null;
+
+ AddStep("select beatmap/ruleset externally", () =>
+ {
+ target = manager.GetAllUsableBeatmapSets()
+ .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
+
+ Beatmap.Value = manager.GetWorkingBeatmap(target);
+ Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
+ });
+
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
+
+ AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
+
+ // this is an important check, to make sure updateComponentFromBeatmap() was actually run
+ AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
+ }
+
[Test]
public void TestRulesetChangeResetsMods()
{
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index 492494ada3..2eaac2a45f 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Tests.Visual
typeof(IdleTracker),
typeof(OnScreenDisplay),
typeof(NotificationOverlay),
- typeof(DirectOverlay),
- typeof(SocialOverlay),
+ typeof(BeatmapListingOverlay),
+ typeof(DashboardOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
similarity index 69%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index 1d8db71527..d6ede950df 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -15,25 +15,27 @@ using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneBeatmapListingSearchSection : OsuTestScene
+ public class TestSceneBeatmapListingSearchControl : OsuTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(BeatmapListingSearchSection),
+ typeof(BeatmapListingSearchControl),
};
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
- private readonly BeatmapListingSearchSection section;
+ private readonly BeatmapListingSearchControl control;
- public TestSceneBeatmapListingSearchSection()
+ public TestSceneBeatmapListingSearchControl()
{
OsuSpriteText query;
OsuSpriteText ruleset;
OsuSpriteText category;
+ OsuSpriteText genre;
+ OsuSpriteText language;
- Add(section = new BeatmapListingSearchSection
+ Add(control = new BeatmapListingSearchControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -49,20 +51,24 @@ namespace osu.Game.Tests.Visual.UserInterface
query = new OsuSpriteText(),
ruleset = new OsuSpriteText(),
category = new OsuSpriteText(),
+ genre = new OsuSpriteText(),
+ language = new OsuSpriteText(),
}
});
- section.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
- section.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
- section.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
+ control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
+ control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
+ control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
+ control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
+ control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true);
}
[Test]
public void TestCovers()
{
- AddStep("Set beatmap", () => section.BeatmapSet = beatmap_set);
- AddStep("Set beatmap (no cover)", () => section.BeatmapSet = no_cover_beatmap_set);
- AddStep("Set null beatmap", () => section.BeatmapSet = null);
+ AddStep("Set beatmap", () => control.BeatmapSet = beatmap_set);
+ AddStep("Set beatmap (no cover)", () => control.BeatmapSet = no_cover_beatmap_set);
+ AddStep("Set null beatmap", () => control.BeatmapSet = null);
}
private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
similarity index 91%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
index a5fa085abf..f643d4e3fe 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
@@ -13,18 +13,17 @@ using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneBeatmapListingSort : OsuTestScene
+ public class TestSceneBeatmapListingSortTabControl : OsuTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(BeatmapListingSortTabControl),
typeof(OverlaySortTabControl<>),
};
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
- public TestSceneBeatmapListingSort()
+ public TestSceneBeatmapListingSortTabControl()
{
BeatmapListingSortTabControl control;
OsuSpriteText current;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs
index 7b4424e568..283fe03af3 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
-using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osuTK;
@@ -20,8 +19,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public override IReadOnlyList RequiredTypes => new[]
{
typeof(BeatmapSearchFilterRow<>),
- typeof(BeatmapSearchRulesetFilterRow),
- typeof(BeatmapSearchSmallFilterRow<>),
+ typeof(BeatmapSearchRulesetFilterRow)
};
[Cached]
@@ -42,8 +40,8 @@ namespace osu.Game.Tests.Visual.UserInterface
Children = new Drawable[]
{
new BeatmapSearchRulesetFilterRow(),
- new BeatmapSearchFilterRow("Categories"),
- new BeatmapSearchSmallFilterRow("Header Name")
+ new BeatmapSearchFilterRow("Categories"),
+ new BeatmapSearchFilterRow("Header Name")
}
});
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 2294cd6966..ec6ee6bc83 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -14,8 +14,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Mods.Sections;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mania;
-using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -117,8 +115,6 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestManiaMods()
{
changeRuleset(3);
-
- testRankedText(new ManiaRuleset().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom));
}
[Test]
@@ -217,15 +213,6 @@ namespace osu.Game.Tests.Visual.UserInterface
checkLabelColor(() => Color4.White);
}
- private void testRankedText(Mod mod)
- {
- AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
- selectNext(mod);
- AddUntilStep("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0);
- selectPrevious(mod);
- AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
- }
-
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
@@ -272,7 +259,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}
public new OsuSpriteText MultiplierLabel => base.MultiplierLabel;
- public new OsuSpriteText UnrankedLabel => base.UnrankedLabel;
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
index 2ea9aec50a..532744a0fc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs
@@ -1,12 +1,15 @@
// 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.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
+using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.UserInterface
@@ -21,9 +24,14 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay;
+ private RulesetStore rulesets;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio, GameHost host)
{
+ Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
+ Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
+
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
nowPlayingOverlay = new NowPlayingOverlay
@@ -44,21 +52,43 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"hide", () => nowPlayingOverlay.Hide());
}
+ private BeatmapManager manager { get; set; }
+
+ private int importId;
+
[Test]
public void TestPrevTrackBehavior()
{
- AddStep(@"Play track", () =>
+ // ensure we have at least two beatmaps available.
+ AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo
{
- musicController.NextTrack();
- currentBeatmap = Beatmap.Value;
- });
+ Beatmaps = new List
+ {
+ new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty(),
+ }
+ },
+ Metadata = new BeatmapMetadata
+ {
+ Artist = $"a test map {importId++}",
+ Title = "title",
+ }
+ }).Wait(), 5);
+
+ AddStep(@"Next track", () => musicController.NextTrack());
+ AddStep("Store track", () => currentBeatmap = Beatmap.Value);
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000);
- AddAssert(@"Check action is restart track", () => musicController.PreviousTrack() == PreviousTrackResult.Restart);
- AddUntilStep("Wait for current time to update", () => Precision.AlmostEquals(currentBeatmap.Track.CurrentTime, 0));
- AddAssert(@"Check track didn't change", () => currentBeatmap == Beatmap.Value);
- AddAssert(@"Check action is not restart", () => musicController.PreviousTrack() != PreviousTrackResult.Restart);
+
+ AddStep(@"Set previous", () => musicController.PreviousTrack());
+
+ AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
+ AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000);
+
+ AddStep(@"Set previous", () => musicController.PreviousTrack());
+ AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs
new file mode 100644
index 0000000000..9ea76c2c7b
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.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 System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneOsuMenu : OsuManualInputManagerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuMenu),
+ typeof(DrawableOsuMenuItem)
+ };
+
+ private OsuMenu menu;
+ private bool actionPerformed;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ actionPerformed = false;
+
+ Child = menu = new OsuMenu(Direction.Vertical, true)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Items = new[]
+ {
+ new OsuMenuItem("standard", MenuItemType.Standard, performAction),
+ new OsuMenuItem("highlighted", MenuItemType.Highlighted, performAction),
+ new OsuMenuItem("destructive", MenuItemType.Destructive, performAction),
+ }
+ };
+ });
+
+ [Test]
+ public void TestClickEnabledMenuItem()
+ {
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action performed", () => actionPerformed);
+ }
+
+ [Test]
+ public void TestDisableMenuItemsAndClick()
+ {
+ AddStep("disable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = true;
+ });
+
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action not performed", () => !actionPerformed);
+ }
+
+ [Test]
+ public void TestEnableMenuItemsAndClick()
+ {
+ AddStep("disable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = true;
+ });
+
+ AddStep("enable menu items", () =>
+ {
+ foreach (var item in menu.Items)
+ ((OsuMenuItem)item).Action.Disabled = false;
+ });
+
+ AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First()));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("action performed", () => actionPerformed);
+ }
+
+ private void performAction() => actionPerformed = true;
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs
new file mode 100644
index 0000000000..e9e63613c0
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs
@@ -0,0 +1,113 @@
+// 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.Containers;
+using osu.Game.Overlays;
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneOverlayScrollContainer : OsuManualInputManagerTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OverlayScrollContainer)
+ };
+
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
+ private TestScrollContainer scroll;
+
+ private int invocationCount;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = scroll = new TestScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new Container
+ {
+ Height = 3000,
+ RelativeSizeAxes = Axes.X,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Gray
+ }
+ }
+ };
+
+ invocationCount = 0;
+
+ scroll.Button.Action += () => invocationCount++;
+ });
+
+ [Test]
+ public void TestButtonVisibility()
+ {
+ AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
+
+ AddStep("scroll to end", () => scroll.ScrollToEnd(false));
+ AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
+
+ AddStep("scroll to start", () => scroll.ScrollToStart(false));
+ AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
+
+ AddStep("scroll to 500", () => scroll.ScrollTo(500));
+ AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f));
+ AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
+ }
+
+ [Test]
+ public void TestButtonAction()
+ {
+ AddStep("scroll to end", () => scroll.ScrollToEnd(false));
+
+ AddStep("invoke action", () => scroll.Button.Action.Invoke());
+
+ AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
+ }
+
+ [Test]
+ public void TestClick()
+ {
+ AddStep("scroll to end", () => scroll.ScrollToEnd(false));
+
+ AddStep("click button", () =>
+ {
+ InputManager.MoveMouseTo(scroll.Button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
+ }
+
+ [Test]
+ public void TestMultipleClicks()
+ {
+ AddStep("scroll to end", () => scroll.ScrollToEnd(false));
+
+ AddAssert("invocation count is 0", () => invocationCount == 0);
+
+ AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button));
+ AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3);
+
+ AddAssert("invocation count is 1", () => invocationCount == 1);
+ }
+
+ private class TestScrollContainer : OverlayScrollContainer
+ {
+ public new ScrollToTopButton Button => base.Button;
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 35eb3fa161..5ee887cb64 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 3b45fc83fd..aa37326a49 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
index 8d766ec9ba..e86fd890c1 100644
--- a/osu.Game.Tournament/Components/SongBar.cs
+++ b/osu.Game.Tournament/Components/SongBar.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components
{
private BeatmapInfo beatmap;
- private const float height = 145;
+ public const float HEIGHT = 145 / 2f;
[Resolved]
private IBindable ruleset { get; set; }
@@ -157,7 +157,7 @@ namespace osu.Game.Tournament.Components
new Container
{
RelativeSizeAxes = Axes.X,
- Height = height / 2,
+ Height = HEIGHT,
Width = 0.5f,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
@@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
- Height = height / 2,
+ Height = HEIGHT,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
}
diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
index d809dfc994..9785b7e647 100644
--- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
+++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
@@ -2,15 +2,42 @@
// 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.Game.Tournament.Components;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
- public class ShowcaseScreen : BeatmapInfoScreen
+ public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
{
[BackgroundDependencyLoader]
private void load()
{
- AddInternal(new TournamentLogo());
+ AddRangeInternal(new Drawable[]
+ {
+ new TournamentLogo(),
+ new TourneyVideo("showcase")
+ {
+ Loop = true,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Padding = new MarginPadding { Bottom = SongBar.HEIGHT },
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ // chroma key area for stable gameplay
+ Name = "chroma",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(0, 255, 0, 255),
+ }
+ }
+ });
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 68d113ce40..90c100db05 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -149,7 +149,12 @@ namespace osu.Game.Beatmaps
}
}
- public override string ToString() => $"{Metadata} [{Version}]".Trim();
+ public override string ToString()
+ {
+ string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
+
+ return $"{Metadata} {version}".Trim();
+ }
public bool Equals(BeatmapInfo other)
{
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 6542866936..5651d07566 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -246,6 +246,12 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap;
+ if (beatmapInfo.BeatmapSet.Files == null)
+ {
+ var info = beatmapInfo;
+ beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
+ }
+
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
@@ -287,13 +293,34 @@ namespace osu.Game.Beatmaps
/// Returns a list of all usable s.
///
/// A list of available .
- public List GetAllUsableBeatmapSets() => GetAllUsableBeatmapSetsEnumerable().ToList();
+ public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList();
///
- /// Returns a list of all usable s.
+ /// Returns a list of all usable s. Note that files are not populated.
///
+ /// The level of detail to include in the returned objects.
/// A list of available .
- public IQueryable GetAllUsableBeatmapSetsEnumerable() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected);
+ public IQueryable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes)
+ {
+ IQueryable queryable;
+
+ switch (includes)
+ {
+ case IncludedDetails.Minimal:
+ queryable = beatmaps.BeatmapSetsOverview;
+ break;
+
+ case IncludedDetails.AllButFiles:
+ queryable = beatmaps.BeatmapSetsWithoutFiles;
+ break;
+
+ default:
+ queryable = beatmaps.ConsumableItems;
+ break;
+ }
+
+ return queryable.Where(s => !s.DeletePending && !s.Protected);
+ }
///
/// Perform a lookup query on available s.
@@ -482,4 +509,25 @@ namespace osu.Game.Beatmaps
}
}
}
+
+ ///
+ /// The level of detail to include in database results.
+ ///
+ public enum IncludedDetails
+ {
+ ///
+ /// Only include beatmap difficulties and set level metadata.
+ ///
+ Minimal,
+
+ ///
+ /// Include all difficulties, rulesets, difficulty metadata but no files.
+ ///
+ AllButFiles,
+
+ ///
+ /// Include everything.
+ ///
+ All
+ }
}
diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs
index 001f319307..775d78f1fb 100644
--- a/osu.Game/Beatmaps/BeatmapMetadata.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadata.cs
@@ -53,7 +53,11 @@ namespace osu.Game.Beatmaps
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
- public override string ToString() => $"{Artist} - {Title} ({Author})";
+ public override string ToString()
+ {
+ string author = Author == null ? string.Empty : $"({Author})";
+ return $"{Artist} - {Title} {author}".Trim();
+ }
[JsonIgnore]
public string[] SearchableTerms => new[]
diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs
index a2279fdb14..642bafd2ac 100644
--- a/osu.Game/Beatmaps/BeatmapStore.cs
+++ b/osu.Game/Beatmaps/BeatmapStore.cs
@@ -87,6 +87,18 @@ namespace osu.Game.Beatmaps
base.Purge(items, context);
}
+ public IQueryable BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo
+ .Include(s => s.Metadata)
+ .Include(s => s.Beatmaps)
+ .AsNoTracking();
+
+ public IQueryable BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo
+ .Include(s => s.Metadata)
+ .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
+ .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
+ .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
+ .AsNoTracking();
+
public IQueryable Beatmaps =>
ContextFactory.Get().BeatmapInfo
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 39a0e6f6d4..a1822a1163 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -5,7 +5,7 @@ using System;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IEquatable
+ public abstract class ControlPoint : IComparable
{
///
/// The time at which the control point takes effect.
@@ -19,12 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
///
- /// Whether this control point is equivalent to another, ignoring time.
+ /// Determines whether this results in a meaningful change when placed alongside another.
///
- /// Another control point to compare with.
- /// Whether equivalent.
- public abstract bool EquivalentTo(ControlPoint other);
-
- public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other);
+ /// An existing control point to compare with.
+ /// Whether this is redundant when placed alongside .
+ public abstract bool IsRedundant(ControlPoint existing);
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index df68d8acd2..af6ca24165 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -56,6 +56,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// All control points, of all types.
///
+ [JsonIgnore]
public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
///
@@ -247,7 +248,7 @@ namespace osu.Game.Beatmaps.ControlPoints
break;
}
- return existing?.EquivalentTo(newPoint) == true;
+ return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 8b21098a51..2448b2b25c 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints
set => SpeedMultiplierBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is DifficultyControlPoint existingDifficulty
+ && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 369b93ff3d..9b69147468 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -35,8 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints
set => KiaiModeBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is EffectControlPoint otherTyped &&
- KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
+ public override bool IsRedundant(ControlPoint existing)
+ => !OmitFirstBarLine
+ && existing is EffectControlPoint existingEffect
+ && KiaiMode == existingEffect.KiaiMode
+ && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 393bcfdb3c..61851a00d7 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -68,8 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is SampleControlPoint otherTyped &&
- SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume;
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is SampleControlPoint existingSample
+ && SampleBank == existingSample.SampleBank
+ && SampleVolume == existingSample.SampleVolume;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 51b3377394..1927dd6575 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -48,8 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public double BPM => 60000 / BeatLength;
- public override bool EquivalentTo(ControlPoint other) =>
- other is TimingControlPoint otherTyped
- && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
+ // Timing points are never redundant as they can change the time signature.
+ public override bool IsRedundant(ControlPoint existing) => false;
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 33bb9774df..388abf4648 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -386,17 +386,10 @@ namespace osu.Game.Beatmaps.Formats
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
}, timingChange);
-
- // To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but
- // appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line
- // with the same time value (allowing them to overwrite as necessary).
- //
- // The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal.
- if (timingChange)
- flushPendingPoints();
}
private readonly List pendingControlPoints = new List();
+ private readonly HashSet pendingControlPointTypes = new HashSet();
private double pendingControlPointsTime;
private void addControlPoint(double time, ControlPoint point, bool timingChange)
@@ -405,21 +398,28 @@ namespace osu.Game.Beatmaps.Formats
flushPendingPoints();
if (timingChange)
- {
- beatmap.ControlPointInfo.Add(time, point);
- return;
- }
+ pendingControlPoints.Insert(0, point);
+ else
+ pendingControlPoints.Add(point);
- pendingControlPoints.Add(point);
pendingControlPointsTime = time;
}
private void flushPendingPoints()
{
- foreach (var p in pendingControlPoints)
- beatmap.ControlPointInfo.Add(pendingControlPointsTime, p);
+ // Changes from non-timing-points are added to the end of the list (see addControlPoint()) and should override any changes from timing-points (added to the start of the list).
+ for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
+ {
+ var type = pendingControlPoints[i].GetType();
+ if (pendingControlPointTypes.Contains(type))
+ continue;
+
+ pendingControlPointTypes.Add(type);
+ beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
+ }
pendingControlPoints.Clear();
+ pendingControlPointTypes.Clear();
}
private void handleHitObject(string line)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 12f2c58e35..7727f25967 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@@ -10,7 +11,9 @@ using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
+using osuTK;
namespace osu.Game.Beatmaps.Formats
{
@@ -48,7 +51,7 @@ namespace osu.Game.Beatmaps.Formats
handleEvents(writer);
writer.WriteLine();
- handleTimingPoints(writer);
+ handleControlPoints(writer);
writer.WriteLine();
handleHitObjects(writer);
@@ -58,7 +61,7 @@ namespace osu.Game.Beatmaps.Formats
{
writer.WriteLine("[General]");
- writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
+ if (beatmap.Metadata.AudioFile != null) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
// Todo: Not all countdown types are supported by lazer yet
@@ -103,15 +106,15 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[Metadata]");
writer.WriteLine(FormattableString.Invariant($"Title: {beatmap.Metadata.Title}"));
- writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}"));
+ if (beatmap.Metadata.TitleUnicode != null) writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}"));
writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}"));
- writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
+ if (beatmap.Metadata.ArtistUnicode != null) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}"));
writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}"));
- writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
- writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
- writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}"));
- writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1}"));
+ if (beatmap.Metadata.Source != null) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
+ if (beatmap.Metadata.Tags != null) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
+ if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}"));
+ if (beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID}"));
}
private void handleDifficulty(TextWriter writer)
@@ -122,7 +125,12 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}"));
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
- writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
+
+ // Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
+ writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
+ ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}")
+ : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
+
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
}
@@ -137,7 +145,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}"));
}
- private void handleTimingPoints(TextWriter writer)
+ private void handleControlPoints(TextWriter writer)
{
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
@@ -146,20 +154,30 @@ namespace osu.Game.Beatmaps.Formats
foreach (var group in beatmap.ControlPointInfo.Groups)
{
- var timingPoint = group.ControlPoints.OfType().FirstOrDefault();
- var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
- var samplePoint = beatmap.ControlPointInfo.SamplePointAt(group.Time);
- var effectPoint = beatmap.ControlPointInfo.EffectPointAt(group.Time);
+ var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault();
- // Convert beat length the legacy format
- double beatLength;
- if (timingPoint != null)
- beatLength = timingPoint.BeatLength;
- else
- beatLength = -100 / difficultyPoint.SpeedMultiplier;
+ // If the group contains a timing control point, it needs to be output separately.
+ if (groupTimingPoint != null)
+ {
+ writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
+ writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
+ outputControlPointEffectsAt(groupTimingPoint.Time, true);
+ }
+
+ // Output any remaining effects as secondary non-timing control point.
+ var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
+ writer.Write(FormattableString.Invariant($"{group.Time},"));
+ writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
+ outputControlPointEffectsAt(group.Time, false);
+ }
+
+ void outputControlPointEffectsAt(double time, bool isTimingPoint)
+ {
+ var samplePoint = beatmap.ControlPointInfo.SamplePointAt(time);
+ var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
- HitSampleInfo tempHitSample = samplePoint.ApplyTo(new HitSampleInfo());
+ HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo());
// Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags.None;
@@ -168,13 +186,11 @@ namespace osu.Game.Beatmaps.Formats
if (effectPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
- writer.Write(FormattableString.Invariant($"{group.Time},"));
- writer.Write(FormattableString.Invariant($"{beatLength},"));
- writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},"));
+ writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
- writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},"));
+ writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
- writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},"));
+ writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},"));
writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
writer.WriteLine();
}
@@ -187,65 +203,63 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
+ foreach (var h in beatmap.HitObjects)
+ handleHitObject(writer, h);
+ }
+
+ private void handleHitObject(TextWriter writer, HitObject hitObject)
+ {
+ Vector2 position = new Vector2(256, 192);
+
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
- foreach (var h in beatmap.HitObjects)
- handleOsuHitObject(writer, h);
- break;
-
- case 1:
- foreach (var h in beatmap.HitObjects)
- handleTaikoHitObject(writer, h);
+ position = ((IHasPosition)hitObject).Position;
break;
case 2:
- foreach (var h in beatmap.HitObjects)
- handleCatchHitObject(writer, h);
+ position.X = ((IHasXPosition)hitObject).X * 512;
break;
case 3:
- foreach (var h in beatmap.HitObjects)
- handleManiaHitObject(writer, h);
+ int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns));
break;
}
- }
- private void handleOsuHitObject(TextWriter writer, HitObject hitObject)
- {
- var positionData = (IHasPosition)hitObject;
-
- writer.Write(FormattableString.Invariant($"{positionData.X},"));
- writer.Write(FormattableString.Invariant($"{positionData.Y},"));
+ writer.Write(FormattableString.Invariant($"{position.X},"));
+ writer.Write(FormattableString.Invariant($"{position.Y},"));
writer.Write(FormattableString.Invariant($"{hitObject.StartTime},"));
writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},"));
-
- writer.Write(hitObject is IHasCurve
- ? FormattableString.Invariant($"0,")
- : FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
+ writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
if (hitObject is IHasCurve curveData)
{
- addCurveData(writer, curveData, positionData);
+ addCurveData(writer, curveData, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
}
else
{
- if (hitObject is IHasEndTime endTimeData)
- writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},"));
+ if (hitObject is IHasEndTime)
+ addEndTimeData(writer, hitObject);
+
writer.Write(getSampleBank(hitObject.Samples));
}
writer.WriteLine();
}
- private static LegacyHitObjectType getObjectType(HitObject hitObject)
+ private LegacyHitObjectType getObjectType(HitObject hitObject)
{
- var comboData = (IHasCombo)hitObject;
+ LegacyHitObjectType type = 0;
- var type = (LegacyHitObjectType)(comboData.ComboOffset << 4);
+ if (hitObject is IHasCombo combo)
+ {
+ type = (LegacyHitObjectType)(combo.ComboOffset << 4);
- if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo;
+ if (combo.NewCombo)
+ type |= LegacyHitObjectType.NewCombo;
+ }
switch (hitObject)
{
@@ -254,7 +268,10 @@ namespace osu.Game.Beatmaps.Formats
break;
case IHasEndTime _:
- type |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo;
+ if (beatmap.BeatmapInfo.RulesetID == 3)
+ type |= LegacyHitObjectType.Hold;
+ else
+ type |= LegacyHitObjectType.Spinner;
break;
default:
@@ -265,7 +282,7 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
- private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData)
+ private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position)
{
PathType? lastType = null;
@@ -301,13 +318,13 @@ namespace osu.Game.Beatmaps.Formats
else
{
// New segment with the same type - duplicate the control point
- writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|"));
+ writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|"));
}
}
if (i != 0)
{
- writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}"));
+ writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}"));
writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ",");
}
}
@@ -328,11 +345,19 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
+ private void addEndTimeData(TextWriter writer, HitObject hitObject)
+ {
+ var endTimeData = (IHasEndTime)hitObject;
+ var type = getObjectType(hitObject);
- private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
+ char suffix = ',';
- private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
+ // Holds write the end time as if it's part of sample data.
+ if (type == LegacyHitObjectType.Hold)
+ suffix = ':';
+
+ writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
+ }
private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false)
{
@@ -346,7 +371,7 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly)
{
- string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))?.Suffix);
+ string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100;
@@ -402,6 +427,15 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private string toLegacyCustomSampleBank(string sampleSuffix) => string.IsNullOrEmpty(sampleSuffix) ? "0" : sampleSuffix;
+ private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
+ {
+ if (hitSampleInfo == null)
+ return "0";
+
+ if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
+ return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture);
+
+ return "0";
+ }
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 561707f9ef..6406bd88a5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -8,6 +8,7 @@ using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO;
+using osu.Game.Rulesets.Objects.Legacy;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Formats
@@ -149,7 +150,8 @@ namespace osu.Game.Beatmaps.Formats
HitObjects,
Variables,
Fonts,
- Mania
+ CatchTheBeat,
+ Mania,
}
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
@@ -168,15 +170,19 @@ namespace osu.Game.Beatmaps.Formats
{
var baseInfo = base.ApplyTo(hitSampleInfo);
- if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 1)
- baseInfo.Suffix = CustomSampleBank.ToString();
+ if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy
+ && legacy.CustomSampleBank == 0)
+ {
+ legacy.CustomSampleBank = CustomSampleBank;
+ }
return baseInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
- CustomSampleBank == otherTyped.CustomSampleBank;
+ public override bool IsRedundant(ControlPoint existing)
+ => base.IsRedundant(existing)
+ && existing is LegacySampleControlPoint existingSample
+ && CustomSampleBank == existingSample.CustomSampleBank;
}
}
}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 41f6747b74..9d31bc9bba 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -49,6 +49,7 @@ namespace osu.Game.Configuration
};
Set(OsuSetting.ExternalLinkWarning, true);
+ Set(OsuSetting.PreferNoVideo, false);
// Audio
Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
@@ -87,7 +88,9 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowProgressGraph, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
+ Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
Set(OsuSetting.KeyOverlay, false);
+ Set(OsuSetting.PositionalHitSounds, true);
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@@ -176,11 +179,13 @@ namespace osu.Game.Configuration
LightenDuringBreaks,
ShowStoryboard,
KeyOverlay,
+ PositionalHitSounds,
ScoreMeter,
FloatingComments,
ShowInterface,
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
+ FadePlayfieldWhenHealthLow,
MouseDisableButtons,
MouseDisableWheel,
AudioOffset,
@@ -212,6 +217,7 @@ namespace osu.Game.Configuration
IncreaseFirstObjectVisibility,
ScoreDisplayMode,
ExternalLinkWarning,
+ PreferNoVideo,
Scaling,
ScalingPositionX,
ScalingPositionY,
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index f36079682e..5a613d1a54 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
- if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat)
+ if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
return;
using (BeginDelayedSequence(-TimeSinceLastBeat, true))
diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs
index 07a50c39e1..a3125614aa 100644
--- a/osu.Game/Graphics/Containers/SectionsContainer.cs
+++ b/osu.Game/Graphics/Containers/SectionsContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -16,12 +17,7 @@ namespace osu.Game.Graphics.Containers
public class SectionsContainer : Container
where T : Drawable
{
- private Drawable expandableHeader, fixedHeader, footer, headerBackground;
- private readonly OsuScrollContainer scrollContainer;
- private readonly Container headerBackgroundContainer;
- private readonly FlowContainer scrollContentContainer;
-
- protected override Container Content => scrollContentContainer;
+ public Bindable SelectedSection { get; } = new Bindable();
public Drawable ExpandableHeader
{
@@ -83,6 +79,7 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Clear();
headerBackground = value;
+
if (value == null) return;
headerBackgroundContainer.Add(headerBackground);
@@ -91,15 +88,37 @@ namespace osu.Game.Graphics.Containers
}
}
- public Bindable SelectedSection { get; } = new Bindable();
+ protected override Container Content => scrollContentContainer;
- protected virtual FlowContainer CreateScrollContentContainer()
- => new FillFlowContainer
+ private readonly OsuScrollContainer scrollContainer;
+ private readonly Container headerBackgroundContainer;
+ private readonly MarginPadding originalSectionsMargin;
+ private Drawable expandableHeader, fixedHeader, footer, headerBackground;
+ private FlowContainer scrollContentContainer;
+
+ private float headerHeight, footerHeight;
+
+ private float lastKnownScroll;
+
+ public SectionsContainer()
+ {
+ AddRangeInternal(new Drawable[]
{
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- };
+ scrollContainer = CreateScrollContainer().With(s =>
+ {
+ s.RelativeSizeAxes = Axes.Both;
+ s.Masking = true;
+ s.ScrollbarVisible = false;
+ s.Child = scrollContentContainer = CreateScrollContentContainer();
+ }),
+ headerBackgroundContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X
+ }
+ });
+
+ originalSectionsMargin = scrollContentContainer.Margin;
+ }
public override void Add(T drawable)
{
@@ -109,40 +128,23 @@ namespace osu.Game.Graphics.Containers
footerHeight = float.NaN;
}
- private float headerHeight, footerHeight;
- private readonly MarginPadding originalSectionsMargin;
-
- private void updateSectionsMargin()
- {
- if (!Children.Any()) return;
-
- var newMargin = originalSectionsMargin;
- newMargin.Top += headerHeight;
- newMargin.Bottom += footerHeight;
-
- scrollContentContainer.Margin = newMargin;
- }
-
- public SectionsContainer()
- {
- AddInternal(scrollContainer = new OsuScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- ScrollbarVisible = false,
- Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }
- });
- AddInternal(headerBackgroundContainer = new Container
- {
- RelativeSizeAxes = Axes.X
- });
- originalSectionsMargin = scrollContentContainer.Margin;
- }
-
- public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
+ public void ScrollTo(Drawable section) =>
+ scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0));
public void ScrollToTop() => scrollContainer.ScrollTo(0);
+ [NotNull]
+ protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
+
+ [NotNull]
+ protected virtual FlowContainer CreateScrollContentContainer() =>
+ new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ };
+
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
var result = base.OnInvalidate(invalidation, source);
@@ -156,8 +158,6 @@ namespace osu.Game.Graphics.Containers
return result;
}
- private float lastKnownScroll;
-
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -208,5 +208,16 @@ namespace osu.Game.Graphics.Containers
SelectedSection.Value = bestMatch;
}
}
+
+ private void updateSectionsMargin()
+ {
+ if (!Children.Any()) return;
+
+ var newMargin = originalSectionsMargin;
+ newMargin.Top += headerHeight;
+ newMargin.Bottom += footerHeight;
+
+ scrollContentContainer.Margin = newMargin;
+ }
}
}
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index a3ca851341..abaae7b43c 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
updateTextColour();
+
+ Item.Action.BindDisabledChanged(_ => updateState(), true);
}
private void updateTextColour()
@@ -65,19 +67,33 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
- sampleHover.Play();
- text.BoldText.FadeIn(transition_length, Easing.OutQuint);
- text.NormalText.FadeOut(transition_length, Easing.OutQuint);
+ updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- text.BoldText.FadeOut(transition_length, Easing.OutQuint);
- text.NormalText.FadeIn(transition_length, Easing.OutQuint);
+ updateState();
base.OnHoverLost(e);
}
+ private void updateState()
+ {
+ Alpha = Item.Action.Disabled ? 0.2f : 1;
+
+ if (IsHovered && !Item.Action.Disabled)
+ {
+ sampleHover.Play();
+ text.BoldText.FadeIn(transition_length, Easing.OutQuint);
+ text.NormalText.FadeOut(transition_length, Easing.OutQuint);
+ }
+ else
+ {
+ text.BoldText.FadeOut(transition_length, Easing.OutQuint);
+ text.NormalText.FadeIn(transition_length, Easing.OutQuint);
+ }
+ }
+
protected override bool OnClick(ClickEvent e)
{
sampleClick.Play();
diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
index 6d244bff60..64f1ebeb1a 100644
--- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
+++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs
@@ -43,13 +43,13 @@ namespace osu.Game.IO.Serialization.Converters
var list = new List();
var obj = JObject.Load(reader);
- var lookupTable = serializer.Deserialize>(obj["lookup_table"].CreateReader());
+ var lookupTable = serializer.Deserialize>(obj["$lookup_table"].CreateReader());
- foreach (var tok in obj["items"])
+ foreach (var tok in obj["$items"])
{
var itemReader = tok.CreateReader();
- var typeName = lookupTable[(int)tok["type"]];
+ var typeName = lookupTable[(int)tok["$type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName));
serializer.Populate(itemReader, instance);
@@ -61,7 +61,7 @@ namespace osu.Game.IO.Serialization.Converters
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
- var list = (List)value;
+ var list = (IEnumerable)value;
var lookupTable = new List();
var objects = new List();
@@ -84,16 +84,16 @@ namespace osu.Game.IO.Serialization.Converters
}
var itemObject = JObject.FromObject(item, serializer);
- itemObject.AddFirst(new JProperty("type", typeId));
+ itemObject.AddFirst(new JProperty("$type", typeId));
objects.Add(itemObject);
}
writer.WriteStartObject();
- writer.WritePropertyName("lookup_table");
+ writer.WritePropertyName("$lookup_table");
serializer.Serialize(writer, lookupTable);
- writer.WritePropertyName("items");
+ writer.WritePropertyName("$items");
serializer.Serialize(writer, objects);
writer.WriteEndObject();
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index e83d899469..94edc33099 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings
store.KeyBindingChanged -= ReloadMappings;
}
- protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList();
+ protected override void ReloadMappings()
+ {
+ if (ruleset != null && !ruleset.ID.HasValue)
+ // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
+ // fallback to defaults instead.
+ KeyBindings = DefaultKeyBindings;
+ else
+ KeyBindings = store.Query(ruleset?.ID, variant).ToList();
+ }
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 6a6c7b72a8..0bba04cac3 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -16,20 +16,35 @@ namespace osu.Game.Online.API
{
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri);
- public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
-
- protected APIRequest()
- {
- base.Success += onSuccess;
- }
-
- private void onSuccess() => Success?.Invoke(Result);
+ public T Result { get; private set; }
///
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
///
public new event APISuccessHandler Success;
+
+ protected override void PostProcess()
+ {
+ base.PostProcess();
+ Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+ }
+
+ internal void TriggerSuccess(T result)
+ {
+ if (Result != null)
+ throw new InvalidOperationException("Attempted to trigger success more than once");
+
+ Result = result;
+
+ TriggerSuccess();
+ }
+
+ internal override void TriggerSuccess()
+ {
+ base.TriggerSuccess();
+ Success?.Invoke(Result);
+ }
}
///
@@ -92,14 +107,28 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
+ PostProcess();
+
API.Schedule(delegate
{
if (cancelled) return;
- Success?.Invoke();
+ TriggerSuccess();
});
}
+ ///
+ /// Perform any post-processing actions after a successful request.
+ ///
+ protected virtual void PostProcess()
+ {
+ }
+
+ internal virtual void TriggerSuccess()
+ {
+ Success?.Invoke();
+ }
+
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index a1c3475fd9..7800241904 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -30,6 +31,11 @@ namespace osu.Game.Online.API
private readonly List components = new List();
+ ///
+ /// Provide handling logic for an arbitrary API request.
+ ///
+ public Action HandleRequest;
+
public APIState State
{
get => state;
@@ -55,11 +61,16 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
+ HandleRequest?.Invoke(request);
}
- public void Perform(APIRequest request) { }
+ public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
- public Task PerformAsync(APIRequest request) => Task.CompletedTask;
+ public Task PerformAsync(APIRequest request)
+ {
+ HandleRequest?.Invoke(request);
+ return Task.CompletedTask;
+ }
public void Register(IOnlineComponent component)
{
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 930ca8fdf1..047496b473 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -1,30 +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 System.ComponentModel;
using osu.Framework.IO.Network;
using osu.Game.Overlays;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest
{
+ public SearchCategory SearchCategory { get; set; }
+
+ public SortCriteria SortCriteria { get; set; }
+
+ public SortDirection SortDirection { get; set; }
+
+ public SearchGenre Genre { get; set; }
+
+ public SearchLanguage Language { get; set; }
+
private readonly string query;
private readonly RulesetInfo ruleset;
- private readonly BeatmapSearchCategory searchCategory;
- private readonly DirectSortCriteria sortCriteria;
- private readonly SortDirection direction;
- private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc";
- public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending)
+ private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
+
+ public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset)
{
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
- this.searchCategory = searchCategory;
- this.sortCriteria = sortCriteria;
- this.direction = direction;
+
+ SearchCategory = SearchCategory.Any;
+ SortCriteria = SortCriteria.Ranked;
+ SortDirection = SortDirection.Descending;
+ Genre = SearchGenre.Any;
+ Language = SearchLanguage.Any;
}
protected override WebRequest CreateWebRequest()
@@ -35,31 +45,19 @@ namespace osu.Game.Online.API.Requests
if (ruleset.ID.HasValue)
req.AddParameter("m", ruleset.ID.Value.ToString());
- req.AddParameter("s", searchCategory.ToString().ToLowerInvariant());
- req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}");
+ req.AddParameter("s", SearchCategory.ToString().ToLowerInvariant());
+
+ if (Genre != SearchGenre.Any)
+ req.AddParameter("g", ((int)Genre).ToString());
+
+ if (Language != SearchLanguage.Any)
+ req.AddParameter("l", ((int)Language).ToString());
+
+ req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}");
return req;
}
protected override string Target => @"beatmapsets/search";
}
-
- public enum BeatmapSearchCategory
- {
- Any,
-
- [Description("Has Leaderboard")]
- Leaderboard,
- Ranked,
- Qualified,
- Loved,
- Favourites,
-
- [Description("Pending & WIP")]
- Pending,
- Graveyard,
-
- [Description("My Maps")]
- Mine,
- }
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 2c37216fd6..822f628dd2 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat
///
/// Manages everything channel related
///
- public class ChannelManager : PollingComponent
+ public class ChannelManager : PollingComponent, IChannelPostTarget
{
///
/// The channels the player joins on startup
@@ -204,6 +204,10 @@ namespace osu.Game.Online.Chat
switch (command)
{
+ case "np":
+ AddInternal(new NowPlayingCommand());
+ break;
+
case "me":
if (string.IsNullOrWhiteSpace(content))
{
@@ -234,7 +238,7 @@ namespace osu.Game.Online.Chat
break;
case "help":
- target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]"));
+ target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np"));
break;
default:
diff --git a/osu.Game/Online/Chat/IChannelPostTarget.cs b/osu.Game/Online/Chat/IChannelPostTarget.cs
new file mode 100644
index 0000000000..5697e918f0
--- /dev/null
+++ b/osu.Game/Online/Chat/IChannelPostTarget.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+
+namespace osu.Game.Online.Chat
+{
+ [Cached(typeof(IChannelPostTarget))]
+ public interface IChannelPostTarget
+ {
+ ///
+ /// Posts a message to the currently opened channel.
+ ///
+ /// The message text that is going to be posted
+ /// Is true if the message is an action, e.g.: user is currently eating
+ /// An optional target channel. If null, will be used.
+ void PostMessage(string text, bool isAction = false, Channel target = null);
+ }
+}
diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs
new file mode 100644
index 0000000000..c0b54812b6
--- /dev/null
+++ b/osu.Game/Online/Chat/NowPlayingCommand.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Chat
+{
+ public class NowPlayingCommand : Component
+ {
+ [Resolved]
+ private IChannelPostTarget channelManager { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private Bindable currentBeatmap { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ string verb;
+ BeatmapInfo beatmap;
+
+ switch (api.Activity.Value)
+ {
+ case UserActivity.SoloGame solo:
+ verb = "playing";
+ beatmap = solo.Beatmap;
+ break;
+
+ case UserActivity.Editing edit:
+ verb = "editing";
+ beatmap = edit.Beatmap;
+ break;
+
+ default:
+ verb = "listening to";
+ beatmap = currentBeatmap.Value.BeatmapInfo;
+ break;
+ }
+
+ var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString();
+
+ channelManager.PostMessage($"is {verb} {beatmapString}", true);
+ Expire();
+ }
+ }
+}
diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs
index acbb2c39f4..228f147835 100644
--- a/osu.Game/Online/PollingComponent.cs
+++ b/osu.Game/Online/PollingComponent.cs
@@ -3,7 +3,7 @@
using System;
using System.Threading.Tasks;
-using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
namespace osu.Game.Online
@@ -11,7 +11,7 @@ namespace osu.Game.Online
///
/// A component which requires a constant polling process.
///
- public abstract class PollingComponent : Component
+ public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
{
private double? lastTimePolled;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1b2fd658f4..f5f7d0cef4 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -65,9 +65,9 @@ namespace osu.Game
private NowPlayingOverlay nowPlaying;
- private DirectOverlay direct;
+ private BeatmapListingOverlay beatmapListing;
- private SocialOverlay social;
+ private DashboardOverlay dashboard;
private UserProfileOverlay userProfile;
@@ -315,8 +315,15 @@ namespace osu.Game
/// The user should have already requested this interactively.
///
/// The beatmap to select.
- public void PresentBeatmap(BeatmapSetInfo beatmap)
+ ///
+ /// Optional predicate used to try and find a difficulty to select.
+ /// If omitted, this will try to present the first beatmap from the current ruleset.
+ /// In case of failure the first difficulty of the set will be presented, ignoring the predicate.
+ ///
+ public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null)
{
+ difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value);
+
var databasedSet = beatmap.OnlineBeatmapSetID != null
? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID)
: BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash);
@@ -334,13 +341,13 @@ namespace osu.Game
menuScreen.LoadToSolo();
// we might even already be at the song
- if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash)
+ if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo))
{
return;
}
- // Use first beatmap available for current ruleset, else switch ruleset.
- var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First();
+ // Find first beatmap that matches our predicate.
+ var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First();
Ruleset.Value = first.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first);
@@ -603,8 +610,8 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
//overlay elements
- loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@@ -663,7 +670,7 @@ namespace osu.Game
}
// ensure only one of these overlays are open at once.
- var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay };
+ var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@@ -835,7 +842,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSocial:
- social.ToggleVisibility();
+ dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
@@ -858,7 +865,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleDirect:
- direct.ToggleVisibility();
+ beatmapListing.ToggleVisibility();
return true;
case GlobalAction.ToggleGameplayMouseButtons:
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 5487bd9320..609b6ce98e 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -168,7 +168,7 @@ namespace osu.Game
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
- dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
+ dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage));
dependencies.Cache(FileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
diff --git a/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs b/osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs
similarity index 94%
rename from osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs
rename to osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs
index fd04a1541e..f6b5b181c3 100644
--- a/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs
+++ b/osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs
@@ -5,7 +5,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays
{
public abstract class BeatmapDownloadTrackingComposite : DownloadTrackingComposite
{
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
new file mode 100644
index 0000000000..4dd60c7113
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -0,0 +1,164 @@
+// 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.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Rulesets;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public class BeatmapListingFilterControl : CompositeDrawable
+ {
+ public Action> SearchFinished;
+ public Action SearchStarted;
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ private readonly BeatmapListingSearchControl searchControl;
+ private readonly BeatmapListingSortTabControl sortControl;
+ private readonly Box sortControlBackground;
+
+ private SearchBeatmapSetsRequest getSetsRequest;
+
+ public BeatmapListingFilterControl()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ InternalChild = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.25f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 3,
+ Offset = new Vector2(0f, 1f),
+ },
+ Child = searchControl = new BeatmapListingSearchControl(),
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Children = new Drawable[]
+ {
+ sortControlBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ sortControl = new BeatmapListingSortTabControl
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Left = 20 }
+ }
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ sortControlBackground.Colour = colourProvider.Background5;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ var sortCriteria = sortControl.Current;
+ var sortDirection = sortControl.SortDirection;
+
+ searchControl.Query.BindValueChanged(query =>
+ {
+ sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? SortCriteria.Ranked : SortCriteria.Relevance;
+ sortDirection.Value = SortDirection.Descending;
+ queueUpdateSearch(true);
+ });
+
+ searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch());
+ searchControl.Category.BindValueChanged(_ => queueUpdateSearch());
+ searchControl.Genre.BindValueChanged(_ => queueUpdateSearch());
+ searchControl.Language.BindValueChanged(_ => queueUpdateSearch());
+
+ sortCriteria.BindValueChanged(_ => queueUpdateSearch());
+ sortDirection.BindValueChanged(_ => queueUpdateSearch());
+ }
+
+ private ScheduledDelegate queryChangedDebounce;
+
+ private void queueUpdateSearch(bool queryTextChanged = false)
+ {
+ SearchStarted?.Invoke();
+
+ getSetsRequest?.Cancel();
+
+ queryChangedDebounce?.Cancel();
+ queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
+ }
+
+ private void updateSearch()
+ {
+ getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value)
+ {
+ SearchCategory = searchControl.Category.Value,
+ SortCriteria = sortControl.Current.Value,
+ SortDirection = sortControl.SortDirection.Value,
+ Genre = searchControl.Genre.Value,
+ Language = searchControl.Language.Value
+ };
+
+ getSetsRequest.Success += response => Schedule(() => onSearchFinished(response));
+
+ api.Queue(getSetsRequest);
+ }
+
+ private void onSearchFinished(SearchBeatmapSetsResponse response)
+ {
+ var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
+
+ searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First();
+
+ SearchFinished?.Invoke(beatmaps);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ getSetsRequest?.Cancel();
+ queryChangedDebounce?.Cancel();
+
+ base.Dispose(isDisposing);
+ }
+
+ public void TakeFocus() => searchControl.TakeFocus();
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
similarity index 81%
rename from osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs
rename to osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index f9799d8a6b..29c4fe0d2e 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -5,8 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Online.API.Requests;
-using osu.Game.Rulesets;
using osuTK;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.Drawables;
@@ -14,16 +12,21 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
+using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
{
- public class BeatmapListingSearchSection : CompositeDrawable
+ public class BeatmapListingSearchControl : CompositeDrawable
{
public Bindable Query => textBox.Current;
public Bindable Ruleset => modeFilter.Current;
- public Bindable Category => categoryFilter.Current;
+ public Bindable Category => categoryFilter.Current;
+
+ public Bindable Genre => genreFilter.Current;
+
+ public Bindable Language => languageFilter.Current;
public BeatmapSetInfo BeatmapSet
{
@@ -42,12 +45,14 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly BeatmapSearchTextBox textBox;
private readonly BeatmapSearchRulesetFilterRow modeFilter;
- private readonly BeatmapSearchFilterRow categoryFilter;
+ private readonly BeatmapSearchFilterRow categoryFilter;
+ private readonly BeatmapSearchFilterRow genreFilter;
+ private readonly BeatmapSearchFilterRow languageFilter;
private readonly Box background;
private readonly UpdateableBeatmapSetCover beatmapCover;
- public BeatmapListingSearchSection()
+ public BeatmapListingSearchControl()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
@@ -97,7 +102,9 @@ namespace osu.Game.Overlays.BeatmapListing
Children = new Drawable[]
{
modeFilter = new BeatmapSearchRulesetFilterRow(),
- categoryFilter = new BeatmapSearchFilterRow(@"Categories"),
+ categoryFilter = new BeatmapSearchFilterRow(@"Categories"),
+ genreFilter = new BeatmapSearchFilterRow(@"Genre"),
+ languageFilter = new BeatmapSearchFilterRow(@"Language"),
}
}
}
@@ -105,7 +112,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
});
- Category.Value = BeatmapSearchCategory.Leaderboard;
+ categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
[BackgroundDependencyLoader]
@@ -114,6 +121,8 @@ namespace osu.Game.Overlays.BeatmapListing
background.Colour = colourProvider.Dark6;
}
+ public void TakeFocus() => textBox.TakeFocus();
+
private class BeatmapSearchTextBox : SearchTextBox
{
protected override Color4 SelectionColour => Color4.Gray;
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
index 27c43b092a..4c77a736ac 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
@@ -8,17 +8,16 @@ using osu.Framework.Graphics;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Input.Events;
-using osu.Game.Overlays.Direct;
namespace osu.Game.Overlays.BeatmapListing
{
- public class BeatmapListingSortTabControl : OverlaySortTabControl
+ public class BeatmapListingSortTabControl : OverlaySortTabControl
{
public readonly Bindable SortDirection = new Bindable(Overlays.SortDirection.Descending);
public BeatmapListingSortTabControl()
{
- Current.Value = DirectSortCriteria.Ranked;
+ Current.Value = SortCriteria.Ranked;
}
protected override SortTabControl CreateControl() => new BeatmapSortTabControl
@@ -30,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
public readonly Bindable SortDirection = new Bindable();
- protected override TabItem CreateTabItem(DirectSortCriteria value) => new BeatmapSortTabItem(value)
+ protected override TabItem CreateTabItem(SortCriteria value) => new BeatmapSortTabItem(value)
{
SortDirection = { BindTarget = SortDirection }
};
@@ -40,12 +39,12 @@ namespace osu.Game.Overlays.BeatmapListing
{
public readonly Bindable SortDirection = new Bindable();
- public BeatmapSortTabItem(DirectSortCriteria value)
+ public BeatmapSortTabItem(SortCriteria value)
: base(value)
{
}
- protected override TabButton CreateTabButton(DirectSortCriteria value) => new BeatmapTabButton(value)
+ protected override TabButton CreateTabButton(SortCriteria value) => new BeatmapTabButton(value)
{
Active = { BindTarget = Active },
SortDirection = { BindTarget = SortDirection }
@@ -67,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly SpriteIcon icon;
- public BeatmapTabButton(DirectSortCriteria value)
+ public BeatmapTabButton(SortCriteria value)
: base(value)
{
Add(icon = new SpriteIcon
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
index 2c046a2bbf..64b3afcae1 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
@@ -15,6 +15,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
+using Humanizer;
+using osu.Game.Utils;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -53,8 +55,8 @@ namespace osu.Game.Overlays.BeatmapListing
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
- Font = OsuFont.GetFont(size: 10),
- Text = headerName.ToUpper()
+ Font = OsuFont.GetFont(size: 13),
+ Text = headerName.Titleize()
},
CreateFilter().With(f =>
{
@@ -81,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapListing
if (typeof(T).IsEnum)
{
- foreach (var val in (T[])Enum.GetValues(typeof(T)))
+ foreach (var val in OrderAttributeUtils.GetValuesInOrder())
AddItem(val);
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs
deleted file mode 100644
index 6daa7cb0e0..0000000000
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics.UserInterface;
-
-namespace osu.Game.Overlays.BeatmapListing
-{
- public class BeatmapSearchSmallFilterRow : BeatmapSearchFilterRow
- {
- public BeatmapSearchSmallFilterRow(string headerName)
- : base(headerName)
- {
- }
-
- protected override BeatmapSearchFilter CreateFilter() => new SmallBeatmapSearchFilter();
-
- private class SmallBeatmapSearchFilter : BeatmapSearchFilter
- {
- protected override TabItem CreateTabItem(T value) => new SmallTabItem(value);
-
- private class SmallTabItem : FilterTabItem
- {
- public SmallTabItem(T value)
- : base(value)
- {
- }
-
- protected override float TextSize => 10;
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs
similarity index 96%
rename from osu.Game/Overlays/Direct/DirectPanel.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs
index 4ad8e95512..88c15776cd 100644
--- a/osu.Game/Overlays/Direct/DirectPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs
@@ -26,9 +26,9 @@ using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
- public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu
+ public abstract class BeatmapPanel : OsuClickableContainer, IHasContextMenu
{
public readonly BeatmapSetInfo SetInfo;
@@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Direct
protected Action ViewBeatmap;
- protected DirectPanel(BeatmapSetInfo setInfo)
+ protected BeatmapPanel(BeatmapSetInfo setInfo)
{
Debug.Assert(setInfo.OnlineBeatmapSetID != null);
@@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Direct
if (SetInfo.Beatmaps.Count > maximum_difficulty_icons)
{
foreach (var ruleset in SetInfo.Beatmaps.Select(b => b.Ruleset).Distinct())
- icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is DirectListPanel ? Color4.White : colours.Gray5));
+ icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is ListBeatmapPanel ? Color4.White : colours.Gray5));
}
else
{
diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
similarity index 63%
rename from osu.Game/Overlays/Direct/PanelDownloadButton.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
index 1b3657f010..589f2d5072 100644
--- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
@@ -1,29 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
- public class PanelDownloadButton : BeatmapDownloadTrackingComposite
+ public class BeatmapPanelDownloadButton : BeatmapDownloadTrackingComposite
{
protected bool DownloadEnabled => button.Enabled.Value;
- private readonly bool noVideo;
+ ///
+ /// Currently selected beatmap. Used to present the correct difficulty after completing a download.
+ ///
+ public readonly IBindable SelectedBeatmap = new Bindable();
private readonly ShakeContainer shakeContainer;
private readonly DownloadButton button;
+ private Bindable noVideoSetting;
- public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
+ public BeatmapPanelDownloadButton(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
- this.noVideo = noVideo;
-
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
@@ -43,7 +48,7 @@ namespace osu.Game.Overlays.Direct
}
[BackgroundDependencyLoader(true)]
- private void load(OsuGame game, BeatmapManager beatmaps)
+ private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig)
{
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{
@@ -52,6 +57,8 @@ namespace osu.Game.Overlays.Direct
return;
}
+ noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo);
+
button.Action = () =>
{
switch (State.Value)
@@ -62,11 +69,15 @@ namespace osu.Game.Overlays.Direct
break;
case DownloadState.LocallyAvailable:
- game?.PresentBeatmap(BeatmapSet.Value);
+ Predicate findPredicate = null;
+ if (SelectedBeatmap.Value != null)
+ findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID;
+
+ game?.PresentBeatmap(BeatmapSet.Value, findPredicate);
break;
default:
- beatmaps.Download(BeatmapSet.Value, noVideo);
+ beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value);
break;
}
};
diff --git a/osu.Game/Overlays/Direct/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
similarity index 97%
rename from osu.Game/Overlays/Direct/DownloadProgressBar.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
index 9a8644efd2..93cf8799b5 100644
--- a/osu.Game/Overlays/Direct/DownloadProgressBar.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
@@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class DownloadProgressBar : BeatmapDownloadTrackingComposite
{
diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
similarity index 97%
rename from osu.Game/Overlays/Direct/DirectGridPanel.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
index 2528ccec41..84d35da096 100644
--- a/osu.Game/Overlays/Direct/DirectGridPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
@@ -1,25 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
- public class DirectGridPanel : DirectPanel
+ public class GridBeatmapPanel : BeatmapPanel
{
private const float horizontal_padding = 10;
private const float vertical_padding = 5;
@@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Direct
protected override PlayButton PlayButton => playButton;
protected override Box PreviewBar => progressBar;
- public DirectGridPanel(BeatmapSetInfo beatmap)
+ public GridBeatmapPanel(BeatmapSetInfo beatmap)
: base(beatmap)
{
Width = 380;
@@ -156,7 +156,7 @@ namespace osu.Game.Overlays.Direct
},
},
},
- new PanelDownloadButton(SetInfo)
+ new BeatmapPanelDownloadButton(SetInfo)
{
Size = new Vector2(50, 30),
Margin = new MarginPadding(horizontal_padding),
diff --git a/osu.Game/Overlays/Direct/IconPill.cs b/osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs
similarity index 96%
rename from osu.Game/Overlays/Direct/IconPill.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs
index d63bb2a292..1cb6c84f13 100644
--- a/osu.Game/Overlays/Direct/IconPill.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class IconPill : CircularContainer
{
diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
similarity index 97%
rename from osu.Game/Overlays/Direct/DirectListPanel.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
index b64142dfe7..433ea37f06 100644
--- a/osu.Game/Overlays/Direct/DirectListPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
@@ -1,25 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
-using osuTK.Graphics;
+using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
- public class DirectListPanel : DirectPanel
+ public class ListBeatmapPanel : BeatmapPanel
{
private const float transition_duration = 120;
private const float horizontal_padding = 10;
@@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Direct
private const float height = 70;
private FillFlowContainer statusContainer;
- protected PanelDownloadButton DownloadButton;
+ protected BeatmapPanelDownloadButton DownloadButton;
private PlayButton playButton;
private Box progressBar;
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Direct
protected override PlayButton PlayButton => playButton;
protected override Box PreviewBar => progressBar;
- public DirectListPanel(BeatmapSetInfo beatmap)
+ public ListBeatmapPanel(BeatmapSetInfo beatmap)
: base(beatmap)
{
RelativeSizeAxes = Axes.X;
@@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Direct
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
- Child = DownloadButton = new PanelDownloadButton(SetInfo)
+ Child = DownloadButton = new BeatmapPanelDownloadButton(SetInfo)
{
Size = new Vector2(height - vertical_padding * 3),
Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding },
diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs
similarity index 98%
rename from osu.Game/Overlays/Direct/PlayButton.cs
rename to osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs
index d9f335b6a7..e95fdeecf4 100644
--- a/osu.Game/Overlays/Direct/PlayButton.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs
@@ -14,7 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Overlays.Direct
+namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class PlayButton : Container
{
diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs
new file mode 100644
index 0000000000..84859bf5b5
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public enum SearchCategory
+ {
+ Any,
+
+ [Description("Has Leaderboard")]
+ Leaderboard,
+ Ranked,
+ Qualified,
+ Loved,
+ Favourites,
+
+ [Description("Pending & WIP")]
+ Pending,
+ Graveyard,
+
+ [Description("My Maps")]
+ Mine,
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs
new file mode 100644
index 0000000000..b12bba6249
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ public enum SearchGenre
+ {
+ Any = 0,
+ Unspecified = 1,
+
+ [Description("Video Game")]
+ VideoGame = 2,
+ Anime = 3,
+ Rock = 4,
+ Pop = 5,
+ Other = 6,
+ Novelty = 7,
+
+ [Description("Hip Hop")]
+ HipHop = 9,
+ Electronic = 10
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
new file mode 100644
index 0000000000..dac7e4f1a2
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Utils;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+ [HasOrderedElements]
+ public enum SearchLanguage
+ {
+ [Order(0)]
+ Any,
+
+ [Order(11)]
+ Other,
+
+ [Order(1)]
+ English,
+
+ [Order(6)]
+ Japanese,
+
+ [Order(2)]
+ Chinese,
+
+ [Order(10)]
+ Instrumental,
+
+ [Order(7)]
+ Korean,
+
+ [Order(3)]
+ French,
+
+ [Order(4)]
+ German,
+
+ [Order(9)]
+ Swedish,
+
+ [Order(8)]
+ Spanish,
+
+ [Order(5)]
+ Italian
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs
new file mode 100644
index 0000000000..e409cbdda7
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs
@@ -0,0 +1,17 @@
+// 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.Overlays.BeatmapListing
+{
+ public enum SortCriteria
+ {
+ Title,
+ Artist,
+ Difficulty,
+ Ranked,
+ Rating,
+ Plays,
+ Favourites,
+ Relevance
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 5bac5a5402..f680f7c67b 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -1,27 +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 System.Collections.Generic;
using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
-using osu.Framework.Threading;
+using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapListing;
-using osu.Game.Overlays.Direct;
-using osu.Game.Rulesets;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -30,20 +27,17 @@ namespace osu.Game.Overlays
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
- private SearchBeatmapSetsRequest getSetsRequest;
-
private Drawable currentContent;
- private BeatmapListingSearchSection searchSection;
- private BeatmapListingSortTabControl sortControl;
+ private LoadingLayer loadingLayer;
+ private Container panelTarget;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
{
}
+ private BeatmapListingFilterControl filterControl;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -54,7 +48,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
- new BasicScrollContainer
+ new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
@@ -63,27 +57,13 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
- new FillFlowContainer
+ new BeatmapListingHeader(),
+ filterControl = new BeatmapListingFilterControl
{
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Masking = true,
- EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.Black.Opacity(0.25f),
- Type = EdgeEffectType.Shadow,
- Radius = 3,
- Offset = new Vector2(0f, 1f),
- },
- Children = new Drawable[]
- {
- new BeatmapListingHeader(),
- searchSection = new BeatmapListingSearchSection(),
- }
+ SearchStarted = onSearchStarted,
+ SearchFinished = onSearchFinished,
},
new Container
{
@@ -96,154 +76,70 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
- new FillFlowContainer
+ panelTarget = new Container
{
- RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = 40,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background5
- },
- sortControl = new BeatmapListingSortTabControl
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Margin = new MarginPadding { Left = 20 }
- }
- }
- },
- new Container
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 20 },
- Children = new Drawable[]
- {
- panelTarget = new Container
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- },
- loadingLayer = new LoadingLayer(panelTarget),
- }
- },
- }
- }
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding { Horizontal = 20 }
+ },
+ loadingLayer = new LoadingLayer(panelTarget)
}
- }
+ },
}
}
}
};
}
- protected override void LoadComplete()
+ protected override void OnFocus(FocusEvent e)
{
- base.LoadComplete();
+ base.OnFocus(e);
- var sortCriteria = sortControl.Current;
- var sortDirection = sortControl.SortDirection;
-
- searchSection.Query.BindValueChanged(query =>
- {
- sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance;
- sortDirection.Value = SortDirection.Descending;
-
- queueUpdateSearch(true);
- });
-
- searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch());
- searchSection.Category.BindValueChanged(_ => queueUpdateSearch());
- sortCriteria.BindValueChanged(_ => queueUpdateSearch());
- sortDirection.BindValueChanged(_ => queueUpdateSearch());
+ filterControl.TakeFocus();
}
- private ScheduledDelegate queryChangedDebounce;
+ private CancellationTokenSource cancellationToken;
- private LoadingLayer loadingLayer;
- private Container panelTarget;
-
- private void queueUpdateSearch(bool queryTextChanged = false)
+ private void onSearchStarted()
{
- getSetsRequest?.Cancel();
-
- queryChangedDebounce?.Cancel();
- queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
- }
-
- private void updateSearch()
- {
- if (!IsLoaded)
- return;
-
- if (State.Value == Visibility.Hidden)
- return;
-
- if (API == null)
- return;
+ cancellationToken?.Cancel();
previewTrackManager.StopAnyPlaying(this);
- loadingLayer.Show();
-
- getSetsRequest = new SearchBeatmapSetsRequest(
- searchSection.Query.Value,
- searchSection.Ruleset.Value,
- searchSection.Category.Value,
- sortControl.Current.Value,
- sortControl.SortDirection.Value);
-
- getSetsRequest.Success += response => Schedule(() => recreatePanels(response));
-
- API.Queue(getSetsRequest);
+ if (panelTarget.Any())
+ loadingLayer.Show();
}
- private void recreatePanels(SearchBeatmapSetsResponse response)
+ private void onSearchFinished(List beatmaps)
{
- if (response.Total == 0)
+ if (!beatmaps.Any())
{
- searchSection.BeatmapSet = null;
- LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder);
+ LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
return;
}
- var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
-
- var newPanels = new FillFlowContainer
+ var newPanels = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding { Vertical = 15 },
- ChildrenEnumerable = beatmaps.Select(b => new DirectGridPanel(b)
+ ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
})
};
- LoadComponentAsync(newPanels, loaded =>
- {
- addContentToPlaceholder(loaded);
- searchSection.BeatmapSet = beatmaps.First();
- });
+ LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
}
private void addContentToPlaceholder(Drawable content)
{
loadingLayer.Hide();
- Drawable lastContent = currentContent;
+ var lastContent = currentContent;
if (lastContent != null)
{
@@ -262,9 +158,7 @@ namespace osu.Game.Overlays
protected override void Dispose(bool isDisposing)
{
- getSetsRequest?.Cancel();
- queryChangedDebounce?.Cancel();
-
+ cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
index e64256b850..56c0052bfe 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
@@ -13,7 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osu.Game.Online.API;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs
index 7eae05e4a9..6accce7d77 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs
@@ -11,7 +11,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Buttons
diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs
index 29c259b7f8..17fa689cd2 100644
--- a/osu.Game/Overlays/BeatmapSet/Header.cs
+++ b/osu.Game/Overlays/BeatmapSet/Header.cs
@@ -15,8 +15,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.BeatmapSet.Buttons;
-using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -274,10 +274,11 @@ namespace osu.Game.Overlays.BeatmapSet
{
case DownloadState.LocallyAvailable:
// temporary for UX until new design is implemented.
- downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value)
+ downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value)
{
Width = 50,
- RelativeSizeAxes = Axes.Y
+ RelativeSizeAxes = Axes.Y,
+ SelectedBeatmap = { BindTarget = Picker.Beatmap }
};
break;
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index 0d16c4842d..3e23442023 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays
public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue)
{
- OsuScrollContainer scroll;
+ OverlayScrollContainer scroll;
Info info;
CommentsSection comments;
@@ -49,7 +49,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both
},
- scroll = new OsuScrollContainer
+ scroll = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
diff --git a/osu.Game/Overlays/Changelog/Comments.cs b/osu.Game/Overlays/Changelog/Comments.cs
deleted file mode 100644
index 4cf39e7b44..0000000000
--- a/osu.Game/Overlays/Changelog/Comments.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Game.Online.API.Requests.Responses;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.Changelog
-{
- public class Comments : CompositeDrawable
- {
- private readonly APIChangelogBuild build;
-
- public Comments(APIChangelogBuild build)
- {
- this.build = build;
-
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- Padding = new MarginPadding
- {
- Horizontal = 50,
- Vertical = 20,
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- LinkFlowContainer text;
-
- InternalChildren = new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- CornerRadius = 10,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colours.GreyVioletDarker
- },
- },
- text = new LinkFlowContainer(t =>
- {
- t.Colour = colours.PinkLighter;
- t.Font = OsuFont.Default.With(size: 14);
- })
- {
- Padding = new MarginPadding(20),
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
- };
-
- text.AddParagraph("Got feedback?", t =>
- {
- t.Colour = Color4.White;
- t.Font = OsuFont.Default.With(italics: true, size: 20);
- t.Padding = new MarginPadding { Bottom = 20 };
- });
-
- text.AddParagraph("We would love to hear what you think of this update! ");
- text.AddIcon(FontAwesome.Regular.GrinHearts);
-
- text.AddParagraph("Please visit the ");
- text.AddLink("web version", $"{build.Url}#comments");
- text.AddText(" of this changelog to leave any comments.");
- }
- }
-}
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index d13ac5c2de..726be9e194 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
- new OsuScrollContainer
+ new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index 591a9dc86e..e7bfeaf968 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Comments
request?.Cancel();
loadCancellation?.Cancel();
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
- request.Success += onSuccess;
+ request.Success += res => Schedule(() => onSuccess(res));
api.PerformAsync(request);
}
diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs
new file mode 100644
index 0000000000..9ee679a866
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.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.
+
+namespace osu.Game.Overlays.Dashboard
+{
+ public class DashboardOverlayHeader : TabControlOverlayHeader
+ {
+ protected override OverlayTitle CreateTitle() => new DashboardTitle();
+
+ private class DashboardTitle : OverlayTitle
+ {
+ public DashboardTitle()
+ {
+ Title = "dashboard";
+ IconTexture = "Icons/changelog";
+ }
+ }
+ }
+
+ public enum DashboardOverlayTabs
+ {
+ Friends
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index 3c9b31daae..79fda99c73 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Overlays.Dashboard.Friends
{
- public class FriendDisplay : CompositeDrawable
+ public class FriendDisplay : OverlayView>
{
private List users = new List();
@@ -26,34 +26,29 @@ namespace osu.Game.Overlays.Dashboard.Friends
set
{
users = value;
-
onlineStreamControl.Populate(value);
}
}
- [Resolved]
- private IAPIProvider api { get; set; }
-
- private GetFriendsRequest request;
private CancellationTokenSource cancellationToken;
private Drawable currentContent;
- private readonly FriendOnlineStreamControl onlineStreamControl;
- private readonly Box background;
- private readonly Box controlBackground;
- private readonly UserListToolbar userListToolbar;
- private readonly Container itemsPlaceholder;
- private readonly LoadingLayer loading;
+ private FriendOnlineStreamControl onlineStreamControl;
+ private Box background;
+ private Box controlBackground;
+ private UserListToolbar userListToolbar;
+ private Container itemsPlaceholder;
+ private LoadingLayer loading;
- public FriendDisplay()
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
@@ -134,11 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
}
}
};
- }
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
background.Colour = colourProvider.Background4;
controlBackground.Colour = colourProvider.Background5;
}
@@ -152,14 +143,11 @@ namespace osu.Game.Overlays.Dashboard.Friends
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
}
- public void Fetch()
- {
- if (!api.IsLoggedIn)
- return;
+ protected override APIRequest> CreateRequest() => new GetFriendsRequest();
- request = new GetFriendsRequest();
- request.Success += response => Schedule(() => Users = response);
- api.Queue(request);
+ protected override void OnSuccess(List response)
+ {
+ Users = response;
}
private void recreatePanels()
@@ -258,9 +246,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
protected override void Dispose(bool isDisposing)
{
- request?.Cancel();
cancellationToken?.Cancel();
-
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
new file mode 100644
index 0000000000..a72c3f4fa5
--- /dev/null
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -0,0 +1,150 @@
+// 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.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Overlays.Dashboard;
+using osu.Game.Overlays.Dashboard.Friends;
+
+namespace osu.Game.Overlays
+{
+ public class DashboardOverlay : FullscreenOverlay
+ {
+ private CancellationTokenSource cancellationToken;
+
+ private Box background;
+ private Container content;
+ private DashboardOverlayHeader header;
+ private LoadingLayer loading;
+ private OverlayScrollContainer scrollFlow;
+
+ public DashboardOverlay()
+ : base(OverlayColourScheme.Purple)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ scrollFlow = new OverlayScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ header = new DashboardOverlayHeader
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Depth = -float.MaxValue
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ }
+ },
+ loading = new LoadingLayer(content),
+ };
+
+ background.Colour = ColourProvider.Background5;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ header.Current.BindValueChanged(onTabChanged);
+ }
+
+ private bool displayUpdateRequired = true;
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ // We don't want to create a new display on every call, only when exiting from fully closed state.
+ if (displayUpdateRequired)
+ {
+ header.Current.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ loadDisplay(Empty());
+ displayUpdateRequired = true;
+ }
+
+ private void loadDisplay(Drawable display)
+ {
+ scrollFlow.ScrollToStart();
+
+ LoadComponentAsync(display, loaded =>
+ {
+ if (API.IsLoggedIn)
+ loading.Hide();
+
+ content.Child = loaded;
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ private void onTabChanged(ValueChangedEvent tab)
+ {
+ cancellationToken?.Cancel();
+ loading.Show();
+
+ if (!API.IsLoggedIn)
+ {
+ loadDisplay(Empty());
+ return;
+ }
+
+ switch (tab.NewValue)
+ {
+ case DashboardOverlayTabs.Friends:
+ loadDisplay(new FriendDisplay());
+ break;
+
+ default:
+ throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
+ }
+ }
+
+ public override void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ if (State.Value == Visibility.Hidden)
+ return;
+
+ header.Current.TriggerChange();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs b/osu.Game/Overlays/Direct/DirectRulesetSelector.cs
deleted file mode 100644
index 106aaa616b..0000000000
--- a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Rulesets;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.Direct
-{
- public class DirectRulesetSelector : RulesetSelector
- {
- public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;
-
- public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput;
-
- public override bool PropagatePositionalInputSubTree => !Current.Disabled && base.PropagatePositionalInputSubTree;
-
- public DirectRulesetSelector()
- {
- TabContainer.Masking = false;
- TabContainer.Spacing = new Vector2(10, 0);
- AutoSizeAxes = Axes.Both;
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Current.BindDisabledChanged(value => SelectedTab.FadeColour(value ? Color4.DarkGray : Color4.White, 200, Easing.OutQuint), true);
- }
-
- protected override TabItem CreateTabItem(RulesetInfo value) => new DirectRulesetTabItem(value);
-
- protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- };
-
- private class DirectRulesetTabItem : TabItem
- {
- private readonly ConstrainedIconContainer iconContainer;
-
- public DirectRulesetTabItem(RulesetInfo value)
- : base(value)
- {
- AutoSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- iconContainer = new ConstrainedIconContainer
- {
- Icon = value.CreateInstance().CreateIcon(),
- Size = new Vector2(32),
- },
- new HoverClickSounds()
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- updateState();
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- base.OnHover(e);
- updateState();
- return true;
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- base.OnHoverLost(e);
- updateState();
- }
-
- protected override void OnActivated() => updateState();
-
- protected override void OnDeactivated() => updateState();
-
- private void updateState() => iconContainer.FadeColour(IsHovered || Active.Value ? Color4.White : Color4.Gray, 120, Easing.InQuad);
- }
- }
-}
diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs
deleted file mode 100644
index e5b2b5cc34..0000000000
--- a/osu.Game/Overlays/Direct/FilterControl.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Game.Graphics;
-using osu.Game.Online.API.Requests;
-using osu.Game.Overlays.SearchableList;
-using osu.Game.Rulesets;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.Direct
-{
- public class FilterControl : SearchableListFilterControl
- {
- private DirectRulesetSelector rulesetSelector;
-
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552");
- protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked;
- protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard;
-
- protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector();
-
- public Bindable Ruleset => rulesetSelector.Current;
-
- [BackgroundDependencyLoader(true)]
- private void load(OsuColour colours, Bindable ruleset)
- {
- DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark;
- rulesetSelector.Current.BindTo(ruleset);
- }
- }
-
- public enum DirectSortCriteria
- {
- Title,
- Artist,
- Difficulty,
- Ranked,
- Rating,
- Plays,
- Favourites,
- Relevance,
- }
-}
diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs
deleted file mode 100644
index 5b3e394a18..0000000000
--- a/osu.Game/Overlays/Direct/Header.cs
+++ /dev/null
@@ -1,43 +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.ComponentModel;
-using osu.Framework.Extensions.Color4Extensions;
-using osuTK.Graphics;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Overlays.SearchableList;
-
-namespace osu.Game.Overlays.Direct
-{
- public class Header : SearchableListHeader
- {
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"252f3a");
-
- protected override DirectTab DefaultTab => DirectTab.Search;
- protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", Font = OsuFont.GetFont(size: 25) };
- protected override IconUsage Icon => OsuIcon.ChevronDownCircle;
-
- public Header()
- {
- Tabs.Current.Value = DirectTab.NewestMaps;
- Tabs.Current.TriggerChange();
- }
- }
-
- public enum DirectTab
- {
- Search,
-
- [Description("Newest Maps")]
- NewestMaps = DirectSortCriteria.Ranked,
-
- [Description("Top Rated")]
- TopRated = DirectSortCriteria.Rating,
-
- [Description("Most Played")]
- MostPlayed = DirectSortCriteria.Plays,
- }
-}
diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs
deleted file mode 100644
index 61986d1cf0..0000000000
--- a/osu.Game/Overlays/DirectOverlay.cs
+++ /dev/null
@@ -1,298 +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.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Humanizer;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Threading;
-using osu.Game.Audio;
-using osu.Game.Beatmaps;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests;
-using osu.Game.Overlays.Direct;
-using osu.Game.Overlays.SearchableList;
-using osu.Game.Rulesets;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays
-{
- public class DirectOverlay : SearchableListOverlay
- {
- private const float panel_padding = 10f;
-
- [Resolved]
- private RulesetStore rulesets { get; set; }
-
- private readonly FillFlowContainer resultCountsContainer;
- private readonly OsuSpriteText resultCountsText;
- private FillFlowContainer panels;
-
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"485e74");
- protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"465b71");
- protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265");
-
- protected override SearchableListHeader CreateHeader() => new Header();
- protected override SearchableListFilterControl CreateFilterControl() => new FilterControl();
-
- private IEnumerable beatmapSets;
-
- public IEnumerable BeatmapSets
- {
- get => beatmapSets;
- set
- {
- if (ReferenceEquals(beatmapSets, value)) return;
-
- beatmapSets = value?.ToList();
-
- if (beatmapSets == null) return;
-
- var artists = new List();
- var songs = new List();
- var tags = new List();
-
- foreach (var s in beatmapSets)
- {
- artists.Add(s.Metadata.Artist);
- songs.Add(s.Metadata.Title);
- tags.AddRange(s.Metadata.Tags.Split(' '));
- }
-
- ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags));
- }
- }
-
- private ResultCounts resultAmounts;
-
- public ResultCounts ResultAmounts
- {
- get => resultAmounts;
- set
- {
- if (value == ResultAmounts) return;
-
- resultAmounts = value;
-
- updateResultCounts();
- }
- }
-
- public DirectOverlay()
- : base(OverlayColourScheme.Blue)
- {
- ScrollFlow.Children = new Drawable[]
- {
- resultCountsContainer = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Margin = new MarginPadding { Top = 5 },
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Text = "Found ",
- Font = OsuFont.GetFont(size: 15)
- },
- resultCountsText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold)
- },
- }
- },
- };
-
- Filter.Search.Current.ValueChanged += text =>
- {
- if (!string.IsNullOrEmpty(text.NewValue))
- {
- Header.Tabs.Current.Value = DirectTab.Search;
-
- if (Filter.Tabs.Current.Value == DirectSortCriteria.Ranked)
- Filter.Tabs.Current.Value = DirectSortCriteria.Relevance;
- }
- else
- {
- Header.Tabs.Current.Value = DirectTab.NewestMaps;
-
- if (Filter.Tabs.Current.Value == DirectSortCriteria.Relevance)
- Filter.Tabs.Current.Value = DirectSortCriteria.Ranked;
- }
- };
- ((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch();
- Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
- Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch();
-
- Header.Tabs.Current.ValueChanged += tab =>
- {
- if (tab.NewValue != DirectTab.Search)
- {
- currentQuery.Value = string.Empty;
- Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value;
- queueUpdateSearch();
- }
- };
-
- currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue));
-
- currentQuery.BindTo(Filter.Search.Current);
-
- Filter.Tabs.Current.ValueChanged += tab =>
- {
- if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value)
- Header.Tabs.Current.Value = DirectTab.Search;
-
- queueUpdateSearch();
- };
-
- updateResultCounts();
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- resultCountsContainer.Colour = colours.Yellow;
- }
-
- private void updateResultCounts()
- {
- resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, Easing.OutQuint);
- if (ResultAmounts == null) return;
-
- resultCountsText.Text = "Artist".ToQuantity(ResultAmounts.Artists) + ", " +
- "Song".ToQuantity(ResultAmounts.Songs) + ", " +
- "Tag".ToQuantity(ResultAmounts.Tags);
- }
-
- private void recreatePanels(PanelDisplayStyle displayStyle)
- {
- if (panels != null)
- {
- panels.FadeOut(200);
- panels.Expire();
- panels = null;
- }
-
- if (BeatmapSets == null) return;
-
- var newPanels = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(panel_padding),
- Margin = new MarginPadding { Top = 10 },
- ChildrenEnumerable = BeatmapSets.Select(b =>
- {
- switch (displayStyle)
- {
- case PanelDisplayStyle.Grid:
- return new DirectGridPanel(b)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- };
-
- default:
- return new DirectListPanel(b);
- }
- })
- };
-
- LoadComponentAsync(newPanels, p =>
- {
- if (panels != null) ScrollFlow.Remove(panels);
- ScrollFlow.Add(panels = newPanels);
- });
- }
-
- protected override void PopIn()
- {
- base.PopIn();
-
- // Queries are allowed to be run only on the first pop-in
- if (getSetsRequest == null)
- queueUpdateSearch();
- }
-
- private SearchBeatmapSetsRequest getSetsRequest;
-
- private readonly Bindable currentQuery = new Bindable(string.Empty);
-
- private ScheduledDelegate queryChangedDebounce;
-
- [Resolved]
- private PreviewTrackManager previewTrackManager { get; set; }
-
- private void queueUpdateSearch(bool queryTextChanged = false)
- {
- BeatmapSets = null;
- ResultAmounts = null;
-
- getSetsRequest?.Cancel();
-
- queryChangedDebounce?.Cancel();
- queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
- }
-
- private void updateSearch()
- {
- if (!IsLoaded)
- return;
-
- if (State.Value == Visibility.Hidden)
- return;
-
- if (API == null)
- return;
-
- previewTrackManager.StopAnyPlaying(this);
-
- getSetsRequest = new SearchBeatmapSetsRequest(
- currentQuery.Value,
- ((FilterControl)Filter).Ruleset.Value,
- Filter.DisplayStyleControl.Dropdown.Current.Value,
- Filter.Tabs.Current.Value); //todo: sort direction (?)
-
- getSetsRequest.Success += response =>
- {
- Task.Run(() =>
- {
- var sets = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
-
- // may not need scheduling; loads async internally.
- Schedule(() =>
- {
- BeatmapSets = sets;
- recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value);
- });
- });
- };
-
- API.Queue(getSetsRequest);
- }
-
- private int distinctCount(List list) => list.Distinct().ToArray().Length;
-
- public class ResultCounts
- {
- public readonly int Artists;
- public readonly int Songs;
- public readonly int Tags;
-
- public ResultCounts(int artists, int songs, int tags)
- {
- Artists = artists;
- Songs = songs;
- Tags = tags;
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
index 56e93b6a1e..5b44c486a3 100644
--- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
@@ -9,7 +10,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class GlobalKeyBindingsSection : SettingsSection
{
- public override IconUsage Icon => FontAwesome.Solid.Globe;
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Globe
+ };
+
public override string Header => "Global";
public GlobalKeyBindingsSection(GlobalActionContainer manager)
diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
index 1f4042c57c..332fb6c8fc 100644
--- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
@@ -10,7 +11,11 @@ namespace osu.Game.Overlays.KeyBinding
{
public class RulesetBindingsSection : SettingsSection
{
- public override IconUsage Icon => (ruleset.CreateInstance().CreateIcon() as SpriteIcon)?.Icon ?? OsuIcon.Hot;
+ public override Drawable CreateIcon() => ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon
+ {
+ Icon = OsuIcon.Hot
+ };
+
public override string Header => ruleset.Name;
private readonly RulesetInfo ruleset;
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index e9b3598625..3d0ad1a594 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -37,7 +37,6 @@ namespace osu.Game.Overlays.Mods
protected readonly TriangleButton CloseButton;
protected readonly OsuSpriteText MultiplierLabel;
- protected readonly OsuSpriteText UnrankedLabel;
protected override bool BlockNonPositionalInput => false;
@@ -57,6 +56,8 @@ namespace osu.Game.Overlays.Mods
protected Color4 HighMultiplierColour;
private const float content_width = 0.8f;
+ private const float footer_button_spacing = 20;
+
private readonly FillFlowContainer footerContainer;
private SampleChannel sampleOn, sampleOff;
@@ -103,7 +104,7 @@ namespace osu.Game.Overlays.Mods
{
new Dimension(GridSizeMode.Absolute, 90),
new Dimension(GridSizeMode.Distributed),
- new Dimension(GridSizeMode.Absolute, 70),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@@ -197,7 +198,8 @@ namespace osu.Game.Overlays.Mods
// Footer
new Container
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Children = new Drawable[]
@@ -215,7 +217,9 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Width = content_width,
- Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2),
+ LayoutDuration = 100,
+ LayoutEasing = Easing.OutQuint,
Padding = new MarginPadding
{
Vertical = 15,
@@ -228,10 +232,8 @@ namespace osu.Game.Overlays.Mods
Width = 180,
Text = "Deselect All",
Action = DeselectAll,
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
CustomiseButton = new TriangleButton
{
@@ -239,49 +241,41 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1,
Enabled = { Value = false },
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
CloseButton = new TriangleButton
{
Width = 180,
Text = "Close",
Action = Hide,
- Margin = new MarginPadding
- {
- Right = 20
- }
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
},
- new OsuSpriteText
+ new FillFlowContainer
{
- Text = @"Score Multiplier:",
- Font = OsuFont.GetFont(size: 30),
- Margin = new MarginPadding
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(footer_button_spacing / 2, 0),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Children = new Drawable[]
{
- Top = 5,
- Right = 10
- }
+ new OsuSpriteText
+ {
+ Text = @"Score Multiplier:",
+ Font = OsuFont.GetFont(size: 30),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ },
+ MultiplierLabel = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes.
+ },
+ },
},
- MultiplierLabel = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
- Margin = new MarginPadding
- {
- Top = 5
- }
- },
- UnrankedLabel = new OsuSpriteText
- {
- Text = @"(Unranked)",
- Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
- Margin = new MarginPadding
- {
- Top = 5,
- Left = 10
- }
- }
}
}
},
@@ -327,7 +321,6 @@ namespace osu.Game.Overlays.Mods
{
LowMultiplierColour = colours.Red;
HighMultiplierColour = colours.Green;
- UnrankedLabel.Colour = colours.Blue;
availableMods = osu.AvailableMods.GetBoundCopy();
@@ -431,12 +424,10 @@ namespace osu.Game.Overlays.Mods
private void updateMods()
{
var multiplier = 1.0;
- var ranked = true;
foreach (var mod in SelectedMods.Value)
{
multiplier *= mod.ScoreMultiplier;
- ranked &= mod.Ranked;
}
MultiplierLabel.Text = $"{multiplier:N2}x";
@@ -446,8 +437,6 @@ namespace osu.Game.Overlays.Mods
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
else
MultiplierLabel.FadeColour(Color4.White, 200);
-
- UnrankedLabel.FadeTo(ranked ? 0 : 1, 200);
}
private void updateModSettings(ValueChangedEvent> selectedMods)
diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionsDropdown.cs
index 4f59b053b6..5bd321f31e 100644
--- a/osu.Game/Overlays/Music/CollectionsDropdown.cs
+++ b/osu.Game/Overlays/Music/CollectionsDropdown.cs
@@ -29,14 +29,8 @@ namespace osu.Game.Overlays.Music
{
public CollectionsMenu()
{
+ Masking = true;
CornerRadius = 5;
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Colour = Color4.Black.Opacity(0.3f),
- Radius = 3,
- Offset = new Vector2(0f, 1f),
- };
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index d788929739..c872f82b32 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved;
- beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()));
+ beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next()));
}
protected override void LoadComplete()
@@ -172,10 +172,15 @@ namespace osu.Game.Overlays
}
///
- /// Play the previous track or restart the current track if it's current time below
+ /// Play the previous track or restart the current track if it's current time below .
///
- /// The that indicate the decided action
- public PreviousTrackResult PreviousTrack()
+ public void PreviousTrack() => Schedule(() => prev());
+
+ ///
+ /// Play the previous track or restart the current track if it's current time below .
+ ///
+ /// The that indicate the decided action.
+ private PreviousTrackResult prev()
{
var currentTrackPosition = current?.Track.CurrentTime;
@@ -204,8 +209,7 @@ namespace osu.Game.Overlays
///
/// Play the next random or playlist track.
///
- /// Whether the operation was successful.
- public bool NextTrack() => next();
+ public void NextTrack() => Schedule(() => next());
private bool next(bool instant = false)
{
@@ -319,13 +323,13 @@ namespace osu.Game.Overlays
return true;
case GlobalAction.MusicNext:
- if (NextTrack())
+ if (next())
onScreenDisplay?.Display(new MusicControllerToast("Next track"));
return true;
case GlobalAction.MusicPrev:
- switch (PreviousTrack())
+ switch (prev())
{
case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicControllerToast("Restart track"));
diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs
index 71c205ff63..46d692d44d 100644
--- a/osu.Game/Overlays/NewsOverlay.cs
+++ b/osu.Game/Overlays/NewsOverlay.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
using osu.Game.Overlays.News;
namespace osu.Game.Overlays
@@ -36,7 +35,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = colours.PurpleDarkAlternative
},
- new OsuScrollContainer
+ new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index 4ac0f697c3..dbc934bde9 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -12,6 +12,8 @@ namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
+ public const int CONTENT_X_MARGIN = 50;
+
private readonly Box titleBackground;
protected readonly FillFlowContainer HeaderInfo;
@@ -54,7 +56,7 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
+ Horizontal = CONTENT_X_MARGIN,
},
Children = new[]
{
diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs
new file mode 100644
index 0000000000..e7415e6f74
--- /dev/null
+++ b/osu.Game/Overlays/OverlayScrollContainer.cs
@@ -0,0 +1,149 @@
+// 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.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays
+{
+ ///
+ /// which provides . Mostly used in .
+ ///
+ public class OverlayScrollContainer : OsuScrollContainer
+ {
+ ///
+ /// Scroll position at which the will be shown.
+ ///
+ private const int button_scroll_position = 200;
+
+ protected readonly ScrollToTopButton Button;
+
+ public OverlayScrollContainer()
+ {
+ AddInternal(Button = new ScrollToTopButton
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding(20),
+ Action = () =>
+ {
+ ScrollToStart();
+ Button.State = Visibility.Hidden;
+ }
+ });
+ }
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
+ {
+ Button.State = Visibility.Hidden;
+ return;
+ }
+
+ Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden;
+ }
+
+ public class ScrollToTopButton : OsuHoverContainer
+ {
+ private const int fade_duration = 500;
+
+ private Visibility state;
+
+ public Visibility State
+ {
+ get => state;
+ set
+ {
+ if (value == state)
+ return;
+
+ state = value;
+ Enabled.Value = state == Visibility.Visible;
+ this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint);
+ }
+ }
+
+ protected override IEnumerable EffectTargets => new[] { background };
+
+ private Color4 flashColour;
+
+ private readonly Container content;
+ private readonly Box background;
+
+ public ScrollToTopButton()
+ {
+ Size = new Vector2(50);
+ Alpha = 0;
+ Add(content = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Offset = new Vector2(0f, 1f),
+ Radius = 3f,
+ Colour = Color4.Black.Opacity(0.25f),
+ },
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(15),
+ Icon = FontAwesome.Solid.ChevronUp
+ }
+ }
+ });
+
+ TooltipText = "Scroll to top";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = colourProvider.Background6;
+ HoverColour = colourProvider.Background5;
+ flashColour = colourProvider.Light1;
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ background.FlashColour(flashColour, 800, Easing.OutQuint);
+ return base.OnClick(e);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ content.ScaleTo(0.75f, 2000, Easing.OutQuint);
+ return true;
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ content.ScaleTo(1, 1000, Easing.OutElastic);
+ base.OnMouseUp(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs
new file mode 100644
index 0000000000..3e2c54c726
--- /dev/null
+++ b/osu.Game/Overlays/OverlayView.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+
+namespace osu.Game.Overlays
+{
+ ///
+ /// A subview containing online content, to be displayed inside a .
+ ///
+ ///
+ /// Automatically performs a data fetch on load.
+ ///
+ /// The type of the API response.
+ public abstract class OverlayView : CompositeDrawable, IOnlineComponent
+ where T : class
+ {
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ private APIRequest request;
+
+ protected OverlayView()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ API.Register(this);
+ }
+
+ ///
+ /// Create the API request for fetching data.
+ ///
+ protected abstract APIRequest CreateRequest();
+
+ ///
+ /// Fired when results arrive from the main API request.
+ ///
+ ///
+ protected abstract void OnSuccess(T response);
+
+ ///
+ /// Force a re-request for data from the API.
+ ///
+ protected void PerformFetch()
+ {
+ request?.Cancel();
+
+ request = CreateRequest();
+ request.Success += response => Schedule(() => OnSuccess(response));
+
+ API.Queue(request);
+ }
+
+ public virtual void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ switch (state)
+ {
+ case APIState.Online:
+ PerformFetch();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ request?.Cancel();
+ API?.Unregister(this);
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index fcd12e2b54..191f3c908a 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -7,7 +7,7 @@ using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Overlays.Direct;
+using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Users;
using osuTK;
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue
? null
- : new DirectGridPanel(model.ToBeatmapSet(Rulesets))
+ : new GridBeatmapPanel(model.ToBeatmapSet(Rulesets))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
index 6f06eecd6e..917509e842 100644
--- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
@@ -12,10 +12,10 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables;
using System.Linq;
-using osu.Game.Overlays.Direct;
using System.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.BeatmapListing.Panels;
namespace osu.Game.Overlays.Rankings
{
@@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Rankings
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10),
- Children = response.BeatmapSets.Select(b => new DirectGridPanel(b.ToBeatmapSet(rulesets))
+ Children = response.BeatmapSets.Select(b => new GridBeatmapPanel(b.ToBeatmapSet(rulesets))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs
index afb23883ac..7b200d4226 100644
--- a/osu.Game/Overlays/RankingsOverlay.cs
+++ b/osu.Game/Overlays/RankingsOverlay.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Overlays
protected Bindable Scope => header.Current;
- private readonly BasicScrollContainer scrollFlow;
+ private readonly OverlayScrollContainer scrollFlow;
private readonly Container contentContainer;
private readonly LoadingLayer loading;
private readonly Box background;
@@ -44,7 +44,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both
},
- scrollFlow = new BasicScrollContainer
+ scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
index d6174e0733..4ab2de06b6 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Backgrounds;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
namespace osu.Game.Overlays.SearchableList
@@ -72,7 +71,7 @@ namespace osu.Game.Overlays.SearchableList
{
RelativeSizeAxes = Axes.Both,
Masking = true,
- Child = new OsuScrollContainer
+ Child = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs
index b18488b616..69538358f1 100644
--- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs
@@ -13,9 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections
{
public override string Header => "Audio";
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.VolumeUp
+ };
- public override IconUsage Icon => FontAwesome.Solid.VolumeUp;
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" });
public AudioSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs
index f62de0b243..44d4088972 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class DebugSection : SettingsSection
{
public override string Header => "Debug";
- public override IconUsage Icon => FontAwesome.Solid.Bug;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Bug
+ };
public DebugSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 2d2cd42213..93a02ea0e4 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -53,10 +53,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Keywords = new[] { "hp", "bar" }
},
new SettingsCheckbox
+ {
+ LabelText = "Fade playfield to red when health is low",
+ Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow),
+ },
+ new SettingsCheckbox
{
LabelText = "Always show key overlay",
Bindable = config.GetBindable(OsuSetting.KeyOverlay)
},
+ new SettingsCheckbox
+ {
+ LabelText = "Positional hitsounds",
+ Bindable = config.GetBindable(OsuSetting.PositionalHitSounds)
+ },
new SettingsEnumDropdown
{
LabelText = "Score meter type",
diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
index 97d9d3c697..aca507f20a 100644
--- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs
@@ -13,7 +13,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GameplaySection : SettingsSection
{
public override string Header => "Gameplay";
- public override IconUsage Icon => FontAwesome.Regular.Circle;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Regular.Circle
+ };
public GameplaySection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
index d9947f16cc..fefc3fe6a7 100644
--- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GeneralSection : SettingsSection
{
public override string Header => "General";
- public override IconUsage Icon => FontAwesome.Solid.Cog;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Cog
+ };
public GeneralSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
index 89caa3dc8f..c1b4b0bbcb 100644
--- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class GraphicsSection : SettingsSection
{
public override string Header => "Graphics";
- public override IconUsage Icon => FontAwesome.Solid.Laptop;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Laptop
+ };
public GraphicsSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 2a348b4e03..b43453f53d 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -10,7 +10,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class InputSection : SettingsSection
{
public override string Header => "Input";
- public override IconUsage Icon => FontAwesome.Regular.Keyboard;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Keyboard
+ };
public InputSection(KeyBindingPanel keyConfig)
{
diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
index 0f3acd5b7f..73c88b8e71 100644
--- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs
@@ -11,7 +11,11 @@ namespace osu.Game.Overlays.Settings.Sections
public class MaintenanceSection : SettingsSection
{
public override string Header => "Maintenance";
- public override IconUsage Icon => FontAwesome.Solid.Wrench;
+
+ public override Drawable CreateIcon() => new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.Wrench
+ };
public MaintenanceSection()
{
diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
index a8b3e45a83..23513eade8 100644
--- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs
@@ -21,6 +21,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
LabelText = "Warn about opening external links",
Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning)
},
+ new SettingsCheckbox
+ {
+ LabelText = "Prefer downloads without video",
+ Keywords = new[] { "no-video" },
+ Bindable = config.GetBindable