diff --git a/global.json b/global.json
index 0223dc7330..6c793a3f1d 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.32"
+ "Microsoft.Build.Traversal": "2.0.34"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 77365b51a9..d2bdbc8b61 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/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
new file mode 100644
index 0000000000..0c46b078b5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public abstract class CatchSkinnableTestScene : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(CatchRuleset),
+ typeof(CatchLegacySkinTransformer),
+ };
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index fe0d512166..acc5f4e428 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -4,21 +4,21 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Tests.Visual;
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Graphics;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : SkinnableTestScene
+ public class TestSceneCatcher : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(CatcherArea),
typeof(CatcherSprite)
- };
+ }).ToList();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index cf68c5424d..2b30edb70b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -17,12 +17,11 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcherArea : SkinnableTestScene
+ public class TestSceneCatcherArea : CatchSkinnableTestScene
{
private RulesetInfo catchRuleset;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 82d5aa936f..cd674bb754 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -3,20 +3,20 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneFruitObjects : SkinnableTestScene
+ public class TestSceneFruitObjects : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(CatchHitObject),
typeof(Fruit),
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
typeof(DrawableBanana),
typeof(DrawableBananaShower),
typeof(Pulp),
- };
+ }).ToList();
protected override void LoadComplete()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 49ab70f5d7..347b71f3ff 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -4,15 +4,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
-using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
@@ -32,29 +27,113 @@ namespace osu.Game.Rulesets.Catch.Tests
private SkinManager skins { get; set; }
[Test]
- public void TestHyperDashCatcherColour()
+ public void TestDefaultCatcherColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomCatcherColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR, skin.HyperDashAfterImageColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour, skin.HyperDashAfterImageColour);
+ }
+
+ [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 checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
{
CatcherArea catcherArea = null;
- AddStep("setup catcher", () =>
+ AddStep("create hyper-dashing catcher", () =>
{
Child = setupSkinHierarchy(catcherArea = new CatcherArea
{
- RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4f),
- }, false, false, false);
- });
+ }, skin);
- AddStep("start hyper-dashing", () =>
- {
catcherArea.MovableCatcher.SetHyperDashState(2);
catcherArea.MovableCatcher.FinishTransforms();
});
- AddAssert("catcher has default hyper-dash colour", () => catcherArea.MovableCatcher.Colour == Color4.OrangeRed);
- AddAssert("catcher trails have default hyper-dash colour", () => catcherArea.OfType>().Any(c => c.Colour == Catcher.DefaultHyperDashColour));
+ AddAssert("catcher colour is correct", () =>
+ expectedCatcherColour == Catcher.DEFAULT_HYPER_DASH_COLOUR
+ ? catcherArea.MovableCatcher.Colour == Catcher.DEFAULT_CATCHER_HYPER_DASH_COLOUR
+ : catcherArea.MovableCatcher.Colour == expectedCatcherColour);
+
+ AddAssert("catcher trails colours are correct", () => catcherArea.OfType>().Any(c => c.Colour == expectedCatcherColour));
+ AddAssert("catcher end-glow colours are correct", () => catcherArea.OfType>().Any(c => c.Colour == (expectedEndGlowColour ?? expectedCatcherColour)));
AddStep("finish hyper-dashing", () =>
{
@@ -62,111 +141,14 @@ namespace osu.Game.Rulesets.Catch.Tests
catcherArea.MovableCatcher.FinishTransforms();
});
- AddAssert("hyper-dash colour cleared from catcher", () => catcherArea.MovableCatcher.Colour == Color4.White);
+ AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
}
- [Test]
- public void TestCustomHyperDashCatcherColour()
- {
- CatcherArea catcherArea = null;
-
- AddStep("setup catcher", () =>
- {
- Child = setupSkinHierarchy(catcherArea = new CatcherArea
- {
- RelativePositionAxes = Axes.None,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
- }, true, false, false);
- });
-
- AddStep("start hyper-dashing", () =>
- {
- catcherArea.MovableCatcher.SetHyperDashState(2);
- catcherArea.MovableCatcher.FinishTransforms();
- });
-
- AddAssert("catcher use hyper-dash colour from skin", () => catcherArea.MovableCatcher.Colour == TestSkin.CustomHyperDashColour);
- AddAssert("catcher trails use hyper-dash colour from skin", () => catcherArea.OfType>().Any(c => c.Colour == TestSkin.CustomHyperDashColour));
-
- AddStep("clear hyper-dash", () =>
- {
- catcherArea.MovableCatcher.SetHyperDashState(1);
- catcherArea.MovableCatcher.FinishTransforms();
- });
-
- AddAssert("hyper-dash colour cleared from catcher", () => catcherArea.MovableCatcher.Colour == Color4.White);
- }
-
- [Test]
- public void TestHyperDashCatcherEndGlowColour()
- {
- CatcherArea catcherArea = null;
-
- AddStep("setup catcher", () =>
- {
- Child = setupSkinHierarchy(catcherArea = new CatcherArea
- {
- RelativePositionAxes = Axes.None,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
- }, false, false, false);
- });
-
- AddStep("start hyper-dashing", () => catcherArea.MovableCatcher.SetHyperDashState(2));
- AddAssert("end-glow catcher sprite has default hyper-dash colour", () => catcherArea.OfType>().Any(c => c.Colour == Catcher.DefaultHyperDashColour));
- }
-
- [TestCase(true)]
- [TestCase(false)]
- public void TestCustomHyperDashCatcherEndGlowColour(bool customHyperDashCatcherColour)
- {
- CatcherArea catcherArea = null;
-
- AddStep("setup catcher", () =>
- {
- Child = setupSkinHierarchy(catcherArea = new CatcherArea
- {
- RelativePositionAxes = Axes.None,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
- }, customHyperDashCatcherColour, true, false);
- });
-
- AddStep("start hyper-dashing", () => catcherArea.MovableCatcher.SetHyperDashState(2));
- AddAssert("end-glow catcher sprite use its hyper-dash colour from skin", () => catcherArea.OfType>().Any(c => c.Colour == TestSkin.CustomHyperDashAfterColour));
- }
-
- [Test]
- public void TestCustomHyperDashCatcherEndGlowColourFallback()
- {
- CatcherArea catcherArea = null;
-
- AddStep("setup catcher", () =>
- {
- Child = setupSkinHierarchy(catcherArea = new CatcherArea
- {
- RelativePositionAxes = Axes.None,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
- }, true, false, false);
- });
-
- AddStep("start hyper-dashing", () => catcherArea.MovableCatcher.SetHyperDashState(2));
- AddAssert("end-glow catcher sprite colour falls back to catcher colour from skin", () => catcherArea.OfType>().Any(c => c.Colour == TestSkin.CustomHyperDashColour));
- }
-
- [TestCase(false)]
- [TestCase(true)]
- public void TestHyperDashFruitColour(bool legacyFruit)
+ private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
{
DrawableFruit drawableFruit = null;
- AddStep("setup hyper-dash fruit", () =>
+ AddStep("create hyper-dash fruit", () =>
{
var fruit = new Fruit { HyperDashTarget = new Banana() };
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@@ -176,130 +158,50 @@ namespace osu.Game.Rulesets.Catch.Tests
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4f),
- }, false, false, false, legacyFruit);
+ }, skin);
});
- AddAssert("hyper-dash fruit has default colour", () =>
- legacyFruit
- ? checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)
- : checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour));
+ AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
}
- [TestCase(false, true)]
- [TestCase(false, false)]
- [TestCase(true, true)]
- [TestCase(true, false)]
- public void TestCustomHyperDashFruitColour(bool legacyFruit, bool customCatcherHyperDashColour)
+ private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
{
- DrawableFruit drawableFruit = null;
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var testSkinProvider = new SkinProvidingContainer(skin);
+ var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
- AddStep("setup 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),
- }, customCatcherHyperDashColour, false, true, legacyFruit);
- });
-
- AddAssert("hyper-dash fruit use fruit colour from skin", () =>
- legacyFruit
- ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)
- : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour));
+ return legacySkinProvider
+ .WithChild(testSkinProvider
+ .WithChild(legacySkinTransformer
+ .WithChild(child)));
}
- [TestCase(false)]
- [TestCase(true)]
- public void TestCustomHyperDashFruitColourFallback(bool legacyFruit)
- {
- DrawableFruit drawableFruit = null;
-
- AddStep("setup 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),
- }, true, false, false, legacyFruit);
- });
-
- AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () =>
- legacyFruit
- ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)
- : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour));
- }
-
- private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour, bool legacySkin = true)
- {
- var testSkinProvider = new SkinProvidingContainer(new TestSkin(customCatcherColour, customAfterColour, customFruitColour));
-
- if (legacySkin)
- {
- var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
- var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
-
- return legacySkinProvider
- .WithChild(testSkinProvider
- .WithChild(legacySkinTransformer
- .WithChild(child)));
- }
-
- return testSkinProvider.WithChild(child);
- }
-
- private bool checkFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
- fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Single(c => c.BorderColour == expectedColour).Any(d => d.Colour == expectedColour);
-
private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour);
- private class TestSkin : ISkin
+ private class TestSkin : LegacySkin
{
- public static Color4 CustomHyperDashColour { get; } = Color4.Goldenrod;
- public static Color4 CustomHyperDashFruitColour { get; } = Color4.Cyan;
- public static Color4 CustomHyperDashAfterColour { get; } = Color4.Lime;
-
- private readonly bool customCatcherColour;
- private readonly bool customAfterColour;
- private readonly bool customFruitColour;
-
- public TestSkin(bool customCatcherColour, bool customAfterColour, bool customFruitColour)
+ public Color4 HyperDashColour
{
- this.customCatcherColour = customCatcherColour;
- this.customAfterColour = customAfterColour;
- this.customFruitColour = customFruitColour;
+ get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
}
- public Drawable GetDrawableComponent(ISkinComponent component) => null;
-
- public Texture GetTexture(string componentName) => null;
-
- public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
-
- public IBindable GetConfig(TLookup lookup)
+ public Color4 HyperDashAfterImageColour
{
- if (lookup is CatchSkinColour config)
- {
- if (config == CatchSkinColour.HyperDash && customCatcherColour)
- return SkinUtils.As(new Bindable(CustomHyperDashColour));
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
+ }
- if (config == CatchSkinColour.HyperDashFruit && customFruitColour)
- return SkinUtils.As(new Bindable(CustomHyperDashFruitColour));
+ public Color4 HyperDashFruitColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
+ }
- if (config == CatchSkinColour.HyperDashAfterImage && customAfterColour)
- return SkinUtils.As(new Bindable(CustomHyperDashAfterColour));
- }
-
- return null;
+ public TestSkin()
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
}
}
}
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 2437958916..7ac9f11ad6 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -7,10 +7,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
@@ -34,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
}
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(DrawableHitObject drawableObject)
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
hitObject = drawableCatchObject.HitObject;
@@ -63,10 +61,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
},
});
- var hyperDashColour =
- skin.GetHyperDashFruitColour()?.Value ??
- Catcher.DefaultHyperDashColour;
-
if (hitObject.HyperDash)
{
AddInternal(new Circle
@@ -74,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- BorderColour = hyperDashColour,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 12f * RADIUS_ADJUST,
Children = new Drawable[]
{
@@ -84,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
- Colour = hyperDashColour,
+ 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
index 2ad8f89739..4506111498 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
@@ -6,12 +6,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
public enum CatchSkinColour
{
///
- /// The colour to be used for the catcher while on hyper-dashing state.
+ /// The colour to be used for the catcher while in hyper-dashing state.
///
HyperDash,
///
- /// The colour to be used for hyper-dash fruits.
+ /// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
///
HyperDashFruit,
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs
deleted file mode 100644
index 06d21f8c5e..0000000000
--- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Bindables;
-using osu.Game.Skinning;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Catch.Skinning
-{
- internal static class CatchSkinExtensions
- {
- public static IBindable GetHyperDashCatcherColour(this ISkin skin)
- => skin.GetConfig(CatchSkinColour.HyperDash);
-
- public static IBindable GetHyperDashCatcherAfterImageColour(this ISkin skin)
- => skin.GetConfig(CatchSkinColour.HyperDashAfterImage) ??
- skin.GetConfig(CatchSkinColour.HyperDash);
-
- public static IBindable GetHyperDashFruitColour(this ISkin skin)
- => skin.GetConfig(CatchSkinColour.HyperDashFruit) ??
- skin.GetConfig(CatchSkinColour.HyperDash);
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
index d8489399d2..5be54d3882 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -56,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
var hyperDash = new Sprite
{
- Texture = skin.GetTexture(lookupName),
- Colour = skin.GetHyperDashFruitColour()?.Value ?? Catcher.DefaultHyperDashColour,
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 7971a17e68..f37dae29dd 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class Catcher : SkinReloadableDrawable, IKeyBindingHandler
{
- public static Color4 DefaultHyperDashColour { get; } = Color4.Red;
+ public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
///
/// Whether we are hyper-dashing or not.
@@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Catch.UI
private CatcherSprite currentCatcher;
- private Color4 hyperDashColour = DefaultHyperDashColour;
- private Color4 hyperDashEndGlowColour = DefaultHyperDashColour;
+ private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
+ private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
private int currentDirection;
@@ -390,8 +390,14 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.SkinChanged(skin, allowFallback);
- hyperDashColour = skin.GetHyperDashCatcherColour()?.Value ?? DefaultHyperDashColour;
- hyperDashEndGlowColour = skin.GetHyperDashCatcherAfterImageColour()?.Value ?? DefaultHyperDashColour;
+ hyperDashColour =
+ skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
+ DEFAULT_HYPER_DASH_COLOUR;
+
+ hyperDashEndGlowColour =
+ skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ??
+ hyperDashColour;
+
updateCatcherColour(HyperDashing);
}
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/Resources/metrics-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png
new file mode 100644
index 0000000000..aa681f6f22
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png
new file mode 100644
index 0000000000..ca590eaf08
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
index eaa2a56e36..a3c1d518c5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.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;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
using osu.Game.Tests.Visual;
@@ -24,6 +27,15 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(ManiaRuleset),
+ typeof(ManiaLegacySkinTransformer),
+ typeof(ManiaSettingsSubsection)
+ };
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();
+
protected ManiaSkinnableTestScene()
{
scrollingInfo.Direction.Value = ScrollingDirection.Down;
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index a6bc64550f..6ab8a68176 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -10,11 +10,10 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
- public class TestSceneDrawableJudgement : SkinnableTestScene
+ public class TestSceneDrawableJudgement : ManiaSkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
index 0d5ebd33e9..37b97a444a 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
- Child = new ManiaStage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
+ Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
};
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
new file mode 100644
index 0000000000..a8fc68188a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.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 System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneStageBackground : ManiaSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DefaultStageBackground),
+ typeof(LegacyStageBackground),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
new file mode 100644
index 0000000000..d436445b59
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -0,0 +1,33 @@
+// 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.Graphics;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneStageForeground : ManiaSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(LegacyStageForeground),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
index 9aad08c433..5e06002f41 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
@@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mania.Tests
{
typeof(Column),
typeof(ColumnBackground),
- typeof(ColumnHitObjectArea)
+ typeof(ColumnHitObjectArea),
+ typeof(DefaultKeyArea),
+ typeof(DefaultHitTarget)
};
[Cached(typeof(IReadOnlyList))]
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index d5fd2808b8..7376a90f17 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
- private readonly List stages = new List();
+ private readonly List stages = new List();
private FillFlowContainer fill;
@@ -81,9 +81,9 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
}
- private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
+ private bool notesInStageAreAnchored(Stage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
- private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
+ private bool barsInStageAreAnchored(Stage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
private void createNote()
{
@@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var specialAction = ManiaAction.Special1;
- var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
+ var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
stages.Add(stage);
return new ScrollingTestContainer(direction)
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/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index 2371d74a2b..c0c8505f44 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Mania
HoldNoteHead,
HoldNoteTail,
HoldNoteBody,
- HitExplosion
+ HitExplosion,
+ StageBackground,
+ StageForeground,
}
}
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/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/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
index 8cd0272b52..6504321bb2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -50,12 +50,18 @@ namespace osu.Game.Rulesets.Mania.Skinning
Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
?? Color4.White;
+ Color4 backgroundColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
+ ?? Color4.Black;
+
+ Color4 lightColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
+ ?? Color4.White;
+
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black
+ Colour = backgroundColour
},
new Box
{
@@ -82,6 +88,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
+ Colour = lightColour,
Texture = skin.GetTexture(lightImage),
RelativeSizeAxes = Axes.X,
Width = 1,
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index c87a1d438b..ce0b9fe4b6 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
- explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d =>
+ explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
{
if (d == null)
return;
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
index 53e4f3cd14..40752d3f4b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning
{
@@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
bool showJudgementLine = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value
?? true;
+ Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value
+ ?? Color4.White;
+
InternalChild = directionContainer = new Container
{
Origin = Anchor.CentreLeft,
@@ -52,6 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
+ Colour = lineColour,
Alpha = showJudgementLine ? 0.9f : 0
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
index d2ceb06d0b..85523ae3c0 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Container directionContainer;
private Sprite noteSprite;
+ private float? minimumColumnWidth;
+
public LegacyNotePiece()
{
RelativeSizeAxes = Axes.X;
@@ -29,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
+ minimumColumnWidth = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
+
InternalChild = directionContainer = new Container
{
Origin = Anchor.BottomCentre,
@@ -47,8 +51,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
if (noteSprite.Texture != null)
{
- var scale = DrawWidth / noteSprite.Texture.DisplayWidth;
- noteSprite.Scale = new Vector2(scale);
+ // The height is scaled to the minimum column width, if provided.
+ float minimumWidth = minimumColumnWidth ?? DrawWidth;
+
+ noteSprite.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), noteSprite.Texture.DisplayWidth);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
new file mode 100644
index 0000000000..7680526ac4
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.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.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyStageBackground : LegacyManiaElement
+ {
+ private Drawable leftSprite;
+ private Drawable rightSprite;
+
+ public LegacyStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ string leftImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
+ ?? "mania-stage-left";
+
+ string rightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
+ ?? "mania-stage-right";
+
+ InternalChildren = new[]
+ {
+ leftSprite = new Sprite
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopRight,
+ X = 0.05f,
+ Texture = skin.GetTexture(leftImage),
+ },
+ rightSprite = new Sprite
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopLeft,
+ X = -0.05f,
+ Texture = skin.GetTexture(rightImage)
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (leftSprite?.Height > 0)
+ leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height);
+
+ if (rightSprite?.Height > 0)
+ rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
new file mode 100644
index 0000000000..9719005d54
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
@@ -0,0 +1,56 @@
+// 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.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyStageForeground : LegacyManiaElement
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Drawable sprite;
+
+ public LegacyStageForeground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string bottomImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
+ ?? "mania-stage-bottom";
+
+ sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
+ {
+ if (d == null)
+ return;
+
+ d.Scale = new Vector2(1.6f);
+ });
+
+ if (sprite != null)
+ InternalChild = sprite;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (sprite == null)
+ return;
+
+ if (direction.NewValue == ScrollingDirection.Up)
+ sprite.Anchor = sprite.Origin = Anchor.TopCentre;
+ else
+ sprite.Anchor = sprite.Origin = Anchor.BottomCentre;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index cbe2036343..e64178083a 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy(() => source.GetAnimation(
- source.GetConfig(
+ GetConfig(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value
?? "mania-key1", true, true) != null);
}
@@ -81,6 +81,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
case ManiaSkinComponents.HitExplosion:
return new LegacyHitExplosion();
+
+ case ManiaSkinComponents.StageBackground:
+ return new LegacyStageBackground();
+
+ case ManiaSkinComponents.StageForeground:
+ return new LegacyStageForeground();
}
break;
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index d2f58d7255..506a07f26b 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
Index = index;
RelativeSizeAxes = Axes.Y;
+ Width = COLUMN_WIDTH;
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
{
@@ -138,6 +139,6 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
- => DrawRectangle.Inflate(new Vector2(ManiaStage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
+ => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
index 982a18cb60..47cb9bd45a 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
InternalChild = directionContainer = new Container
{
RelativeSizeAxes = Axes.X,
- Height = ManiaStage.HIT_TARGET_POSITION,
+ Height = Stage.HIT_TARGET_POSITION,
Children = new[]
{
gradient = new Box
@@ -53,9 +53,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
keyIcon = new Container
{
Name = "Key icon",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
Size = new Vector2(key_icon_size),
+ Origin = Anchor.Centre,
Masking = true,
CornerRadius = key_icon_corner_radius,
BorderThickness = 2,
@@ -88,11 +87,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
if (direction.NewValue == ScrollingDirection.Up)
{
+ keyIcon.Anchor = Anchor.BottomCentre;
+ keyIcon.Y = -20;
directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft;
gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0));
}
else
{
+ keyIcon.Anchor = Anchor.TopCentre;
+ keyIcon.Y = 20;
directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft;
gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black);
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs
new file mode 100644
index 0000000000..f5b542d085
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs
@@ -0,0 +1,30 @@
+// 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 osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class DefaultStageBackground : CompositeDrawable
+ {
+ public DefaultStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new Box
+ {
+ Name = "Background",
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
index bca7c3ff08..ba5281a1a2 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
float hitPosition = CurrentSkin.GetConfig(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
- ?? ManiaStage.HIT_TARGET_POSITION;
+ ?? Stage.HIT_TARGET_POSITION;
Padding = Direction.Value == ScrollingDirection.Up
? new MarginPadding { Top = hitPosition }
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index c8c537964f..14cad39b04 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
// Mania doesn't care about global velocity
p.Velocity = 1;
+ p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 08f6049782..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,9 +15,10 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
+ [Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
- private readonly List stages = new List();
+ private readonly List stages = new List();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
@@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < stageDefinitions.Count; i++)
{
- var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
+ var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
playfieldGrid.Content[0][i] = newStage;
@@ -90,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.UI
///
public int TotalColumns => stages.Sum(s => s.Columns.Count);
- private ManiaStage getStageByColumn(int column)
+ private Stage getStageByColumn(int column)
{
int sum = 0;
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
similarity index 92%
rename from osu.Game.Rulesets.Mania/UI/ManiaStage.cs
rename to osu.Game.Rulesets.Mania/UI/Stage.cs
index adab08eb06..faa04dea97 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -25,11 +24,11 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// A collection of s.
///
- public class ManiaStage : ScrollingPlayfield
+ public class Stage : ScrollingPlayfield
{
public const float COLUMN_SPACING = 1;
- public const float HIT_TARGET_POSITION = 50;
+ public const float HIT_TARGET_POSITION = 110;
public IReadOnlyList Columns => columnFlow.Children;
private readonly FillFlowContainer columnFlow;
@@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly int firstColumnIndex;
- public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
+ public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
@@ -72,11 +71,9 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new Box
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
- Name = "Background",
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black
+ RelativeSizeAxes = Axes.Both
},
columnFlow = new FillFlowContainer
{
@@ -103,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ {
+ RelativeSizeAxes = Axes.Both
+ },
judgements = new JudgementContainer
{
Anchor = Anchor.TopCentre,
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/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
new file mode 100644
index 0000000000..90ebbd9f04
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Osu.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public abstract class OsuSkinnableTestScene : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(OsuRuleset),
+ typeof(OsuLegacySkinTransformer),
+ };
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 02d4406809..f867630df6 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -10,17 +10,16 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
- public class TestSceneDrawableJudgement : SkinnableTestScene
+ public class TestSceneDrawableJudgement : OsuSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableJudgement),
typeof(DrawableOsuJudgement)
- };
+ }).ToList();
public TestSceneDrawableJudgement()
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 7b96e2ec6a..22dacc6f5e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -3,26 +3,32 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing.Input;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneGameplayCursor : SkinnableTestScene
+ public class TestSceneGameplayCursor : OsuSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
+ typeof(GameplayCursorContainer),
typeof(OsuCursorContainer),
+ typeof(OsuCursor),
+ typeof(LegacyCursor),
+ typeof(LegacyCursorTrail),
typeof(CursorTrail)
- };
+ }).ToList();
[Cached]
private GameplayBeatmap gameplayBeatmap;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index ae5a28217c..e117729f01 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -14,12 +14,11 @@ using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneHitCircle : SkinnableTestScene
+ public class TestSceneHitCircle : OsuSkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
new file mode 100644
index 0000000000..40ee53e8f2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -0,0 +1,453 @@
+// 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 bool allJudgedFired;
+
+ 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);
+ };
+ 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);
+ }
+
+ 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/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index a201364de4..eb6130c8a6 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -22,12 +22,11 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSlider : SkinnableTestScene
+ public class TestSceneSlider : OsuSkinnableTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
index 0522260150..9fc479953e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
@@ -1,18 +1,285 @@
// 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);
+ }
+
+ 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/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
new file mode 100644
index 0000000000..f5b20fd1c5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -0,0 +1,253 @@
+// 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.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osu.Game.Storyboards;
+using osuTK;
+using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestSceneSliderSnaking : TestSceneOsuPlayer
+ {
+ [Resolved]
+ private AudioManager audioManager { get; set; }
+
+ private TrackVirtualManual track;
+
+ protected override bool Autoplay => autoplay;
+ private bool autoplay;
+
+ private readonly BindableBool snakingIn = new BindableBool();
+ private readonly BindableBool snakingOut = new BindableBool();
+
+ private const double duration_of_span = 3605;
+ private const double fade_in_modifier = -1200;
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ {
+ var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+ track = (TrackVirtualManual)working.Track;
+ return working;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(RulesetConfigCache configCache)
+ {
+ var config = (OsuRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
+ config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
+ config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
+ }
+
+ private DrawableSlider slider;
+
+ [SetUpSteps]
+ public override void SetUpSteps() { }
+
+ [TestCase(0)]
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestSnakingEnabled(int sliderIndex)
+ {
+ AddStep("enable autoplay", () => autoplay = true);
+ base.SetUpSteps();
+ AddUntilStep("wait for track to start running", () => track.IsRunning);
+
+ double startTime = hitObjects[sliderIndex].StartTime;
+ retrieveDrawableSlider(sliderIndex);
+ setSnaking(true);
+
+ ensureSnakingIn(startTime + fade_in_modifier);
+
+ for (int i = 0; i < sliderIndex; i++)
+ {
+ // non-final repeats should not snake out
+ ensureNoSnakingOut(startTime, i);
+ }
+
+ // final repeat should snake out
+ ensureSnakingOut(startTime, sliderIndex);
+ }
+
+ [TestCase(0)]
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestSnakingDisabled(int sliderIndex)
+ {
+ AddStep("have autoplay", () => autoplay = true);
+ base.SetUpSteps();
+ AddUntilStep("wait for track to start running", () => track.IsRunning);
+
+ double startTime = hitObjects[sliderIndex].StartTime;
+ retrieveDrawableSlider(sliderIndex);
+ setSnaking(false);
+
+ ensureNoSnakingIn(startTime + fade_in_modifier);
+
+ for (int i = 0; i <= sliderIndex; i++)
+ {
+ // no snaking out ever, including final repeat
+ ensureNoSnakingOut(startTime, i);
+ }
+ }
+
+ [Test]
+ public void TestRepeatArrowDoesNotMoveWhenHit()
+ {
+ AddStep("enable autoplay", () => autoplay = true);
+ setSnaking(true);
+ base.SetUpSteps();
+
+ // repeat might have a chance to update its position depending on where in the frame its hit,
+ // so some leniency is allowed here instead of checking strict equality
+ checkPositionChange(16600, sliderRepeat, positionAlmostSame);
+ }
+
+ [Test]
+ public void TestRepeatArrowMovesWhenNotHit()
+ {
+ AddStep("disable autoplay", () => autoplay = false);
+ setSnaking(true);
+ base.SetUpSteps();
+
+ checkPositionChange(16600, sliderRepeat, positionDecreased);
+ }
+
+ private void retrieveDrawableSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () =>
+ {
+ slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index);
+ });
+
+ private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
+ private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
+
+ private void ensureSnakingOut(double startTime, int repeatIndex)
+ {
+ var repeatTime = timeAtRepeat(startTime, repeatIndex);
+
+ if (repeatIndex % 2 == 0)
+ checkPositionChange(repeatTime, sliderStart, positionIncreased);
+ else
+ checkPositionChange(repeatTime, sliderEnd, positionDecreased);
+ }
+
+ private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
+ checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
+
+ private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
+ private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd;
+
+ private List sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve;
+ private Vector2 sliderStart() => sliderCurve.First();
+ private Vector2 sliderEnd() => sliderCurve.Last();
+
+ private Vector2 sliderRepeat()
+ {
+ var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1);
+ var repeat = drawable.ChildrenOfType>().First().Children.First();
+ return repeat.Position;
+ }
+
+ private bool positionRemainsSame(Vector2 previous, Vector2 current) => previous == current;
+ private bool positionIncreased(Vector2 previous, Vector2 current) => current.X > previous.X && current.Y > previous.Y;
+ private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
+ private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
+
+ private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion)
+ {
+ Vector2 previousPosition = Vector2.Zero;
+
+ string positionDescription = positionToCheck.Method.Name.Humanize(LetterCasing.LowerCase);
+ string assertionDescription = positionAssertion.Method.Name.Humanize(LetterCasing.LowerCase);
+
+ addSeekStep(startTime);
+ AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
+ addSeekStep(startTime + 100);
+ AddAssert($"{positionDescription} {assertionDescription}", () =>
+ {
+ var currentPosition = positionToCheck.Invoke();
+ return positionAssertion.Invoke(previousPosition, currentPosition);
+ });
+ }
+
+ private void setSnaking(bool value)
+ {
+ AddStep($"{(value ? "enable" : "disable")} snaking", () =>
+ {
+ snakingIn.Value = value;
+ snakingOut.Value = value;
+ });
+ }
+
+ private void addSeekStep(double time)
+ {
+ AddStep($"seek to {time}", () => track.Seek(time));
+
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ HitObjects = hitObjects
+ };
+
+ private readonly List hitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(100, 100),
+ Path = new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(300, 200)
+ }),
+ },
+ new Slider
+ {
+ StartTime = 13000,
+ Position = new Vector2(100, 100),
+ Path = new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(300, 200)
+ }),
+ RepeatCount = 1,
+ },
+ new Slider
+ {
+ StartTime = 23000,
+ Position = new Vector2(100, 100),
+ Path = new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(300, 200)
+ }),
+ RepeatCount = 2,
+ },
+ new HitCircle
+ {
+ StartTime = 199999,
+ }
+ };
+ }
+}
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..fed149b5c5 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;
@@ -33,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
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 +52,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 +144,16 @@ 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 (e.Button == MouseButton.Left)
+ {
+ changeHandler?.BeginChange();
+ return true;
+ }
+
+ return false;
+ }
protected override void OnDrag(DragEvent e)
{
@@ -158,6 +174,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 +186,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..9af972dbce 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,11 +77,7 @@ 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;
}
}
@@ -91,17 +91,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break;
case PlacementState.Body:
- switch (e.Button)
- {
- case MouseButton.Left:
- ensureCursor();
+ if (e.Button != MouseButton.Left)
+ break;
- // Detatch the cursor
- cursor = null;
- break;
+ if (canPlaceNewControlPoint(out var lastPoint))
+ {
+ // 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);
+
+ segmentStart = lastPoint;
+ segmentStart.Type.Value = PathType.Linear;
+
+ currentSegmentLength = 1;
}
- break;
+ return true;
}
return true;
@@ -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 c18b3b0ff3..b7074b7ee5 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
@@ -34,6 +35,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
+ [Resolved(CanBeNull = true)]
+ private EditorBeatmap editorBeatmap { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -88,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)
{
@@ -99,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;
@@ -162,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
- UpdateHitObject();
+ editorBeatmap?.UpdateHitObject(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]
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/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index 8bb324d02e..a981648444 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Anchor = Anchor.Centre,
Alpha = 0.5f,
}
- }, confineMode: ConfineMode.NoScaling);
+ });
}
public double AnimationStartTime { get; set; }
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 5c7f4a42b3..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 };
@@ -186,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
- Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White;
+ Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
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/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index b04d484195..720ffcd51c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- Blending = BlendingParameters.Additive;
Origin = Anchor.Centre;
InternalChild = scaleContainer = new ReverseArrowPiece();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
index 35a27bb0a6..1a5195acf8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs
@@ -8,11 +8,16 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ReverseArrowPiece : BeatSyncedContainer
{
+ [Resolved]
+ private DrawableHitObject drawableRepeat { get; set; }
+
public ReverseArrowPiece()
{
Divisor = 2;
@@ -21,13 +26,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- Blending = BlendingParameters.Additive;
-
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(0.35f)
})
@@ -37,7 +41,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
- protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) =>
- Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ if (!drawableRepeat.IsHit)
+ Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 5a6dd49c44..395c76a233 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
this.drawableSlider = drawableSlider;
this.slider = slider;
- Blending = BlendingParameters.Additive;
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
BorderThickness = 10,
BorderColour = Color4.White,
Alpha = 1,
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/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 0d67846b8e..ba0003b5cd 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
- return this.GetAnimation(component.LookupName, true, false, true);
+ return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
@@ -132,6 +132,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return SkinUtils.As(new BindableFloat(LEGACY_CIRCLE_RADIUS));
break;
+
+ case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
+ // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
+ // HitCircleOverlayAboveNumer (with typo) should still be supported for now.
+ return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
+ source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
}
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index c6920bd03e..154160fdb5 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
AllowSliderBallTint,
CursorExpand,
CursorRotate,
- HitCircleOverlayAboveNumber
+ HitCircleOverlayAboveNumber,
+ HitCircleOverlayAboveNumer // Some old skins will have this typo
}
}
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/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/skin.ini b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..462c2c278e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,5 @@
+[General]
+Name: an old skin
+Author: an old guy
+
+// no version specified means v1
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png
new file mode 100644
index 0000000000..ad55fd5a96
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png
new file mode 100644
index 0000000000..f5c02509fb
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png
new file mode 100644
index 0000000000..53905792cb
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.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/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs
new file mode 100644
index 0000000000..6db2a6907f
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public abstract class TaikoSkinnableTestScene : SkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(TaikoRuleset),
+ typeof(TaikoLegacySkinTransformer),
+ };
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new TaikoRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs
new file mode 100644
index 0000000000..301295253d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs
@@ -0,0 +1,70 @@
+// 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
+{
+ [TestFixture]
+ public class TestSceneDrawableHit : TaikoSkinnableTestScene
+ {
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
+ {
+ typeof(DrawableHit),
+ typeof(DrawableCentreHit),
+ typeof(DrawableRimHit),
+ typeof(LegacyHit),
+ }).ToList();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true))
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+
+ AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(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/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs
index c79088056f..1928e9f66f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs
@@ -3,24 +3,26 @@
using System;
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 osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
- public class TestSceneInputDrum : SkinnableTestScene
+ public class TestSceneInputDrum : TaikoSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(InputDrum),
- };
+ typeof(LegacyInputDrum),
+ }).ToList();
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
index 4979135f50..f3f4c59a62 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs
@@ -1,9 +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.Game.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -14,13 +14,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public DrawableCentreHit(Hit hit)
: base(hit)
{
- MainPiece.Add(new CentreHitSymbolPiece());
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- MainPiece.AccentColour = colours.PinkDarker;
- }
+ protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
+ _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 5806c90115..0627eb95fd 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -34,17 +34,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private Color4 colourIdle;
private Color4 colourEngaged;
+ private ElongatedCirclePiece elongatedPiece;
+
public DrawableDrumRoll(DrumRoll drumRoll)
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
- MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
+ elongatedPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- MainPiece.AccentColour = colourIdle = colours.YellowDark;
+ elongatedPiece.AccentColour = colourIdle = colours.YellowDark;
colourEngaged = colours.YellowDarker;
}
@@ -84,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
+ protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece();
public override bool OnPressed(TaikoAction action) => false;
@@ -101,7 +103,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);
+ (MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 25b6141a0e..fea3eea6a9 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
@@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool DisplayResult => false;
- protected override TaikoPiece CreateMainPiece() => new TickPiece
+ protected override CompositeDrawable CreateMainPiece() => new TickPiece
{
Filled = HitObject.FirstTick
};
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
index 5a12d71cea..463a8b746c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs
@@ -1,9 +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.Game.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -14,13 +14,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public DrawableRimHit(Hit hit)
: base(hit)
{
- MainPiece.Add(new RimHitSymbolPiece());
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- MainPiece.AccentColour = colours.BlueDarker;
- }
+ protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
+ _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index fa39819199..3a2e44038f 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -9,11 +9,11 @@ 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;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -34,8 +34,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 +105,22 @@ 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 CompositeDrawable CreateMainPiece() => new SwellCirclePiece
+ {
+ // to allow for rotation transform
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -182,7 +184,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.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..5a954addfb 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -28,5 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
public override bool OnPressed(TaikoAction action) => false;
+
+ protected override CompositeDrawable CreateMainPiece() => new TickPiece();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 5f892dd2fa..2f90f3b96c 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;
@@ -45,7 +44,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 +107,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 CompositeDrawable MainPiece;
private readonly Container strongHitContainer;
- protected DrawableTaikoHitObject(TTaikoHit hitObject)
+ protected DrawableTaikoHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
@@ -132,7 +131,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 +167,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 CompositeDrawable 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..6ca77e666d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs
@@ -10,6 +10,7 @@ using osuTK.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Effects;
+using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{
@@ -20,21 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
/// for a usage example.
///
///
- public class CirclePiece : TaikoPiece
+ public abstract class CirclePiece : BeatSyncedContainer
{
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 +45,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 +69,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/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/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
new file mode 100644
index 0000000000..80bf97936d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.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 LegacyHit : CompositeDrawable, IHasAccentColour
+ {
+ private readonly TaikoSkinComponents component;
+
+ private Drawable backgroundLayer;
+
+ public LegacyHit(TaikoSkinComponents component)
+ {
+ this.component = component;
+
+ 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;
+ }
+
+ AccentColour = component == TaikoSkinComponents.CentreHit
+ ? new Color4(235, 69, 44, 255)
+ : new Color4(67, 142, 172, 255);
+ }
+
+ 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(DrawWidth / 128);
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ if (value == accentColour)
+ return;
+
+ backgroundLayer.Colour = accentColour = value;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
index 8fe7c5e566..c61e35692b 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
@@ -18,9 +18,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
///
internal class LegacyInputDrum : Container
{
+ private LegacyHalfDrum left;
+ private LegacyHalfDrum right;
+
public LegacyInputDrum()
{
- AutoSizeAxes = Axes.Both;
+ Size = new Vector2(180, 200);
}
[BackgroundDependencyLoader]
@@ -32,25 +35,47 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
Texture = skin.GetTexture("taiko-bar-left")
},
- new LegacyHalfDrum(false)
+ left = new LegacyHalfDrum(false)
{
Name = "Left Half",
RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
RimAction = TaikoAction.LeftRim,
CentreAction = TaikoAction.LeftCentre
},
- new LegacyHalfDrum(true)
+ right = new LegacyHalfDrum(true)
{
Name = "Right Half",
- Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
+ Origin = Anchor.TopRight,
Scale = new Vector2(-1, 1),
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
};
+
+ // this will be used in the future for stable skin alignment. keeping here for reference.
+ const float taiko_bar_y = 0;
+
+ // stable things
+ 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;
+
+ if (skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
+ {
+ left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio;
+ right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
+ left.Rim.Position = new Vector2(0, taiko_bar_y) * ratio;
+ right.Rim.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
+ }
+ else
+ {
+ left.Centre.Position = new Vector2(18, taiko_bar_y + 31) * ratio;
+ right.Centre.Position = new Vector2(negativeScaleAdjust - 54, taiko_bar_y + 31) * ratio;
+ left.Rim.Position = new Vector2(8, taiko_bar_y + 23) * ratio;
+ right.Rim.Position = new Vector2(negativeScaleAdjust - 53, taiko_bar_y + 23) * ratio;
+ }
}
///
@@ -68,8 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
///
public TaikoAction CentreAction;
- private readonly Sprite rimHit;
- private readonly Sprite centreHit;
+ public readonly Sprite Rim;
+ public readonly Sprite Centre;
[Resolved]
private DrumSampleMapping sampleMappings { get; set; }
@@ -80,18 +105,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Children = new Drawable[]
{
- rimHit = new Sprite
+ Rim = new Sprite
{
- Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
- Origin = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
Scale = new Vector2(-1, 1),
+ Origin = flipped ? Anchor.TopLeft : Anchor.TopRight,
Alpha = 0,
},
- centreHit = new Sprite
+ Centre = new Sprite
{
- Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
- Origin = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Alpha = 0,
+ Origin = flipped ? Anchor.TopRight : Anchor.TopLeft,
}
};
}
@@ -99,8 +122,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- rimHit.Texture = skin.GetTexture(@"taiko-drum-outer");
- centreHit.Texture = skin.GetTexture(@"taiko-drum-inner");
+ Rim.Texture = skin.GetTexture(@"taiko-drum-outer");
+ Centre.Texture = skin.GetTexture(@"taiko-drum-inner");
}
public bool OnPressed(TaikoAction action)
@@ -110,12 +133,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
if (action == CentreAction)
{
- target = centreHit;
+ target = Centre;
drumSample.Centre?.Play();
}
else if (action == RimAction)
{
- target = rimHit;
+ target = Rim;
drumSample.Rim?.Play();
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 78eec94590..9cd625c35f 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -32,6 +32,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return new LegacyInputDrum();
return null;
+
+ case TaikoSkinComponents.CentreHit:
+ case TaikoSkinComponents.RimHit:
+
+ if (GetTexture("taikohitcircle") != null)
+ return new LegacyHit(taikoComponent.Component);
+
+ return null;
}
return source.GetDrawableComponent(component);
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 6d4581db80..babf21b6a9 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -6,5 +6,7 @@ namespace osu.Game.Rulesets.Taiko
public enum TaikoSkinComponents
{
InputDrum,
+ CentreHit,
+ RimHit
}
}
diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
similarity index 58%
rename from osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
rename to osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
index 12d729d09f..2d4587341d 100644
--- a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs
@@ -1,18 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
- [TestFixture]
- public class EditorBeatmapTest
+ [HeadlessTest]
+ public class TestSceneEditorBeatmap : EditorClockTestScene
{
///
/// Tests that the addition event is correctly invoked after a hitobject is added.
@@ -55,13 +58,19 @@ namespace osu.Game.Tests.Beatmaps
public void TestInitialHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
- editorBeatmap.StartTimeChanged += h => changedObject = h;
- hitCircle.StartTime = 1000;
- Assert.That(changedObject, Is.EqualTo(hitCircle));
+ AddStep("add beatmap", () =>
+ {
+ EditorBeatmap editorBeatmap;
+
+ Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
+ editorBeatmap.HitObjectUpdated += h => changedObject = h;
+ });
+
+ AddStep("change start time", () => hitCircle.StartTime = 1000);
+ AddAssert("received change event", () => changedObject == hitCircle);
}
///
@@ -71,18 +80,22 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestAddedHitObjectStartTimeChangeEvent()
{
- var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
-
+ EditorBeatmap editorBeatmap = null;
HitObject changedObject = null;
- editorBeatmap.StartTimeChanged += h => changedObject = h;
+
+ AddStep("add beatmap", () =>
+ {
+ Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ editorBeatmap.HitObjectUpdated += h => changedObject = h;
+ });
var hitCircle = new HitCircle();
- editorBeatmap.Add(hitCircle);
- Assert.That(changedObject, Is.Null);
+ AddStep("add object", () => editorBeatmap.Add(hitCircle));
+ AddAssert("event not received", () => changedObject == null);
- hitCircle.StartTime = 1000;
- Assert.That(changedObject, Is.EqualTo(hitCircle));
+ AddStep("change start time", () => hitCircle.StartTime = 1000);
+ AddAssert("event received", () => changedObject == hitCircle);
}
///
@@ -95,7 +108,7 @@ namespace osu.Game.Tests.Beatmaps
var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
- editorBeatmap.StartTimeChanged += h => changedObject = h;
+ editorBeatmap.HitObjectUpdated += h => changedObject = h;
editorBeatmap.Remove(hitCircle);
Assert.That(changedObject, Is.Null);
@@ -150,5 +163,69 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(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/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
new file mode 100644
index 0000000000..9613f250c4
--- /dev/null
+++ b/osu.Game.Tests/Editor/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.Editor
+{
+ [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/Editor/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs
new file mode 100644
index 0000000000..c24418d688
--- /dev/null
+++ b/osu.Game.Tests/Editor/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.Editor
+{
+ [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/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/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
new file mode 100644
index 0000000000..867af9c1b8
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -0,0 +1,109 @@
+// 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.Graphics.Textures;
+using osu.Game.Skinning;
+
+namespace osu.Game.Tests.NonVisual.Skinning
+{
+ [TestFixture]
+ public sealed class LegacySkinTextureFallbackTest
+ {
+ private static object[][] fallbackTestCases =
+ {
+ new object[]
+ {
+ // textures in store
+ new[] { "Gameplay/osu/followpoint@2x", "Gameplay/osu/followpoint" },
+ // requested component
+ "Gameplay/osu/followpoint",
+ // returned texture name & scale
+ "Gameplay/osu/followpoint@2x", 2
+ },
+ new object[]
+ {
+ new[] { "Gameplay/osu/followpoint@2x" },
+ "Gameplay/osu/followpoint",
+ "Gameplay/osu/followpoint@2x", 2
+ },
+ new object[]
+ {
+ new[] { "Gameplay/osu/followpoint" },
+ "Gameplay/osu/followpoint",
+ "Gameplay/osu/followpoint", 1
+ },
+ new object[]
+ {
+ new[] { "Gameplay/osu/followpoint", "followpoint@2x" },
+ "Gameplay/osu/followpoint",
+ "Gameplay/osu/followpoint", 1
+ },
+ new object[]
+ {
+ new[] { "followpoint@2x", "followpoint" },
+ "Gameplay/osu/followpoint",
+ "followpoint@2x", 2
+ },
+ new object[]
+ {
+ new[] { "followpoint@2x" },
+ "Gameplay/osu/followpoint",
+ "followpoint@2x", 2
+ },
+ new object[]
+ {
+ new[] { "followpoint" },
+ "Gameplay/osu/followpoint",
+ "followpoint", 1
+ },
+ };
+
+ [TestCaseSource(nameof(fallbackTestCases))]
+ public void TestFallbackOrder(string[] filesInStore, string requestedComponent, string expectedTexture, float expectedScale)
+ {
+ var textureStore = new TestTextureStore(filesInStore);
+ var legacySkin = new TestLegacySkin(textureStore);
+
+ var texture = legacySkin.GetTexture(requestedComponent);
+
+ Assert.IsNotNull(texture);
+ Assert.AreEqual(textureStore.Textures[expectedTexture], texture);
+ Assert.AreEqual(expectedScale, texture.ScaleAdjust);
+ }
+
+ [Test]
+ public void TestReturnNullOnFallbackFailure()
+ {
+ var textureStore = new TestTextureStore("sliderb", "hit100");
+ var legacySkin = new TestLegacySkin(textureStore);
+
+ var texture = legacySkin.GetTexture("Gameplay/osu/followpoint");
+
+ Assert.IsNull(texture);
+ }
+
+ private class TestLegacySkin : LegacySkin
+ {
+ public TestLegacySkin(TextureStore textureStore)
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ Textures = textureStore;
+ }
+ }
+
+ private class TestTextureStore : TextureStore
+ {
+ public readonly Dictionary Textures;
+
+ public TestTextureStore(params string[] fileNames)
+ {
+ Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1));
+ }
+
+ public override Texture Get(string name) => Textures.GetValueOrDefault(name);
+ }
+ }
+}
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/mania-skin-zero-minwidth.ini b/osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini
new file mode 100644
index 0000000000..fd22e2e299
--- /dev/null
+++ b/osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini
@@ -0,0 +1,4 @@
+[Mania]
+Keys: 4
+ColumnWidth: 10,10,10,10
+WidthForNoteHeightScale: 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/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs
index 83fd4878aa..e811979aed 100644
--- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs
@@ -99,5 +99,20 @@ namespace osu.Game.Tests.Skins
Assert.That(configs[0].CustomColours, Contains.Key("ColourBarline").And.ContainValue(new Color4(50, 50, 50, 50)));
}
}
+
+ [Test]
+ public void TestMinimumColumnWidthFallsBackWhenZeroIsProvided()
+ {
+ var decoder = new LegacyManiaSkinDecoder();
+
+ using (var resStream = TestResources.OpenResource("mania-skin-zero-minwidth.ini"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var configs = decoder.Decode(stream);
+
+ Assert.That(configs.Count, Is.EqualTo(1));
+ Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16));
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index cef38bbbb8..aedf26ee75 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-empty.ini"))
using (var stream = new LineBufferedReader(resStream))
- Assert.IsNull(decoder.Decode(stream).LegacyVersion);
+ Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
}
}
}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 35313ee858..685decf097 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -12,7 +13,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
+using osu.Game.IO;
+using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
@@ -22,15 +26,15 @@ namespace osu.Game.Tests.Skins
[HeadlessTest]
public class TestSceneSkinConfigurationLookup : OsuTestScene
{
- private SkinSource source1;
- private SkinSource source2;
+ private UserSkinSource userSource;
+ private BeatmapSkinSource beatmapSource;
private SkinRequester requester;
[SetUp]
public void SetUp() => Schedule(() =>
{
- Add(new SkinProvidingContainer(source1 = new SkinSource())
- .WithChild(new SkinProvidingContainer(source2 = new SkinSource())
+ Add(new SkinProvidingContainer(userSource = new UserSkinSource())
+ .WithChild(new SkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
.WithChild(requester = new SkinRequester())));
});
@@ -39,31 +43,31 @@ namespace osu.Game.Tests.Skins
{
AddStep("Add config values", () =>
{
- source1.Configuration.ConfigDictionary["Lookup"] = "source1";
- source2.Configuration.ConfigDictionary["Lookup"] = "source2";
+ userSource.Configuration.ConfigDictionary["Lookup"] = "user skin";
+ beatmapSource.Configuration.ConfigDictionary["Lookup"] = "beatmap skin";
});
- AddAssert("Check lookup finds source2", () => requester.GetConfig("Lookup")?.Value == "source2");
+ AddAssert("Check lookup finds beatmap skin", () => requester.GetConfig("Lookup")?.Value == "beatmap skin");
}
[Test]
public void TestFloatLookup()
{
- AddStep("Add config values", () => source1.Configuration.ConfigDictionary["FloatTest"] = "1.1");
+ AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["FloatTest"] = "1.1");
AddAssert("Check float parse lookup", () => requester.GetConfig("FloatTest")?.Value == 1.1f);
}
[Test]
public void TestBoolLookup()
{
- AddStep("Add config values", () => source1.Configuration.ConfigDictionary["BoolTest"] = "1");
+ AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1");
AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == true);
}
[Test]
public void TestEnumLookup()
{
- AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Test"] = "Test2");
+ AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["Test"] = "Test2");
AddAssert("Check enum parse lookup", () => requester.GetConfig(LookupType.Test)?.Value == ValueType.Test2);
}
@@ -76,7 +80,7 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestLookupNull()
{
- AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Lookup"] = null);
+ AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["Lookup"] = null);
AddAssert("Check lookup null", () =>
{
@@ -88,7 +92,7 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestColourLookup()
{
- AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red);
+ AddStep("Add config colour", () => userSource.Configuration.CustomColours["Lookup"] = Color4.Red);
AddAssert("Check colour lookup", () => requester.GetConfig(new SkinCustomColourLookup("Lookup"))?.Value == Color4.Red);
}
@@ -101,7 +105,7 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestWrongColourType()
{
- AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red);
+ AddStep("Add config colour", () => userSource.Configuration.CustomColours["Lookup"] = Color4.Red);
AddAssert("perform incorrect lookup", () =>
{
@@ -127,26 +131,51 @@ namespace osu.Game.Tests.Skins
[Test]
public void TestEmptyComboColoursNoFallback()
{
- AddStep("Add custom combo colours to source1", () => source1.Configuration.AddComboColours(
+ AddStep("Add custom combo colours to user skin", () => userSource.Configuration.AddComboColours(
new Color4(100, 150, 200, 255),
new Color4(55, 110, 166, 255),
new Color4(75, 125, 175, 255)
));
- AddStep("Disallow default colours fallback in source2", () => source2.Configuration.AllowDefaultComboColoursFallback = false);
+ AddStep("Disallow default colours fallback in beatmap skin", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false);
- AddAssert("Check retrieved combo colours from source1", () =>
- requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(source1.Configuration.ComboColours) ?? false);
+ AddAssert("Check retrieved combo colours from user skin", () =>
+ requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(userSource.Configuration.ComboColours) ?? false);
}
[Test]
- public void TestLegacyVersionLookup()
+ public void TestNullBeatmapVersionFallsBackToUserSkin()
{
- AddStep("Set source1 version 2.3", () => source1.Configuration.LegacyVersion = 2.3m);
- AddStep("Set source2 version null", () => source2.Configuration.LegacyVersion = null);
+ AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
+ AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m);
}
+ [Test]
+ public void TestSetBeatmapVersionNoFallback()
+ {
+ AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m);
+ AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m);
+ AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m);
+ }
+
+ [Test]
+ public void TestNullBeatmapAndUserVersionFallsBackToLatest()
+ {
+ AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = null);
+ AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
+ AddAssert("Check legacy version lookup",
+ () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == LegacySkinConfiguration.LATEST_VERSION);
+ }
+
+ [Test]
+ public void TestIniWithNoVersionFallsBackTo1()
+ {
+ AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream())));
+ AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null);
+ AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m);
+ }
+
public enum LookupType
{
Test
@@ -159,14 +188,22 @@ namespace osu.Game.Tests.Skins
Test3
}
- public class SkinSource : LegacySkin
+ public class UserSkinSource : LegacySkin
{
- public SkinSource()
+ public UserSkinSource()
: base(new SkinInfo(), null, null, string.Empty)
{
}
}
+ public class BeatmapSkinSource : LegacyBeatmapSkin
+ {
+ public BeatmapSkinSource()
+ : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
+ {
+ }
+ }
+
public class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs
new file mode 100644
index 0000000000..dd1b6cf6aa
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs
@@ -0,0 +1,195 @@
+// 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.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+ public class TestSceneEditorChangeStates : ScreenTestScene
+ {
+ private EditorBeatmap editorBeatmap;
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ Screens.Edit.Editor editor = null;
+
+ AddStep("load editor", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ LoadScreen(editor = new Screens.Edit.Editor());
+ });
+
+ AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
+ && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ 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("press undo", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.PressKey(Key.Z);
+ });
+
+ AddStep("release keys", () =>
+ {
+ InputManager.ReleaseKey(Key.LControl);
+ InputManager.ReleaseKey(Key.Z);
+ });
+ }
+
+ private void addRedoSteps()
+ {
+ AddStep("press redo", () =>
+ {
+ InputManager.PressKey(Key.LControl);
+ InputManager.PressKey(Key.LShift);
+ InputManager.PressKey(Key.Z);
+ });
+
+ AddStep("release keys", () =>
+ {
+ InputManager.ReleaseKey(Key.LControl);
+ InputManager.ReleaseKey(Key.LShift);
+ InputManager.ReleaseKey(Key.Z);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
index ff25e609c1..91d6f2f143 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
@@ -26,16 +26,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly IReadOnlyList testBreaks = new List
{
- new BreakPeriod
- {
- StartTime = 1000,
- EndTime = 5000,
- },
- new BreakPeriod
- {
- StartTime = 6000,
- EndTime = 13500,
- },
+ new BreakPeriod(1000, 5000),
+ new BreakPeriod(6000, 13500),
};
public TestSceneBreakTracker()
@@ -70,7 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestNoEffectsBreak()
{
- var shortBreak = new BreakPeriod { EndTime = 500 };
+ var shortBreak = new BreakPeriod(0, 500);
setClock(true);
loadBreaksStep("short break", new[] { shortBreak });
@@ -127,13 +119,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addShowBreakStep(double seconds)
{
- AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = breakTracker.Breaks = new List
+ AddStep($"show '{seconds}s' break", () =>
{
- new BreakPeriod
+ breakOverlay.Breaks = breakTracker.Breaks = new List
{
- StartTime = Clock.CurrentTime,
- EndTime = Clock.CurrentTime + seconds * 1000,
- }
+ new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000)
+ };
});
}
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/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/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..f612992bf6 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
@@ -149,8 +149,8 @@ namespace osu.Game.Tests.Visual.Online
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/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..8793d880e3 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
typeof(OnScreenDisplay),
typeof(NotificationOverlay),
typeof(DirectOverlay),
- typeof(SocialOverlay),
+ typeof(DashboardOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 03a19b6690..2294cd6966 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
- Position = new Vector2(0, 25),
+ Position = new Vector2(-5, 25),
Current = { BindTarget = modSelect.SelectedMods }
}
};
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/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
index 1cd68d1fdd..c81ec9f663 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
-using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
@@ -100,21 +99,21 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader
{
- protected override ScreenTitle CreateTitle() => new TestTitle();
+ protected override OverlayTitle CreateTitle() => new TestTitle();
}
private class TestNoControlHeader : OverlayHeader
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/changelog");
- protected override ScreenTitle CreateTitle() => new TestTitle();
+ protected override OverlayTitle CreateTitle() => new TestTitle();
}
private class TestStringTabControlHeader : TabControlOverlayHeader
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news");
- protected override ScreenTitle CreateTitle() => new TestTitle();
+ protected override OverlayTitle CreateTitle() => new TestTitle();
protected override Drawable CreateTitleContent() => new OverlayRulesetSelector();
@@ -129,7 +128,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
- protected override ScreenTitle CreateTitle() => new TestTitle();
+ protected override OverlayTitle CreateTitle() => new TestTitle();
}
private enum TestEnum
@@ -141,7 +140,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestBreadcrumbControlHeader : BreadcrumbControlOverlayHeader
{
- protected override ScreenTitle CreateTitle() => new TestTitle();
+ protected override OverlayTitle CreateTitle() => new TestTitle();
public TestBreadcrumbControlHeader()
{
@@ -151,15 +150,13 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
- private class TestTitle : ScreenTitle
+ private class TestTitle : OverlayTitle
{
public TestTitle()
{
Title = "title";
- Section = "section";
+ IconTexture = "Icons/changelog";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}
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.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
index bdaa1ae7fd..fa03518c47 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
+
protected override void OnDrag(DragEvent e)
{
this.MoveTo(target += e.Delta, 1000, Easing.OutQuint);
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..d33a922a32 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -247,7 +247,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 f5b27eddd2..33bb9774df 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -305,12 +305,9 @@ namespace osu.Game.Beatmaps.Formats
case LegacyEventType.Break:
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
+ double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
- var breakEvent = new BreakPeriod
- {
- StartTime = start,
- EndTime = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])))
- };
+ var breakEvent = new BreakPeriod(start, end);
if (!breakEvent.HasEffect)
return;
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index ec2ca30535..fe63eec3f9 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps.Formats
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}"));
+ writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1}"));
}
private void handleDifficulty(TextWriter writer)
@@ -187,27 +187,13 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
+ // TODO: implement other legacy rulesets
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);
- break;
-
- case 2:
- foreach (var h in beatmap.HitObjects)
- handleCatchHitObject(writer, h);
- break;
-
- case 3:
- foreach (var h in beatmap.HitObjects)
- handleManiaHitObject(writer, h);
- break;
}
}
@@ -328,12 +314,6 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
- private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
- private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
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/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs
index 5d79c7a86b..bb8ae4a66a 100644
--- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs
+++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs
@@ -32,6 +32,17 @@ namespace osu.Game.Beatmaps.Timing
///
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
+ ///
+ /// Constructs a new break period.
+ ///
+ /// The start time of the break period.
+ /// The end time of the break period.
+ public BreakPeriod(double startTime, double endTime)
+ {
+ StartTime = startTime;
+ EndTime = endTime;
+ }
+
///
/// Whether this break contains a specified time.
///
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/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
index ca9f1330f9..c2feca171b 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
@@ -113,13 +113,13 @@ namespace osu.Game.Graphics.UserInterface
private const float transition_length = 500;
- private void fadeActive()
+ protected void FadeHovered()
{
Bar.FadeIn(transition_length, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
}
- private void fadeInactive()
+ protected void FadeUnhovered()
{
Bar.FadeOut(transition_length, Easing.OutQuint);
Text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
@@ -128,14 +128,14 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
if (!Active.Value)
- fadeActive();
+ FadeHovered();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Active.Value)
- fadeInactive();
+ FadeUnhovered();
}
[BackgroundDependencyLoader]
@@ -172,13 +172,19 @@ namespace osu.Game.Graphics.UserInterface
},
new HoverClickSounds()
};
-
- Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
- protected override void OnActivated() => fadeActive();
+ protected override void OnActivated()
+ {
+ Text.Font = Text.Font.With(weight: FontWeight.Bold);
+ FadeHovered();
+ }
- protected override void OnDeactivated() => fadeInactive();
+ protected override void OnDeactivated()
+ {
+ Text.Font = Text.Font.With(weight: FontWeight.Medium);
+ FadeUnhovered();
+ }
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs
deleted file mode 100644
index ecd0508258..0000000000
--- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics.Sprites;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Graphics.UserInterface
-{
- public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour
- {
- public const float ICON_WIDTH = ICON_SIZE + spacing;
-
- public const float ICON_SIZE = 25;
- private const float spacing = 6;
- private const int text_offset = 2;
-
- private SpriteIcon iconSprite;
- private readonly OsuSpriteText titleText, pageText;
-
- protected IconUsage Icon
- {
- set
- {
- if (iconSprite == null)
- throw new InvalidOperationException($"Cannot use {nameof(Icon)} with a custom {nameof(CreateIcon)} function.");
-
- iconSprite.Icon = value;
- }
- }
-
- protected string Title
- {
- set => titleText.Text = value;
- }
-
- protected string Section
- {
- set => pageText.Text = value;
- }
-
- public Color4 AccentColour
- {
- get => pageText.Colour;
- set => pageText.Colour = value;
- }
-
- protected virtual Drawable CreateIcon() => iconSprite = new SpriteIcon
- {
- Size = new Vector2(ICON_SIZE),
- };
-
- protected ScreenTitle()
- {
- AutoSizeAxes = Axes.Both;
-
- InternalChildren = new Drawable[]
- {
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(spacing, 0),
- Direction = FillDirection.Horizontal,
- Children = new[]
- {
- CreateIcon().With(t =>
- {
- t.Anchor = Anchor.Centre;
- t.Origin = Anchor.Centre;
- }),
- titleText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
- Margin = new MarginPadding { Bottom = text_offset }
- },
- new Circle
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(4),
- Colour = Color4.Gray,
- },
- pageText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 20),
- Margin = new MarginPadding { Bottom = text_offset }
- }
- }
- },
- };
- }
- }
-}
diff --git a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs
deleted file mode 100644
index c2a13970de..0000000000
--- a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs
+++ /dev/null
@@ -1,40 +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.Sprites;
-using osu.Framework.Graphics.Textures;
-using osuTK;
-
-namespace osu.Game.Graphics.UserInterface
-{
- ///
- /// A custom icon class for use with based off a texture resource.
- ///
- public class ScreenTitleTextureIcon : CompositeDrawable
- {
- private readonly string textureName;
-
- public ScreenTitleTextureIcon(string textureName)
- {
- this.textureName = textureName;
- }
-
- [BackgroundDependencyLoader]
- private void load(TextureStore textures)
- {
- Size = new Vector2(ScreenTitle.ICON_SIZE);
-
- InternalChild = new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Texture = textures.Get(textureName),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- FillMode = FillMode.Fit
- };
- }
- }
-}
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/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/Leaderboards/UpdateableRank.cs b/osu.Game/Online/Leaderboards/UpdateableRank.cs
index d9e8957281..8f74fd84fe 100644
--- a/osu.Game/Online/Leaderboards/UpdateableRank.cs
+++ b/osu.Game/Online/Leaderboards/UpdateableRank.cs
@@ -7,23 +7,31 @@ using osu.Game.Scoring;
namespace osu.Game.Online.Leaderboards
{
- public class UpdateableRank : ModelBackedDrawable
+ public class UpdateableRank : ModelBackedDrawable
{
- public ScoreRank Rank
+ public ScoreRank? Rank
{
get => Model;
set => Model = value;
}
- public UpdateableRank(ScoreRank rank)
+ public UpdateableRank(ScoreRank? rank)
{
Rank = rank;
}
- protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank)
+ protected override Drawable CreateDrawable(ScoreRank? rank)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- };
+ if (rank.HasValue)
+ {
+ return new DrawableRank(rank.Value)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+ }
+
+ return null;
+ }
}
}
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..c861b84835 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -67,7 +67,7 @@ namespace osu.Game
private DirectOverlay direct;
- 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);
@@ -604,7 +611,7 @@ namespace osu.Game
//overlay elements
loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(social = new SocialOverlay(), 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, direct, 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:
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
index 5af92914de..1bab200fec 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
@@ -1,24 +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.Graphics;
-using osu.Game.Graphics.UserInterface;
-
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapListingHeader : OverlayHeader
{
- protected override ScreenTitle CreateTitle() => new BeatmapListingTitle();
+ protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
- private class BeatmapListingTitle : ScreenTitle
+ private class BeatmapListingTitle : OverlayTitle
{
public BeatmapListingTitle()
{
- Title = @"beatmap";
- Section = @"listing";
+ Title = "beatmap listing";
+ IconTexture = "Icons/changelog";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 5bac5a5402..b450f33ee1 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
- new BasicScrollContainer
+ new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
index e5e3e276d5..4626589d81 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs
@@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapSet
@@ -14,22 +13,20 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapRulesetSelector RulesetSelector { get; private set; }
- protected override ScreenTitle CreateTitle() => new BeatmapHeaderTitle();
+ protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle();
protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector
{
Current = Ruleset
};
- private class BeatmapHeaderTitle : ScreenTitle
+ private class BeatmapHeaderTitle : OverlayTitle
{
public BeatmapHeaderTitle()
{
- Title = @"beatmap";
- Section = @"info";
+ Title = "beatmap info";
+ IconTexture = "Icons/changelog";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs
index 29c259b7f8..11dc424183 100644
--- a/osu.Game/Overlays/BeatmapSet/Header.cs
+++ b/osu.Game/Overlays/BeatmapSet/Header.cs
@@ -277,7 +277,8 @@ namespace osu.Game.Overlays.BeatmapSet
downloadButtonsContainer.Child = new PanelDownloadButton(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/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs
index 1d8411dfcc..81315f9638 100644
--- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs
+++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
@@ -16,6 +17,13 @@ namespace osu.Game.Overlays
public OverlayHeaderBreadcrumbControl()
{
RelativeSizeAxes = Axes.X;
+ Height = 47;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ AccentColour = colourProvider.Light2;
}
protected override TabItem CreateTabItem(string value) => new ControlTabItem(value);
@@ -27,10 +35,18 @@ namespace osu.Game.Overlays
public ControlTabItem(string value)
: base(value)
{
+ RelativeSizeAxes = Axes.Y;
Text.Font = Text.Font.With(size: 14);
- Chevron.Y = 3;
+ Text.Anchor = Anchor.CentreLeft;
+ Text.Origin = Anchor.CentreLeft;
+ Chevron.Y = 1;
Bar.Height = 0;
}
+
+ // base OsuTabItem makes font bold on activation, we don't want that here
+ protected override void OnActivated() => FadeHovered();
+
+ protected override void OnDeactivated() => FadeUnhovered();
}
}
}
diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
index 532efeb4bd..050bdea03a 100644
--- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs
@@ -9,7 +9,6 @@ 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.Requests.Responses;
namespace osu.Game.Overlays.Changelog
@@ -50,8 +49,6 @@ namespace osu.Game.Overlays.Changelog
streamsBackground.Colour = colourProvider.Background5;
}
- private ChangelogHeaderTitle title;
-
private void showBuild(ValueChangedEvent e)
{
if (e.OldValue != null)
@@ -63,14 +60,11 @@ namespace osu.Game.Overlays.Changelog
Current.Value = e.NewValue.ToString();
updateCurrentStream();
-
- title.Version = e.NewValue.UpdateStream.DisplayName;
}
else
{
Current.Value = listing_string;
Streams.Current.Value = null;
- title.Version = null;
}
}
@@ -100,7 +94,7 @@ namespace osu.Game.Overlays.Changelog
}
};
- protected override ScreenTitle CreateTitle() => title = new ChangelogHeaderTitle();
+ protected override OverlayTitle CreateTitle() => new ChangelogHeaderTitle();
public void Populate(List streams)
{
@@ -116,20 +110,13 @@ namespace osu.Game.Overlays.Changelog
Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name);
}
- private class ChangelogHeaderTitle : ScreenTitle
+ private class ChangelogHeaderTitle : OverlayTitle
{
- public string Version
- {
- set => Section = value ?? listing_string;
- }
-
public ChangelogHeaderTitle()
{
Title = "changelog";
- Version = null;
+ IconTexture = "Icons/changelog";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}
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/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/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
index 1b3657f010..387ced6acb 100644
--- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs
+++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using 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;
@@ -14,16 +17,18 @@ namespace osu.Game.Overlays.Direct
{
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 PanelDownloadButton(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/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/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs
index b55e3ffba0..8214c71b3a 100644
--- a/osu.Game/Overlays/News/NewsHeader.cs
+++ b/osu.Game/Overlays/News/NewsHeader.cs
@@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
using System;
namespace osu.Game.Overlays.News
@@ -12,8 +11,6 @@ namespace osu.Game.Overlays.News
{
private const string front_page_string = "frontpage";
- private NewsHeaderTitle title;
-
public readonly Bindable Post = new Bindable(null);
public Action ShowFrontPage;
@@ -40,36 +37,24 @@ namespace osu.Game.Overlays.News
{
TabControl.AddItem(e.NewValue);
Current.Value = e.NewValue;
-
- title.IsReadingPost = true;
}
else
{
Current.Value = front_page_string;
- title.IsReadingPost = false;
}
}
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news");
- protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle();
+ protected override OverlayTitle CreateTitle() => new NewsHeaderTitle();
- private class NewsHeaderTitle : ScreenTitle
+ private class NewsHeaderTitle : OverlayTitle
{
- private const string post_string = "post";
-
- public bool IsReadingPost
- {
- set => Section = value ? post_string : front_page_string;
- }
-
public NewsHeaderTitle()
{
Title = "news";
- IsReadingPost = false;
+ IconTexture = "Icons/news";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news");
}
}
}
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 bedf8e5435..dbc934bde9 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -6,15 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
+ public const int CONTENT_X_MARGIN = 50;
+
private readonly Box titleBackground;
- private readonly ScreenTitle title;
protected readonly FillFlowContainer HeaderInfo;
@@ -56,12 +56,11 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
- Vertical = 10,
+ Horizontal = CONTENT_X_MARGIN,
},
Children = new[]
{
- title = CreateTitle().With(title =>
+ CreateTitle().With(title =>
{
title.Anchor = Anchor.CentreLeft;
title.Origin = Anchor.CentreLeft;
@@ -86,7 +85,6 @@ namespace osu.Game.Overlays
private void load(OverlayColourProvider colourProvider)
{
titleBackground.Colour = colourProvider.Dark5;
- title.AccentColour = colourProvider.Highlight1;
}
[NotNull]
@@ -96,11 +94,11 @@ namespace osu.Game.Overlays
protected virtual Drawable CreateBackground() => Empty();
///
- /// Creates a on the opposite side of the . Used mostly to create .
+ /// Creates a on the opposite side of the . Used mostly to create .
///
[NotNull]
protected virtual Drawable CreateTitleContent() => Empty();
- protected abstract ScreenTitle CreateTitle();
+ protected abstract OverlayTitle CreateTitle();
}
}
diff --git a/osu.Game/Overlays/OverlayRulesetSelector.cs b/osu.Game/Overlays/OverlayRulesetSelector.cs
index b73d38eeb3..8c44157f78 100644
--- a/osu.Game/Overlays/OverlayRulesetSelector.cs
+++ b/osu.Game/Overlays/OverlayRulesetSelector.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(25, 0),
+ Spacing = new Vector2(20, 0),
};
}
}
diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs
index 9b4dd5ba1e..9d4afc94d1 100644
--- a/osu.Game/Overlays/OverlayRulesetTabItem.cs
+++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets;
using osuTK.Graphics;
using osuTK;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Overlays
{
@@ -53,6 +54,8 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = value.Name,
+ Font = OsuFont.GetFont(size: 14),
+ ShadowColour = Color4.Black.Opacity(0.75f)
}
},
new HoverClickSounds()
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/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs
index aa96f0e19b..a1cbf2c1e7 100644
--- a/osu.Game/Overlays/OverlayTabControl.cs
+++ b/osu.Game/Overlays/OverlayTabControl.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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
@@ -35,17 +36,22 @@ namespace osu.Game.Overlays
protected OverlayTabControl()
{
TabContainer.Masking = false;
- TabContainer.Spacing = new Vector2(15, 0);
+ TabContainer.Spacing = new Vector2(20, 0);
AddInternal(bar = new Box
{
RelativeSizeAxes = Axes.X,
- Height = 2,
Anchor = Anchor.BottomLeft,
- Origin = Anchor.CentreLeft
+ Origin = Anchor.BottomLeft
});
}
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ AccentColour = colourProvider.Highlight1;
+ }
+
protected override Dropdown CreateDropdown() => null;
protected override TabItem CreateTabItem(T value) => new OverlayTabItem(value);
@@ -90,7 +96,7 @@ namespace osu.Game.Overlays
Bar = new ExpandingBar
{
Anchor = Anchor.BottomCentre,
- ExpandedSize = 7.5f,
+ ExpandedSize = 5f,
CollapsedSize = 0
},
new HoverClickSounds()
@@ -119,6 +125,7 @@ namespace osu.Game.Overlays
{
HoverAction();
Text.Font = Text.Font.With(weight: FontWeight.Bold);
+ Text.FadeColour(Color4.White, 120, Easing.InQuad);
}
protected override void OnDeactivated()
@@ -135,11 +142,7 @@ namespace osu.Game.Overlays
OnDeactivated();
}
- protected virtual void HoverAction()
- {
- Bar.Expand();
- Text.FadeColour(Color4.White, 120, Easing.InQuad);
- }
+ protected virtual void HoverAction() => Bar.Expand();
protected virtual void UnhoverAction()
{
diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs
new file mode 100644
index 0000000000..1c9567428c
--- /dev/null
+++ b/osu.Game/Overlays/OverlayTitle.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays
+{
+ public abstract class OverlayTitle : CompositeDrawable
+ {
+ private readonly OsuSpriteText title;
+ private readonly Container icon;
+
+ protected string Title
+ {
+ set => title.Text = value;
+ }
+
+ protected string IconTexture
+ {
+ set => icon.Child = new OverlayTitleIcon(value);
+ }
+
+ protected OverlayTitle()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(10, 0),
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ icon = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side
+ Size = new Vector2(30)
+ },
+ title = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular),
+ Margin = new MarginPadding { Vertical = 17.5f } // 15px padding + 2.5px line-height difference compensation
+ }
+ }
+ };
+ }
+
+ private class OverlayTitleIcon : Sprite
+ {
+ private readonly string textureName;
+
+ public OverlayTitleIcon(string textureName)
+ {
+ this.textureName = textureName;
+
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ FillMode = FillMode.Fit;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Texture = textures.Get(textureName);
+ }
+ }
+ }
+}
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/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index f7c09e33c1..0161d91daa 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Profile.Header;
using osu.Game.Users;
@@ -87,19 +86,17 @@ namespace osu.Game.Overlays.Profile
}
};
- protected override ScreenTitle CreateTitle() => new ProfileHeaderTitle();
+ protected override OverlayTitle CreateTitle() => new ProfileHeaderTitle();
private void updateDisplay(User user) => coverContainer.User = user;
- private class ProfileHeaderTitle : ScreenTitle
+ private class ProfileHeaderTitle : OverlayTitle
{
public ProfileHeaderTitle()
{
- Title = "player";
- Section = "info";
+ Title = "player info";
+ IconTexture = "Icons/profile";
}
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/profile");
}
}
}
diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
index 99325aa1da..e30c6f07a8 100644
--- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
+++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
@@ -3,7 +3,6 @@
using osu.Framework.Graphics;
using osu.Framework.Bindables;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Users;
@@ -18,33 +17,21 @@ namespace osu.Game.Overlays.Rankings
private OverlayRulesetSelector rulesetSelector;
private CountryFilter countryFilter;
- protected override ScreenTitle CreateTitle() => new RankingsTitle
- {
- Scope = { BindTarget = Current }
- };
+ protected override OverlayTitle CreateTitle() => new RankingsTitle();
protected override Drawable CreateTitleContent() => rulesetSelector = new OverlayRulesetSelector();
protected override Drawable CreateContent() => countryFilter = new CountryFilter();
- protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
+ protected override Drawable CreateBackground() => new OverlayHeaderBackground("Headers/rankings");
- private class RankingsTitle : ScreenTitle
+ private class RankingsTitle : OverlayTitle
{
- public readonly Bindable Scope = new Bindable();
-
public RankingsTitle()
{
Title = "ranking";
+ IconTexture = "Icons/rankings";
}
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- Scope.BindValueChanged(scope => Section = scope.NewValue.ToString().ToLowerInvariant(), true);
- }
-
- protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/rankings");
}
}
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/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/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(OsuSetting.PreferNoVideo)
+ },
};
}
}
diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs
index b199a2a0cf..e8e000f441 100644
--- a/osu.Game/Overlays/TabControlOverlayHeader.cs
+++ b/osu.Game/Overlays/TabControlOverlayHeader.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
-using osuTK;
namespace osu.Game.Overlays
{
@@ -22,6 +21,7 @@ namespace osu.Game.Overlays
{
protected OsuTabControl TabControl;
+ private readonly Box controlBackground;
private readonly BindableWithCurrent current = new BindableWithCurrent();
public Bindable Current
@@ -30,8 +30,6 @@ namespace osu.Game.Overlays
set => current.Current = value;
}
- private readonly Box controlBackground;
-
protected TabControlOverlayHeader()
{
HeaderInfo.Add(new Container
@@ -46,7 +44,7 @@ namespace osu.Game.Overlays
},
TabControl = CreateTabControl().With(control =>
{
- control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN };
+ control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
control.Current = Current;
})
}
@@ -56,7 +54,6 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
- TabControl.AccentColour = colourProvider.Highlight1;
controlBackground.Colour = colourProvider.Dark4;
}
@@ -65,14 +62,16 @@ namespace osu.Game.Overlays
public class OverlayHeaderTabControl : OverlayTabControl
{
+ private const float bar_height = 1;
+
public OverlayHeaderTabControl()
{
- BarHeight = 1;
RelativeSizeAxes = Axes.None;
AutoSizeAxes = Axes.X;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
- Height = 35;
+ Height = 47;
+ BarHeight = bar_height;
}
protected override TabItem CreateTabItem(T value) => new OverlayHeaderTabItem(value);
@@ -82,7 +81,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5, 0),
};
private class OverlayHeaderTabItem : OverlayTabItem
@@ -92,7 +90,8 @@ namespace osu.Game.Overlays
{
Text.Text = value.ToString().ToLower();
Text.Font = OsuFont.GetFont(size: 14);
- Bar.ExpandedSize = 5;
+ Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
+ Bar.Margin = new MarginPadding { Bottom = bar_height };
}
}
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
index 5e353d3319..f6646eb81d 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
@@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar
}
[BackgroundDependencyLoader(true)]
- private void load(SocialOverlay chat)
+ private void load(DashboardOverlay dashboard)
{
- StateContainer = chat;
+ StateContainer = dashboard;
}
}
}
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 045a52a0c7..b4c8a2d3ca 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Overlays
private GetUserRequest userReq;
protected ProfileHeader Header;
private ProfileSectionsContainer sectionsContainer;
- private ProfileTabControl tabs;
+ private ProfileSectionTabControl tabs;
public const float CONTENT_X_MARGIN = 70;
@@ -62,12 +62,11 @@ namespace osu.Game.Overlays
}
: Array.Empty();
- tabs = new ProfileTabControl
+ tabs = new ProfileSectionTabControl
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
- Height = 34
};
Add(new Box
@@ -149,19 +148,24 @@ namespace osu.Game.Overlays
}
}
- private class ProfileTabControl : OverlayTabControl
+ private class ProfileSectionTabControl : OverlayTabControl
{
- public ProfileTabControl()
+ private const float bar_height = 2;
+
+ public ProfileSectionTabControl()
{
TabContainer.RelativeSizeAxes &= ~Axes.X;
TabContainer.AutoSizeAxes |= Axes.X;
TabContainer.Anchor |= Anchor.x1;
TabContainer.Origin |= Anchor.x1;
+
+ Height = 36 + bar_height;
+ BarHeight = bar_height;
}
- protected override TabItem CreateTabItem(ProfileSection value) => new ProfileTabItem(value)
+ protected override TabItem CreateTabItem(ProfileSection value) => new ProfileSectionTabItem(value)
{
- AccentColour = AccentColour
+ AccentColour = AccentColour,
};
[BackgroundDependencyLoader]
@@ -170,12 +174,16 @@ namespace osu.Game.Overlays
AccentColour = colourProvider.Highlight1;
}
- private class ProfileTabItem : OverlayTabItem
+ private class ProfileSectionTabItem : OverlayTabItem
{
- public ProfileTabItem(ProfileSection value)
+ public ProfileSectionTabItem(ProfileSection value)
: base(value)
{
Text.Text = value.Title;
+ Text.Font = Text.Font.With(size: 16);
+ Text.Margin = new MarginPadding { Bottom = 10 + bar_height };
+ Bar.ExpandedSize = 10;
+ Bar.Margin = new MarginPadding { Bottom = bar_height };
}
}
}
@@ -187,6 +195,8 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both;
}
+ protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer();
+
protected override FlowContainer CreateScrollContentContainer() => new FillFlowContainer
{
Direction = FillDirection.Vertical,
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index fb4e945701..883288d6d7 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -69,10 +69,6 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(IFrameBasedClock framedClock)
{
- EditorBeatmap.HitObjectAdded += addHitObject;
- EditorBeatmap.HitObjectRemoved += removeHitObject;
- EditorBeatmap.StartTimeChanged += UpdateHitObject;
-
Config = Dependencies.Get().GetConfigFor(Ruleset);
try
@@ -236,10 +232,6 @@ namespace osu.Game.Rulesets.Edit
lastGridUpdateTime = EditorClock.CurrentTime;
}
- private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
-
- private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
-
public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@@ -302,19 +294,6 @@ namespace osu.Game.Rulesets.Edit
return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
}
-
- public override void UpdateHitObject(HitObject hitObject) => EditorBeatmap.UpdateHitObject(hitObject);
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (EditorBeatmap != null)
- {
- EditorBeatmap.HitObjectAdded -= addHitObject;
- EditorBeatmap.HitObjectRemoved -= removeHitObject;
- }
- }
}
[Cached(typeof(HitObjectComposer))]
@@ -344,12 +323,6 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null;
- ///
- /// Updates a , invoking and re-processing the beatmap.
- ///
- /// The to update.
- public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
-
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
public abstract float GetBeatSnapDistanceAt(double referenceTime);
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index ea77a6091a..fb1eb7adbf 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -106,6 +106,9 @@ namespace osu.Game.Rulesets.Edit
case ScrollEvent _:
return false;
+ case DoubleClickEvent _:
+ return false;
+
case MouseButtonEvent _:
return true;
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index a972d28480..e6a63eae4f 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -108,11 +108,6 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected;
- ///
- /// Updates the , invoking and re-processing the beatmap.
- ///
- protected void UpdateHitObject() => composer?.UpdateHitObject(HitObject);
-
///
/// The s to be displayed in the context menu for this .
///
diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs
index 4e4a75db82..a1915b974c 100644
--- a/osu.Game/Rulesets/Mods/ModHidden.cs
+++ b/osu.Game/Rulesets/Mods/ModHidden.cs
@@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods
protected Bindable IncreaseFirstObjectVisibility = new Bindable();
+ ///
+ /// Check whether the provided hitobject should be considered the "first" hideable object.
+ /// Can be used to skip spinners, for instance.
+ ///
+ /// The hitobject to check.
+ protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true;
+
public void ReadFromConfig(OsuConfigManager config)
{
IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility);
@@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable drawables)
{
- foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0))
- d.ApplyCustomUpdateState += ApplyHiddenState;
+ if (IncreaseFirstObjectVisibility.Value)
+ drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
+
+ foreach (var dho in drawables)
+ dho.ApplyCustomUpdateState += ApplyHiddenState;
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 0011faefbb..b14927bcd5 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -12,11 +12,13 @@ using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading;
+using osu.Framework.Audio;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
+using osu.Game.Configuration;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@@ -84,8 +86,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public JudgementResult Result { get; private set; }
+ ///
+ /// The relative X position of this hit object for sample playback balance adjustment.
+ ///
+ ///
+ /// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right).
+ /// Dampening is post-applied to ensure the effect is not too intense.
+ ///
+ protected virtual float SamplePlaybackPosition => 0.5f;
+
+ private readonly BindableDouble balanceAdjust = new BindableDouble();
+
private BindableList samplesBindable;
private Bindable startTimeBindable;
+ private Bindable userPositionalHitSounds;
private Bindable comboIndexBindable;
public override bool RemoveWhenNotAlive => false;
@@ -104,8 +118,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
+ userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds);
var judgement = HitObject.CreateJudgement();
Result = CreateResult(judgement);
@@ -156,7 +171,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
- AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
+ Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
+ Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust);
+ AddInternal(Samples);
}
private void onDefaultsApplied() => apply(HitObject);
@@ -353,7 +370,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
///
- public virtual void PlaySamples() => Samples?.Play();
+ public virtual void PlaySamples()
+ {
+ const float balance_adjust_amount = 0.4f;
+
+ balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
+ Samples?.Play();
+ }
protected override void Update()
{
@@ -375,7 +398,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
- protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => AllJudged && base.ComputeIsMaskedAway(maskingBounds);
+ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds);
protected override void UpdateAfterChildren()
{
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 8d3ad5984f..9a60a0a75c 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -409,22 +409,34 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
- private class LegacyHitSampleInfo : HitSampleInfo
+ internal class LegacyHitSampleInfo : HitSampleInfo
{
+ private int customSampleBank;
+
public int CustomSampleBank
{
+ get => customSampleBank;
set
{
- if (value > 1)
+ customSampleBank = value;
+
+ if (value >= 2)
Suffix = value.ToString();
}
}
}
- private class FileHitSampleInfo : HitSampleInfo
+ private class FileHitSampleInfo : LegacyHitSampleInfo
{
public string Filename;
+ public FileHitSampleInfo()
+ {
+ // Make sure that the LegacyBeatmapSkin does not fall back to the user skin.
+ // Note that this does not change the lookup names, as they are overridden locally.
+ CustomSampleBank = 1;
+ }
+
public override IEnumerable LookupNames => new[]
{
Filename,
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 8eafaa88ec..1f40f44dce 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Scoring
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
- return bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25);
+ return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier / 25);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index c81c6059cc..ad16e22e5e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -37,6 +37,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private SelectionHandler selectionHandler;
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
[Resolved]
private IAdjustableClock adjustableClock { get; set; }
@@ -164,7 +167,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
if (movementBlueprint != null)
+ {
+ isDraggingBlueprint = true;
+ changeHandler?.BeginChange();
return true;
+ }
if (DragBox.HandleDrag(e))
{
@@ -191,6 +198,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right)
return;
+ if (isDraggingBlueprint)
+ {
+ changeHandler?.EndChange();
+ isDraggingBlueprint = false;
+ }
+
if (DragBox.State == Visibility.Visible)
{
DragBox.Hide();
@@ -354,6 +367,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private Vector2? movementBlueprintOriginalPosition;
private SelectionBlueprint movementBlueprint;
+ private bool isDraggingBlueprint;
///
/// Attempts to begin the movement of any selected blueprints.
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index fc46bf3fed..764eae1056 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -40,6 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
public SelectionHandler()
{
selectedBlueprints = new List();
@@ -152,8 +155,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
+ changeHandler?.BeginChange();
+
foreach (var h in selectedBlueprints.ToList())
- editorBeatmap.Remove(h.HitObject);
+ editorBeatmap?.Remove(h.HitObject);
+
+ changeHandler?.EndChange();
}
#endregion
@@ -205,6 +212,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The name of the hit sample.
public void AddHitSample(string sampleName)
{
+ changeHandler?.BeginChange();
+
foreach (var h in SelectedHitObjects)
{
// Make sure there isn't already an existing sample
@@ -213,6 +222,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
+
+ changeHandler?.EndChange();
}
///
@@ -221,8 +232,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The name of the hit sample.
public void RemoveHitSample(string sampleName)
{
+ changeHandler?.BeginChange();
+
foreach (var h in SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
+
+ changeHandler?.EndChange();
}
#endregion
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index ddca5e42c2..1cb4f737c1 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -60,8 +60,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track;
- MinZoom = getZoomLevelForVisibleMilliseconds(10000);
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
+ MinZoom = getZoomLevelForVisibleMilliseconds(10000);
Zoom = getZoomLevelForVisibleMilliseconds(2000);
}, true);
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 8f12c2f0ed..16ba3ba89a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -254,14 +254,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White;
}
- protected override bool OnDragStart(DragStartEvent e) => true;
-
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ changeHandler?.BeginChange();
+ return true;
+ }
+
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
@@ -301,6 +308,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
base.OnDragEnd(e);
OnDragHandled?.Invoke(null);
+ changeHandler?.EndChange();
}
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index f1cbed57f1..9a1f450dc6 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -62,6 +62,7 @@ namespace osu.Game.Screens.Edit
private IBeatmap playableBeatmap;
private EditorBeatmap editorBeatmap;
+ private EditorChangeHandler changeHandler;
private DependencyContainer dependencies;
@@ -100,10 +101,14 @@ namespace osu.Game.Screens.Edit
}
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap));
-
dependencies.CacheAs(editorBeatmap);
+ changeHandler = new EditorChangeHandler(editorBeatmap);
+ dependencies.CacheAs(changeHandler);
+
EditorMenuBar menuBar;
+ OsuMenuItem undoMenuItem;
+ OsuMenuItem redoMenuItem;
var fileMenuItems = new List