diff --git a/osu.Android.props b/osu.Android.props
index 5b200ee104..723844155f 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
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/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.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/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/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 001100d3ce..b7074b7ee5 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
+ [Resolved(CanBeNull = true)]
+ private IEditorChangeHandler changeHandler { get; set; }
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -92,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int? placementControlPointIndex;
- protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null;
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (placementControlPointIndex != null)
+ {
+ changeHandler?.BeginChange();
+ return true;
+ }
+
+ return false;
+ }
protected override void OnDrag(DragEvent e)
{
@@ -103,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnDragEnd(DragEndEvent e)
{
- placementControlPointIndex = null;
+ if (placementControlPointIndex != null)
+ {
+ placementControlPointIndex = null;
+ changeHandler?.EndChange();
+ }
}
private BindableList controlPoints => HitObject.Path.ControlPoints;
diff --git a/osu.Game.Rulesets.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/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/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/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/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/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
new file mode 100644
index 0000000000..ef16976130
--- /dev/null
+++ b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs
@@ -0,0 +1,71 @@
+// 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.HasUndoState, Is.False);
+
+ handler.SaveState();
+
+ Assert.That(handler.HasUndoState, Is.True);
+
+ handler.RestoreState(-1);
+
+ Assert.That(handler.HasUndoState, Is.False);
+ }
+
+ [Test]
+ public void TestMaxStatesSaved()
+ {
+ var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
+
+ Assert.That(handler.HasUndoState, Is.False);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ handler.SaveState();
+
+ Assert.That(handler.HasUndoState, Is.True);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ {
+ Assert.That(handler.HasUndoState, Is.True);
+ handler.RestoreState(-1);
+ }
+
+ Assert.That(handler.HasUndoState, Is.False);
+ }
+
+ [Test]
+ public void TestMaxStatesExceeded()
+ {
+ var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
+
+ Assert.That(handler.HasUndoState, Is.False);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
+ handler.SaveState();
+
+ Assert.That(handler.HasUndoState, Is.True);
+
+ for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
+ {
+ Assert.That(handler.HasUndoState, Is.True);
+ handler.RestoreState(-1);
+ }
+
+ Assert.That(handler.HasUndoState, Is.False);
+ }
+ }
+}
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/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
new file mode 100644
index 0000000000..64d1024efb
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -0,0 +1,57 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Rulesets.Scoring
+{
+ public class ScoreProcessorTest
+ {
+ private ScoreProcessor scoreProcessor;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void SetUp()
+ {
+ scoreProcessor = new ScoreProcessor();
+ beatmap = new TestBeatmap(new RulesetInfo())
+ {
+ HitObjects = new List
+ {
+ new HitCircle()
+ }
+ };
+ }
+
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, 100)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, 300)]
+ public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
+ {
+ scoreProcessor.Mode.Value = scoringMode;
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
+ {
+ Type = hitResult
+ };
+ scoreProcessor.ApplyResult(judgementResult);
+
+ Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/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/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/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/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index ab5a652a94..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,6 +88,7 @@ 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);
@@ -183,6 +185,7 @@ namespace osu.Game.Configuration
ShowInterface,
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
+ FadePlayfieldWhenHealthLow,
MouseDisableButtons,
MouseDisableWheel,
AudioOffset,
@@ -214,6 +217,7 @@ namespace osu.Game.Configuration
IncreaseFirstObjectVisibility,
ScoreDisplayMode,
ExternalLinkWarning,
+ PreferNoVideo,
Scaling,
ScalingPositionX,
ScalingPositionY,
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/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 6a6c7b72a8..47600e4f68 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -16,20 +16,27 @@ namespace osu.Game.Online.API
{
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri);
- public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+ public T Result { get; private set; }
protected APIRequest()
{
- base.Success += onSuccess;
+ base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject);
}
- private void onSuccess() => Success?.Invoke(Result);
-
///
/// 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;
+
+ internal void TriggerSuccess(T result)
+ {
+ if (Result != null)
+ throw new InvalidOperationException("Attempted to trigger success more than once");
+
+ Result = result;
+ Success?.Invoke(result);
+ }
}
///
@@ -96,10 +103,15 @@ namespace osu.Game.Online.API
{
if (cancelled) return;
- Success?.Invoke();
+ TriggerSuccess();
});
}
+ internal 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/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/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/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/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
index 08e3ed9b38..387ced6acb 100644
--- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs
+++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs
@@ -6,6 +6,7 @@ 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;
@@ -16,8 +17,6 @@ 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.
///
@@ -25,12 +24,11 @@ namespace osu.Game.Overlays.Direct
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,
@@ -50,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)
{
@@ -59,6 +57,8 @@ namespace osu.Game.Overlays.Direct
return;
}
+ noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo);
+
button.Action = () =>
{
switch (State.Value)
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Direct
break;
default:
- beatmaps.Download(BeatmapSet.Value, noVideo);
+ beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value);
break;
}
};
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/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 ef03c0622a..93a02ea0e4 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -53,6 +53,11 @@ 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)
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/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 6ec30f7707..b4c8a2d3ca 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -195,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/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/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/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/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs
index 00a27801f4..a8204715cd 100644
--- a/osu.Game/Screens/Edit/EditorChangeHandler.cs
+++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit
private int bulkChangesStarted;
private bool isRestoring;
+ public const int MAX_SAVED_STATES = 50;
+
///
/// Creates a new .
///
@@ -43,6 +45,8 @@ namespace osu.Game.Screens.Edit
SaveState();
}
+ public bool HasUndoState => currentState > 0;
+
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
@@ -74,6 +78,9 @@ namespace osu.Game.Screens.Edit
if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
+ if (savedStates.Count > MAX_SAVED_STATES)
+ savedStates.RemoveAt(0);
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
index ed3f9af8e2..d7dcca9809 100644
--- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs
@@ -212,8 +212,8 @@ namespace osu.Game.Screens.Multi
private class PlaylistDownloadButton : PanelDownloadButton
{
- public PlaylistDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
- : base(beatmapSet, noVideo)
+ public PlaylistDownloadButton(BeatmapSetInfo beatmapSet)
+ : base(beatmapSet)
{
Alpha = 0;
}
diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs
new file mode 100644
index 0000000000..a49aa89a7c
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/FailingLayer.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 System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Scoring;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by .
+ ///
+ public class FailingLayer : HealthDisplay
+ {
+ private const float max_alpha = 0.4f;
+ private const int fade_time = 400;
+ private const float gradient_size = 0.3f;
+
+ ///
+ /// The threshold under which the current player life should be considered low and the layer should start fading in.
+ ///
+ public double LowHealthThreshold = 0.20f;
+
+ private readonly Bindable enabled = new Bindable();
+ private readonly Container boxes;
+
+ private Bindable configEnabled;
+ private HealthProcessor healthProcessor;
+
+ public FailingLayer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Children = new Drawable[]
+ {
+ boxes = new Container
+ {
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)),
+ Height = gradient_size,
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = gradient_size,
+ Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ },
+ }
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour color, OsuConfigManager config)
+ {
+ boxes.Colour = color.Red;
+
+ configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow);
+ enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateBindings();
+ }
+
+ public override void BindHealthProcessor(HealthProcessor processor)
+ {
+ base.BindHealthProcessor(processor);
+
+ healthProcessor = processor;
+ updateBindings();
+ }
+
+ private void updateBindings()
+ {
+ if (LoadState < LoadState.Ready)
+ return;
+
+ enabled.UnbindBindings();
+
+ // Don't display ever if the ruleset is not using a draining health display.
+ if (healthProcessor is DrainingHealthProcessor)
+ enabled.BindTo(configEnabled);
+ else
+ enabled.Value = false;
+ }
+
+ protected override void Update()
+ {
+ double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha);
+
+ boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f);
+
+ base.Update();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
index 37038ad58c..edc9dedf24 100644
--- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs
@@ -3,15 +3,29 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Play.HUD
{
+ ///
+ /// A container for components displaying the current player health.
+ /// Gets bound automatically to the when inserted to hierarchy.
+ ///
public abstract class HealthDisplay : Container
{
- public readonly BindableDouble Current = new BindableDouble
+ public readonly BindableDouble Current = new BindableDouble(1)
{
MinValue = 0,
MaxValue = 1
};
+
+ ///
+ /// Bind the tracked fields of to this health display.
+ ///
+ public virtual void BindHealthProcessor(HealthProcessor processor)
+ {
+ Current.BindTo(processor.Health);
+ }
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index e06f6d19c2..5114efd9a9 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -37,6 +37,7 @@ namespace osu.Game.Screens.Play
public readonly HitErrorDisplay HitErrorDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
+ public readonly FailingLayer FailingLayer;
public Bindable ShowHealthbar = new Bindable(true);
@@ -75,6 +76,7 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
+ FailingLayer = CreateFailingLayer(),
visibilityContainer = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -260,6 +262,8 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20 }
};
+ protected virtual FailingLayer CreateFailingLayer() => new FailingLayer();
+
protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
{
Anchor = Anchor.BottomRight,
@@ -304,7 +308,8 @@ namespace osu.Game.Screens.Play
protected virtual void BindHealthProcessor(HealthProcessor processor)
{
- HealthDisplay?.Current.BindTo(processor.Health);
+ HealthDisplay?.BindHealthProcessor(processor);
+ FailingLayer?.BindHealthProcessor(processor);
}
}
}
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 59dddc2baa..a8225ba1ec 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -48,6 +48,11 @@ namespace osu.Game.Screens.Select
///
public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet;
+ ///
+ /// A function to optionally decide on a recommended difficulty from a beatmap set.
+ ///
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
private CarouselBeatmapSet selectedBeatmapSet;
///
@@ -116,6 +121,7 @@ namespace osu.Game.Screens.Select
private readonly Stack randomSelectedBeatmaps = new Stack();
protected List Items = new List();
+
private CarouselRoot root;
public BeatmapCarousel()
@@ -579,7 +585,10 @@ namespace osu.Game.Screens.Select
b.Metadata = beatmapSet.Metadata;
}
- var set = new CarouselBeatmapSet(beatmapSet);
+ var set = new CarouselBeatmapSet(beatmapSet)
+ {
+ GetRecommendedBeatmap = beatmaps => GetRecommendedBeatmap?.Invoke(beatmaps)
+ };
foreach (var c in set.Beatmaps)
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 8e323c66e2..92ccfde14b 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Screens.Select.Carousel
public BeatmapSetInfo BeatmapSet;
+ public Func, BeatmapInfo> GetRecommendedBeatmap;
+
public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
{
BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet));
@@ -28,6 +30,17 @@ namespace osu.Game.Screens.Select.Carousel
protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this);
+ protected override CarouselItem GetNextToSelect()
+ {
+ if (LastSelected == null)
+ {
+ if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended)
+ return Children.OfType().First(b => b.Beatmap == recommended);
+ }
+
+ return base.GetNextToSelect();
+ }
+
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
{
if (!(other is CarouselBeatmapSet otherSet))
diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
index 6ce12f7b89..262bea9c71 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
@@ -90,11 +90,15 @@ namespace osu.Game.Screens.Select.Carousel
PerformSelection();
}
+ protected virtual CarouselItem GetNextToSelect()
+ {
+ return Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
+ Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ }
+
protected virtual void PerformSelection()
{
- CarouselItem nextToSelect =
- Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
- Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ CarouselItem nextToSelect = GetNextToSelect();
if (nextToSelect != null)
nextToSelect.State.Value = CarouselItemState.Selected;
diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs
new file mode 100644
index 0000000000..20cdca858a
--- /dev/null
+++ b/osu.Game/Screens/Select/DifficultyRecommender.cs
@@ -0,0 +1,92 @@
+// 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.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Screens.Select
+{
+ public class DifficultyRecommender : Component, IOnlineComponent
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [Resolved]
+ private Bindable ruleset { get; set; }
+
+ private readonly Dictionary recommendedStarDifficulty = new Dictionary();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ api.Register(this);
+ }
+
+ ///
+ /// Find the recommended difficulty from a selection of available difficulties for the current local user.
+ ///
+ ///
+ /// This requires the user to be online for now.
+ ///
+ /// A collection of beatmaps to select a difficulty from.
+ /// The recommended difficulty, or null if a recommendation could not be provided.
+ public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps)
+ {
+ if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars))
+ {
+ return beatmaps.OrderBy(b =>
+ {
+ var difference = b.StarDifficulty - stars;
+ return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
+ }).FirstOrDefault();
+ }
+
+ return null;
+ }
+
+ private void calculateRecommendedDifficulties()
+ {
+ rulesets.AvailableRulesets.ForEach(rulesetInfo =>
+ {
+ var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
+
+ req.Success += result =>
+ {
+ // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
+ recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
+ };
+
+ api.Queue(req);
+ });
+ }
+
+ public void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ switch (state)
+ {
+ case APIState.Online:
+ calculateRecommendedDifficulties();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ api.Unregister(this);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 895a8ad0c9..f164056ede 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -81,6 +81,8 @@ namespace osu.Game.Screens.Select
protected BeatmapCarousel Carousel { get; private set; }
+ private DifficultyRecommender recommender;
+
private BeatmapInfoWedge beatmapInfoWedge;
private DialogOverlay dialogOverlay;
@@ -109,6 +111,7 @@ namespace osu.Game.Screens.Select
AddRangeInternal(new Drawable[]
{
+ recommender = new DifficultyRecommender(),
new ResetScrollContainer(() => Carousel.ScrollToSelected())
{
RelativeSizeAxes = Axes.Y,
@@ -156,6 +159,7 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.Both,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
+ GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
},
}
},
diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs
index a1ddafbacf..d13c874ee2 100644
--- a/osu.Game/Storyboards/Storyboard.cs
+++ b/osu.Game/Storyboards/Storyboard.cs
@@ -47,9 +47,6 @@ namespace osu.Game.Storyboards
if (backgroundPath == null)
return false;
- if (GetLayer("Video").Elements.Any())
- return true;
-
return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath);
}
}
diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
index ce95dfa62f..dc67d28f63 100644
--- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
+++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
@@ -4,8 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Input;
-using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -15,13 +13,11 @@ using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual
{
[Cached(Type = typeof(IPlacementHandler))]
- public abstract class PlacementBlueprintTestScene : OsuTestScene, IPlacementHandler
+ public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{
- protected Container HitObjectContainer;
+ protected readonly Container HitObjectContainer;
private PlacementBlueprint currentBlueprint;
- private InputManager inputManager;
-
protected PlacementBlueprintTestScene()
{
Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
@@ -45,8 +41,7 @@ namespace osu.Game.Tests.Visual
{
base.LoadComplete();
- inputManager = GetContainingInputManager();
- Add(currentBlueprint = CreateBlueprint());
+ ResetPlacement();
}
public void BeginPlacement(HitObject hitObject)
@@ -58,7 +53,13 @@ namespace osu.Game.Tests.Visual
if (commit)
AddHitObject(CreateHitObject(hitObject));
- Remove(currentBlueprint);
+ ResetPlacement();
+ }
+
+ protected void ResetPlacement()
+ {
+ if (currentBlueprint != null)
+ Remove(currentBlueprint);
Add(currentBlueprint = CreateBlueprint());
}
@@ -66,10 +67,11 @@ namespace osu.Game.Tests.Visual
{
}
- protected override bool OnMouseMove(MouseMoveEvent e)
+ protected override void Update()
{
- currentBlueprint.UpdatePosition(e.ScreenSpaceMousePosition);
- return true;
+ base.Update();
+
+ currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position);
}
public override void Add(Drawable drawable)
@@ -79,7 +81,7 @@ namespace osu.Game.Tests.Visual
if (drawable is PlacementBlueprint blueprint)
{
blueprint.Show();
- blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position);
+ blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position);
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 2eae969ab4..76f7a030f9 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index c46e9674d2..7a487a6430 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+