diff --git a/osu.Android.props b/osu.Android.props
index 301c615ce4..3cd4dc48bf 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -54,6 +54,6 @@
-
+
diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest
new file mode 100644
index 0000000000..2e9127bf44
--- /dev/null
+++ b/osu.Desktop/app.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 453cf6f94d..60cada3ae7 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -8,6 +8,7 @@
osu!lazer
osu!lazer
lazer.ico
+ app.manifest
0.0.0
0.0.0
@@ -23,11 +24,11 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 33780427b6..d5d99640af 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -116,7 +116,23 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime;
- public SliderPath Path { get; set; }
+ private readonly SliderPath path = new SliderPath();
+
+ public SliderPath Path
+ {
+ get => path;
+ set
+ {
+ path.ControlPoints.Clear();
+ path.ExpectedDistance.Value = null;
+
+ if (value != null)
+ {
+ path.ControlPoints.AddRange(value.ControlPoints);
+ path.ExpectedDistance.Value = value.ExpectedDistance.Value;
+ }
+ }
+ }
public double Distance => Path.Distance;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index e3c6c93d01..025fa9c56e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle")
+ InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 693faee3b7..85a41137d4 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.931145117263422, "diffcalc-test")]
+ [TestCase(6.9311451172608853d, "diffcalc-test")]
[TestCase(1.0736587013228804d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
index dde2aa53e0..013920684c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep($"move mouse to control point {index}", () =>
{
- Vector2 position = slider.Position + slider.Path.ControlPoints[index];
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
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 0ccf020300..4fe02135c4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece
{
public Action RequestSelection;
- public Action ControlPointsChanged;
public readonly BindableBool IsSelected = new BindableBool();
public readonly int Index;
@@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
- Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
+ Position = slider.StackedPosition + slider.Path.ControlPoints[Index].Position.Value;
updateMarkerDisplay();
updateConnectingPath();
@@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
markerRing.Alpha = IsSelected.Value ? 1 : 0;
- Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ Color4 colour = slider.Path.ControlPoints[Index].Type.Value.HasValue ? colours.Red : colours.Yellow;
if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
@@ -116,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
path.ClearVertices();
- if (Index != slider.Path.ControlPoints.Length - 1)
+ if (Index != slider.Path.ControlPoints.Count - 1)
{
path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
+ path.AddVertex(slider.Path.ControlPoints[Index + 1].Position.Value - slider.Path.ControlPoints[Index].Position.Value);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
@@ -156,8 +155,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDrag(DragEvent e)
{
- var newControlPoints = slider.Path.ControlPoints.ToArray();
-
if (Index == 0)
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
@@ -168,29 +165,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
- for (int i = 1; i < newControlPoints.Length; i++)
- newControlPoints[i] -= movementDelta;
+ for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
+ slider.Path.ControlPoints[i].Position.Value -= movementDelta;
}
else
- newControlPoints[Index] += e.Delta;
-
- if (isSegmentSeparatorWithNext)
- newControlPoints[Index + 1] = newControlPoints[Index];
-
- if (isSegmentSeparatorWithPrevious)
- newControlPoints[Index - 1] = newControlPoints[Index];
-
- ControlPointsChanged?.Invoke(newControlPoints);
+ slider.Path.ControlPoints[Index].Position.Value += e.Delta;
return true;
}
protected override bool OnDragEnd(DragEndEvent e) => true;
-
- private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
-
- private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
-
- private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
}
}
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 cdca48490e..ced4af4e28 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -1,7 +1,6 @@
// 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;
@@ -14,6 +13,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose;
using osuTK;
@@ -23,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
- public Action ControlPointsChanged;
-
internal readonly Container Pieces;
private readonly Slider slider;
private readonly bool allowSelection;
@@ -55,12 +53,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
- while (slider.Path.ControlPoints.Length > Pieces.Count)
+ while (slider.Path.ControlPoints.Count > Pieces.Count)
{
- var piece = new PathControlPointPiece(slider, Pieces.Count)
- {
- ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
- };
+ var piece = new PathControlPointPiece(slider, Pieces.Count);
if (allowSelection)
piece.RequestSelection = selectPiece;
@@ -68,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Pieces.Add(piece);
}
- while (slider.Path.ControlPoints.Length < Pieces.Count)
+ while (slider.Path.ControlPoints.Count < Pieces.Count)
Pieces.Remove(Pieces[Pieces.Count - 1]);
}
@@ -105,38 +100,40 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool deleteSelected()
{
- var newControlPoints = new List();
-
- foreach (var piece in Pieces)
- {
- if (!piece.IsSelected.Value)
- newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
- }
+ List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => slider.Path.ControlPoints[p.Index]).ToList();
// Ensure that there are any points to be deleted
- if (newControlPoints.Count == slider.Path.ControlPoints.Length)
+ if (toRemove.Count == 0)
return false;
- // If there are 0 remaining control points, treat the slider as being deleted
- if (newControlPoints.Count == 0)
+ foreach (var c in toRemove)
+ {
+ // The first control point in the slider must have a type, so take it from the previous "first" one
+ // Todo: Should be handled within SliderPath itself
+ if (c == slider.Path.ControlPoints[0] && slider.Path.ControlPoints.Count > 1 && slider.Path.ControlPoints[1].Type.Value == null)
+ slider.Path.ControlPoints[1].Type.Value = slider.Path.ControlPoints[0].Type.Value;
+
+ slider.Path.ControlPoints.Remove(c);
+ }
+
+ // If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
+ if (slider.Path.ControlPoints.Count <= 1)
{
placementHandler?.Delete(slider);
return true;
}
- // Make control points relative
- Vector2 first = newControlPoints[0];
- for (int i = 0; i < newControlPoints.Count; i++)
- newControlPoints[i] = newControlPoints[i] - first;
-
- // The slider's position defines the position of the first control point, and all further control points are relative to that point
+ // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
+ // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
+ Vector2 first = slider.Path.ControlPoints[0].Position.Value;
+ foreach (var c in slider.Path.ControlPoints)
+ c.Position.Value -= first;
slider.Position += first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
- ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true;
}
@@ -154,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return new MenuItem[]
{
- new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints)}", MenuItemType.Destructive, () => deleteSelected())
+ new OsuMenuItem($"Delete {"control point".ToQuantity(selectedPoints, selectedPoints > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected())
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 9c0afada29..9b820261ab 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -1,10 +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.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@@ -27,11 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
- private readonly List segments = new List();
- private Vector2 cursor;
private InputManager inputManager;
private PlacementState state;
+ private PathControlPoint segmentStart;
+ private PathControlPoint cursor;
+ private int currentSegmentLength;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@@ -40,7 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
: base(new Objects.Slider())
{
RelativeSizeAxes = Axes.Both;
- segments.Add(new Segment(Vector2.Zero));
+
+ HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear));
+ currentSegmentLength = 1;
}
[BackgroundDependencyLoader]
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
- new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
+ new PathControlPointVisualiser(HitObject, false)
};
setState(PlacementState.Initial);
@@ -72,9 +72,11 @@ 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 = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
break;
}
}
@@ -91,7 +93,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (e.Button)
{
case MouseButton.Left:
- segments.Last().ControlPoints.Add(cursor);
+ ensureCursor();
+
+ // Detatch the cursor
+ cursor = null;
break;
}
@@ -110,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override bool OnDoubleClick(DoubleClickEvent e)
{
- segments.Add(new Segment(segments[segments.Count - 1].ControlPoints.Last()));
+ // Todo: This should all not occur on double click, but rather if the previous control point is hovered.
+ segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1];
+ segmentStart.Type.Value = PathType.Linear;
+
+ currentSegmentLength = 1;
return true;
}
@@ -132,14 +141,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updateSlider();
}
+ private void updatePathType()
+ {
+ switch (currentSegmentLength)
+ {
+ case 1:
+ case 2:
+ segmentStart.Type.Value = PathType.Linear;
+ break;
+
+ case 3:
+ segmentStart.Type.Value = PathType.PerfectCurve;
+ break;
+
+ default:
+ segmentStart.Type.Value = PathType.Bezier;
+ break;
+ }
+ }
+
+ private void ensureCursor()
+ {
+ if (cursor == null)
+ {
+ HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
+ currentSegmentLength++;
+
+ updatePathType();
+ }
+ }
+
private void updateSlider()
{
- Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
-
- var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
- var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
-
- HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance);
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@@ -156,15 +190,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Initial,
Body,
}
-
- private class Segment
- {
- public readonly List ControlPoints = new List();
-
- public Segment(Vector2 offset)
- {
- ControlPoints.Add(offset);
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 820d6c92d7..68873093a6 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.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 System;
using System.Diagnostics;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@@ -40,10 +39,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
- ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
+ ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
};
}
+ private IBindable pathVersion;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ pathVersion = HitObject.Path.Version.GetBoundCopy();
+ pathVersion.BindValueChanged(_ => updatePath());
+ }
+
protected override void Update()
{
base.Update();
@@ -77,12 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
Debug.Assert(placementControlPointIndex != null);
- Vector2 position = e.MousePosition - HitObject.Position;
-
- var controlPoints = HitObject.Path.ControlPoints.ToArray();
- controlPoints[placementControlPointIndex.Value] = position;
-
- onNewControlPoints(controlPoints);
+ HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position;
return true;
}
@@ -97,15 +101,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
position -= HitObject.Position;
- var controlPoints = new Vector2[HitObject.Path.ControlPoints.Length + 1];
- HitObject.Path.ControlPoints.CopyTo(controlPoints);
-
int insertionIndex = 0;
float minDistance = float.MaxValue;
- for (int i = 0; i < controlPoints.Length - 2; i++)
+ for (int i = 0; i < HitObject.Path.ControlPoints.Count - 1; i++)
{
- float dist = new Line(controlPoints[i], controlPoints[i + 1]).DistanceToPoint(position);
+ float dist = new Line(HitObject.Path.ControlPoints[i].Position.Value, HitObject.Path.ControlPoints[i + 1].Position.Value).DistanceToPoint(position);
if (dist < minDistance)
{
@@ -115,21 +116,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Move the control points from the insertion index onwards to make room for the insertion
- Array.Copy(controlPoints, insertionIndex, controlPoints, insertionIndex + 1, controlPoints.Length - insertionIndex - 1);
- controlPoints[insertionIndex] = position;
-
- onNewControlPoints(controlPoints);
+ HitObject.Path.ControlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } });
return insertionIndex;
}
- private void onNewControlPoints(Vector2[] controlPoints)
+ private void updatePath()
{
- var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints);
- var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance;
-
- HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance);
-
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
UpdateHitObject();
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index 3d566362ae..bc5f79331f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -28,11 +28,8 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
- var newControlPoints = new Vector2[slider.Path.ControlPoints.Length];
- for (int i = 0; i < slider.Path.ControlPoints.Length; i++)
- newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y);
-
- slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
+ foreach (var point in slider.Path.ControlPoints)
+ point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 69189758a6..1e0402d492 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable positionBindable = new Bindable();
private readonly IBindable stackHeightBindable = new Bindable();
private readonly IBindable scaleBindable = new Bindable();
- private readonly IBindable pathBindable = new Bindable();
+ private readonly IBindable pathVersion = new Bindable();
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@@ -84,9 +84,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
stackHeightBindable.BindTo(HitObject.StackHeightBindable);
scaleBindable.BindTo(HitObject.ScaleBindable);
- pathBindable.BindTo(slider.PathBindable);
+ pathVersion.BindTo(slider.Path.Version);
- pathBindable.BindValueChanged(_ => Body.Refresh());
+ pathVersion.BindValueChanged(_ => Body.Refresh());
AccentColour.BindValueChanged(colour =>
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index a10c66d1df..c5609b01e0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public class DrawableSliderHead : DrawableHitCircle
{
private readonly IBindable positionBindable = new Bindable();
- private readonly IBindable pathBindable = new Bindable();
+ private readonly IBindable pathVersion = new Bindable();
private readonly Slider slider;
@@ -27,10 +26,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load()
{
positionBindable.BindTo(HitObject.PositionBindable);
- pathBindable.BindTo(slider.PathBindable);
+ pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition());
- pathBindable.BindValueChanged(_ => updatePosition(), true);
+ pathVersion.BindValueChanged(_ => updatePosition(), true);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 42bf5e4d21..21a3a0d236 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
@@ -21,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
private readonly IBindable positionBindable = new Bindable();
- private readonly IBindable pathBindable = new Bindable();
+ private readonly IBindable pathVersion = new Bindable();
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
: base(hitCircle)
@@ -36,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true;
positionBindable.BindTo(hitCircle.PositionBindable);
- pathBindable.BindTo(slider.PathBindable);
+ pathVersion.BindTo(slider.Path.Version);
positionBindable.BindValueChanged(_ => updatePosition());
- pathBindable.BindValueChanged(_ => updatePosition(), true);
+ pathVersion.BindValueChanged(_ => updatePosition(), true);
// TODO: This has no drawable content. Support for skins should be added.
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index c6f5a075e0..34e5a7f3cd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using System.Linq;
-using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -28,17 +27,21 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
- public readonly Bindable PathBindable = new Bindable();
+ private readonly SliderPath path = new SliderPath();
public SliderPath Path
{
- get => PathBindable.Value;
+ get => path;
set
{
- PathBindable.Value = value;
- endPositionCache.Invalidate();
+ path.ControlPoints.Clear();
+ path.ExpectedDistance.Value = null;
- updateNestedPositions();
+ if (value != null)
+ {
+ path.ControlPoints.AddRange(value.ControlPoints);
+ path.ExpectedDistance.Value = value.ExpectedDistance.Value;
+ }
}
}
@@ -50,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
base.Position = value;
- endPositionCache.Invalidate();
-
updateNestedPositions();
}
}
@@ -112,6 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
SamplesBindable.ItemsAdded += _ => updateNestedSamples();
SamplesBindable.ItemsRemoved += _ => updateNestedSamples();
+ Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
@@ -189,6 +191,8 @@ namespace osu.Game.Rulesets.Osu.Objects
private void updateNestedPositions()
{
+ endPositionCache.Invalidate();
+
if (HeadCircle != null)
HeadCircle.Position = Position;
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 14c3369967..c17d2275b8 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public class SliderTailCircle : SliderCircle
{
- private readonly IBindable pathBindable = new Bindable();
+ private readonly IBindable pathVersion = new Bindable();
public SliderTailCircle(Slider slider)
{
- pathBindable.BindTo(slider.PathBindable);
- pathBindable.BindValueChanged(_ => Position = slider.EndPosition);
+ pathVersion.BindTo(slider.Path.Version);
+ pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
public override Judgement CreateJudgement() => new OsuSliderTailJudgement();
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
index 470ba3acae..05b38ae195 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
@@ -11,6 +11,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public class LegacyCursor : CompositeDrawable
{
+ private NonPlayfieldSprite cursor;
+ private bool spin;
+
public LegacyCursor()
{
Size = new Vector2(50);
@@ -22,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
+ spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
+
InternalChildren = new Drawable[]
{
new NonPlayfieldSprite
@@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- new NonPlayfieldSprite
+ cursor = new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
@@ -38,5 +43,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
};
}
+
+ protected override void LoadComplete()
+ {
+ if (spin)
+ cursor.Spin(10000, RotationDirection.Clockwise);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 479c250eab..f5b7d9166f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -49,7 +49,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
return this.GetAnimation(component.LookupName, true, false);
case OsuSkinComponents.SliderFollowCircle:
- return this.GetAnimation("sliderfollowcircle", true, true);
+ var followCircle = this.GetAnimation("sliderfollowcircle", true, true);
+ if (followCircle != null)
+ // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
+ followCircle.Scale *= 0.5f;
+ return followCircle;
case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, "");
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 98219cafe8..5d99960f10 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderPathRadius,
AllowSliderBallTint,
CursorExpand,
+ CursorRotate
}
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index 6e5b3b93e9..e618256c03 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -13,6 +13,8 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
@@ -25,6 +27,7 @@ namespace osu.Game.Tests.Visual.Editor
public override IReadOnlyList RequiredTypes => new[]
{
typeof(TimelineArea),
+ typeof(TimelineHitObjectDisplay),
typeof(Timeline),
typeof(TimelineButton),
typeof(CentreMarker)
@@ -35,6 +38,8 @@ namespace osu.Game.Tests.Visual.Editor
{
Beatmap.Value = new WaveformTestBeatmap(audio);
+ var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap);
+
Children = new Drawable[]
{
new FillFlowContainer
@@ -50,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editor
},
new TimelineArea
{
+ Child = new TimelineHitObjectDisplay(editorBeatmap),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 74ae641bfe..f02361e685 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -19,6 +20,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
@@ -55,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ foreach (var mod in Mods.Value.OfType())
+ mod.ApplyToTrack(Beatmap.Value.Track);
+
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() =>
{
@@ -63,6 +68,24 @@ namespace osu.Game.Tests.Visual.Gameplay
}));
}
+ ///
+ /// When exits early, it has to wait for the player load task
+ /// to complete before running disposal on player. This previously caused an issue where mod
+ /// speed adjustments were undone too late, causing cross-screen pollution.
+ ///
+ [Test]
+ public void TestEarlyExit()
+ {
+ AddStep("load dummy beatmap", () => ResetPlayer(false, () => Mods.Value = new[] { new OsuModNightcore() }));
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+ AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
+ AddStep("exit loader", () => loader.Exit());
+ AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
+ AddAssert("player did not load", () => !player.IsLoaded);
+ AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true);
+ AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1);
+ }
+
[Test]
public void TestBlockLoadViaMouseMovement()
{
@@ -196,6 +219,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public new VisualSettings VisualSettings => base.VisualSettings;
+ public new Task DisposalTask => base.DisposalTask;
+
public TestPlayerLoader(Func createPlayer)
: base(createPlayer)
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
new file mode 100644
index 0000000000..606395c289
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
@@ -0,0 +1,193 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Lines;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSliderPath : OsuTestScene
+ {
+ private readonly SmoothPath drawablePath;
+ private SliderPath path;
+
+ public TestSceneSliderPath()
+ {
+ Child = drawablePath = new SmoothPath
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ path = new SliderPath();
+ });
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (path != null)
+ {
+ List vertices = new List();
+ path.GetPathToProgress(vertices, 0, 1);
+
+ drawablePath.Vertices = vertices;
+ }
+ }
+
+ [Test]
+ public void TestEmptyPath()
+ {
+ }
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestSingleSegment(PathType type)
+ => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestMultipleSegment(PathType type)
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
+ path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
+ });
+ }
+
+ [Test]
+ public void TestAddControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
+ AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = { Value = new Vector2(100) } }));
+ }
+
+ [Test]
+ public void TestInsertControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
+ AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = { Value = new Vector2(0, 100) } }));
+ }
+
+ [Test]
+ public void TestRemoveControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
+ }
+
+ [Test]
+ public void TestChangePathType()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("change type to bezier", () => path.ControlPoints[0].Type.Value = PathType.Bezier);
+ }
+
+ [Test]
+ public void TestAddSegmentByChangingType()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
+ AddStep("change second point type to bezier", () => path.ControlPoints[1].Type.Value = PathType.Bezier);
+ }
+
+ [Test]
+ public void TestRemoveSegmentByChangingType()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ path.ControlPoints[1].Type.Value = PathType.Bezier;
+ });
+
+ AddStep("change second point type to null", () => path.ControlPoints[1].Type.Value = null);
+ }
+
+ [Test]
+ public void TestRemoveSegmentByRemovingControlPoint()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ path.ControlPoints[1].Type.Value = PathType.Bezier;
+ });
+
+ AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
+ }
+
+ [TestCase(2)]
+ [TestCase(4)]
+ public void TestPerfectCurveFallbackScenarios(int points)
+ {
+ AddStep("create path", () =>
+ {
+ switch (points)
+ {
+ case 2:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
+ break;
+
+ case 4:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ break;
+ }
+ });
+ }
+
+ [Test]
+ public void TestLengthenLastSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300);
+ }
+
+ [Test]
+ public void TestShortenLastSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
+ }
+
+ [Test]
+ public void TestShortenFirstSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50);
+ }
+
+ [Test]
+ public void TestShortenToZeroLength()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0);
+ }
+
+ [Test]
+ public void TestShortenToNegativeLength()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
+ }
+
+ private List createSegment(PathType type, params Vector2[] controlPoints)
+ {
+ var points = controlPoints.Select(p => new PathControlPoint { Position = { Value = p } }).ToList();
+ points[0].Type.Value = type;
+ return points;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index a4b8d1a24a..5dd02c1ddd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -95,6 +95,42 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
}
+ [Test]
+ public void TestNoFilterOnSimpleResume()
+ {
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ createSongSelect();
+
+ AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
+ AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
+
+ AddStep("return", () => songSelect.MakeCurrent());
+ AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
+ AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
+ }
+
+ [Test]
+ public void TestFilterOnResumeAfterChange()
+ {
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+
+ createSongSelect();
+
+ AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
+ AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
+
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+
+ AddStep("return", () => songSelect.MakeCurrent());
+ AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
+ AddAssert("filter count is 2", () => songSelect.FilterCount == 2);
+ }
+
[Test]
public void TestAudioResuming()
{
diff --git a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
index a68fd0ef40..c55988d1bb 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Parallax is off", () => stack.ParallaxAmount == 0);
}
- private class TestScreen : ScreenWithBeatmapBackground
+ public class TestScreen : ScreenWithBeatmapBackground
{
private readonly string screenText;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
index 3d39bb7003..7207506ccd 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Tests.Visual.UserInterface
@@ -11,13 +14,22 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture]
public class TestScenePopupDialog : OsuTestScene
{
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(PopupDialogOkButton),
+ typeof(PopupDialogCancelButton),
+ typeof(PopupDialogButton),
+ typeof(DialogButton),
+ };
+
public TestScenePopupDialog()
{
- Add(new TestPopupDialog
- {
- RelativeSizeAxes = Axes.Both,
- State = { Value = Framework.Graphics.Containers.Visibility.Visible },
- });
+ AddStep("new popup", () =>
+ Add(new TestPopupDialog
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Framework.Graphics.Containers.Visibility.Visible },
+ }));
}
private class TestPopupDialog : PopupDialog
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index 8e881fdd9c..9cce40c9d3 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game/Configuration/DatabasedConfigManager.cs b/osu.Game/Configuration/DatabasedConfigManager.cs
index 1ef4c2527a..b3783b45a8 100644
--- a/osu.Game/Configuration/DatabasedConfigManager.cs
+++ b/osu.Game/Configuration/DatabasedConfigManager.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
@@ -9,8 +10,8 @@ using osu.Game.Rulesets;
namespace osu.Game.Configuration
{
- public abstract class DatabasedConfigManager : ConfigManager
- where T : struct
+ public abstract class DatabasedConfigManager : ConfigManager
+ where TLookup : struct, Enum
{
private readonly SettingsStore settings;
@@ -53,7 +54,7 @@ namespace osu.Game.Configuration
private readonly List dirtySettings = new List();
- protected override void AddBindable(T lookup, Bindable bindable)
+ protected override void AddBindable(TLookup lookup, Bindable bindable)
{
base.AddBindable(lookup, bindable);
diff --git a/osu.Game/Configuration/InMemoryConfigManager.cs b/osu.Game/Configuration/InMemoryConfigManager.cs
index b0dc6b0e9c..ccf697f680 100644
--- a/osu.Game/Configuration/InMemoryConfigManager.cs
+++ b/osu.Game/Configuration/InMemoryConfigManager.cs
@@ -1,12 +1,13 @@
// 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.Configuration;
namespace osu.Game.Configuration
{
- public class InMemoryConfigManager : ConfigManager
- where T : struct
+ public class InMemoryConfigManager : ConfigManager
+ where TLookup : struct, Enum
{
public InMemoryConfigManager()
{
diff --git a/osu.Game/Graphics/IHasAccentColour.cs b/osu.Game/Graphics/IHasAccentColour.cs
index 1a66819379..af497da70f 100644
--- a/osu.Game/Graphics/IHasAccentColour.cs
+++ b/osu.Game/Graphics/IHasAccentColour.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics
///
/// A to which further transforms can be added.
public static TransformSequence FadeAccent(this T accentedDrawable, Color4 newColour, double duration = 0, Easing easing = Easing.None)
- where T : IHasAccentColour
+ where T : class, IHasAccentColour
=> accentedDrawable.TransformTo(nameof(accentedDrawable.AccentColour), newColour, duration, easing);
///
diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs
index 927ad13829..aed07e56ee 100644
--- a/osu.Game/Graphics/UserInterface/DialogButton.cs
+++ b/osu.Game/Graphics/UserInterface/DialogButton.cs
@@ -20,9 +20,10 @@ namespace osu.Game.Graphics.UserInterface
{
public class DialogButton : OsuClickableContainer
{
+ private const float idle_width = 0.8f;
private const float hover_width = 0.9f;
+
private const float hover_duration = 500;
- private const float glow_fade_duration = 250;
private const float click_duration = 200;
public readonly BindableBool Selected = new BindableBool();
@@ -99,7 +100,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Width = 0.8f,
+ Width = idle_width,
Masking = true,
MaskingSmoothness = 2,
EdgeEffect = new EdgeEffectParameters
@@ -199,26 +200,50 @@ namespace osu.Game.Graphics.UserInterface
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceivePositionalInputAt(screenSpacePos);
+ private bool clickAnimating;
+
protected override bool OnClick(ClickEvent e)
{
- colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In);
- flash();
-
- this.Delay(click_duration).Schedule(delegate
+ var flash = new Box
{
- colourContainer.ResizeTo(new Vector2(0.8f, 1f));
- spriteText.Spacing = Vector2.Zero;
- glowContainer.FadeOut();
- });
+ RelativeSizeAxes = Axes.Both,
+ Colour = ButtonColour,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0.05f
+ };
+
+ colourContainer.Add(flash);
+ flash.FadeOutFromOne(100).Expire();
+
+ clickAnimating = true;
+ colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint)
+ .OnComplete(_ =>
+ {
+ clickAnimating = false;
+ Selected.TriggerChange();
+ });
return base.OnClick(e);
}
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
+ return base.OnMouseDown(e);
+ }
+
+ protected override bool OnMouseUp(MouseUpEvent e)
+ {
+ if (Selected.Value)
+ colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
+ return base.OnMouseUp(e);
+ }
+
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
-
Selected.Value = true;
+
return true;
}
@@ -230,36 +255,23 @@ namespace osu.Game.Graphics.UserInterface
private void selectionChanged(ValueChangedEvent args)
{
+ if (clickAnimating)
+ return;
+
if (args.NewValue)
{
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
- colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic);
- glowContainer.FadeIn(glow_fade_duration, Easing.Out);
+ colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
+ glowContainer.FadeIn(hover_duration, Easing.OutQuint);
}
else
{
- colourContainer.ResizeTo(new Vector2(0.8f, 1f), hover_duration, Easing.OutElastic);
+ colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
- glowContainer.FadeOut(glow_fade_duration, Easing.Out);
+ glowContainer.FadeOut(hover_duration, Easing.OutQuint);
}
}
- private void flash()
- {
- var flash = new Box
- {
- RelativeSizeAxes = Axes.Both
- };
-
- colourContainer.Add(flash);
-
- flash.Colour = ButtonColour;
- flash.Blending = BlendingParameters.Additive;
- flash.Alpha = 0.3f;
- flash.FadeOutFromOne(click_duration);
- flash.Expire();
- }
-
private void updateGlow()
{
leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour);
diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
index e132027787..528d7d60f8 100644
--- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
@@ -6,12 +6,10 @@ using System;
namespace osu.Game.Graphics.UserInterface
{
public class OsuEnumDropdown : OsuDropdown
+ where T : struct, Enum
{
public OsuEnumDropdown()
{
- if (!typeof(T).IsEnum)
- throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument");
-
Items = (T[])Enum.GetValues(typeof(T));
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
index 79ce04ed66..b941cd8973 100644
--- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Online.API.Requests.Responses
PP = PP,
Beatmap = Beatmap,
RulesetID = OnlineRulesetID,
- Hash = "online", // todo: temporary?
+ Hash = Replay ? "online" : string.Empty, // todo: temporary?
Rank = Rank,
Ruleset = ruleset,
Mods = mods,
diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs
index dcec17788a..9a0e112727 100644
--- a/osu.Game/Online/DownloadTrackingComposite.cs
+++ b/osu.Game/Online/DownloadTrackingComposite.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Online
///
protected readonly Bindable State = new Bindable();
- protected readonly Bindable Progress = new Bindable();
+ protected readonly BindableNumber Progress = new BindableNumber { MinValue = 0, MaxValue = 1 };
protected DownloadTrackingComposite(TModel model = null)
{
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 58f5f02956..f6723839b2 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
for (int i = 0; i < value.Count; i++)
- backgroundFlow.Add(new ScoreTableRowBackground(i));
+ backgroundFlow.Add(new ScoreTableRowBackground(i, value[i]));
Columns = createHeaders(value[0]);
Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular();
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs
index d820f4d89d..724a7f8b55 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs
@@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
+using osu.Game.Online.API;
+using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapSet.Scores
{
@@ -17,8 +19,14 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly Box hoveredBackground;
private readonly Box background;
- public ScoreTableRowBackground(int index)
+ private readonly int index;
+ private readonly ScoreInfo score;
+
+ public ScoreTableRowBackground(int index, ScoreInfo score)
{
+ this.index = index;
+ this.score = score;
+
RelativeSizeAxes = Axes.X;
Height = 25;
@@ -37,16 +45,21 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Alpha = 0,
},
};
-
- if (index % 2 != 0)
- background.Alpha = 0;
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, IAPIProvider api)
{
- hoveredBackground.Colour = colours.Gray4;
- background.Colour = colours.Gray3;
+ var isOwnScore = api.LocalUser.Value.Id == score.UserID;
+
+ if (isOwnScore)
+ background.Colour = colours.GreenDarker;
+ else if (index % 2 == 0)
+ background.Colour = colours.Gray3;
+ else
+ background.Alpha = 0;
+
+ hoveredBackground.Colour = isOwnScore ? colours.GreenDark : colours.Gray4;
}
protected override bool OnHover(HoverEvent e)
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index c6b4787ff1..69a4a4181a 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -167,10 +167,6 @@ namespace osu.Game.Overlays.Mods
{
switch (e.Button)
{
- case MouseButton.Left:
- SelectNext(1);
- break;
-
case MouseButton.Right:
SelectNext(-1);
break;
@@ -180,6 +176,13 @@ namespace osu.Game.Overlays.Mods
return true;
}
+ protected override bool OnClick(ClickEvent e)
+ {
+ SelectNext(1);
+
+ return true;
+ }
+
///
/// Select the next available mod in a specified direction.
///
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 5e0a67c2f7..bafdad3508 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -261,8 +261,8 @@ namespace osu.Game.Overlays
if (allowRateAdjustments)
{
- foreach (var mod in mods.Value.OfType())
- mod.ApplyToClock(track);
+ foreach (var mod in mods.Value.OfType())
+ mod.ApplyToTrack(track);
}
}
diff --git a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs
index 0808cc8fcc..a33f4eb30d 100644
--- a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs
+++ b/osu.Game/Overlays/SearchableList/DisplayStyleControl.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 osu.Framework.Bindables;
using osuTK;
using osu.Framework.Graphics;
@@ -11,6 +12,7 @@ using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.SearchableList
{
public class DisplayStyleControl : Container
+ where T : struct, Enum
{
public readonly SlimEnumDropdown Dropdown;
public readonly Bindable DisplayStyle = new Bindable();
diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
index 372da94b37..117f905de4 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
@@ -13,7 +13,9 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Overlays.SearchableList
{
- public abstract class SearchableListFilterControl : Container
+ public abstract class SearchableListFilterControl : Container
+ where TTab : struct, Enum
+ where TCategory : struct, Enum
{
private const float padding = 10;
@@ -21,12 +23,12 @@ namespace osu.Game.Overlays.SearchableList
private readonly Box tabStrip;
public readonly SearchTextBox Search;
- public readonly PageTabControl Tabs;
- public readonly DisplayStyleControl DisplayStyleControl;
+ public readonly PageTabControl Tabs;
+ public readonly DisplayStyleControl DisplayStyleControl;
protected abstract Color4 BackgroundColour { get; }
- protected abstract T DefaultTab { get; }
- protected abstract U DefaultCategory { get; }
+ protected abstract TTab DefaultTab { get; }
+ protected abstract TCategory DefaultCategory { get; }
protected virtual Drawable CreateSupplementaryControls() => null;
///
@@ -36,9 +38,6 @@ namespace osu.Game.Overlays.SearchableList
protected SearchableListFilterControl()
{
- if (!typeof(T).IsEnum)
- throw new InvalidOperationException("SearchableListFilterControl's sort tabs only support enums as the generic type argument");
-
RelativeSizeAxes = Axes.X;
var controls = CreateSupplementaryControls();
@@ -90,7 +89,7 @@ namespace osu.Game.Overlays.SearchableList
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 225 },
- Child = Tabs = new PageTabControl
+ Child = Tabs = new PageTabControl
{
RelativeSizeAxes = Axes.X,
},
@@ -105,7 +104,7 @@ namespace osu.Game.Overlays.SearchableList
},
},
},
- DisplayStyleControl = new DisplayStyleControl
+ DisplayStyleControl = new DisplayStyleControl
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
diff --git a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs b/osu.Game/Overlays/SearchableList/SearchableListHeader.cs
index 73dca956d1..66fedf0a56 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListHeader.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.SearchableList
{
public abstract class SearchableListHeader : Container
+ where T : struct, Enum
{
public readonly HeaderTabControl Tabs;
@@ -24,9 +25,6 @@ namespace osu.Game.Overlays.SearchableList
protected SearchableListHeader()
{
- if (!typeof(T).IsEnum)
- throw new InvalidOperationException("BrowseHeader only supports enums as the generic type argument");
-
RelativeSizeAxes = Axes.X;
Height = 90;
diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
index fb0c1d9808..37478d902b 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.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 osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -16,19 +17,22 @@ namespace osu.Game.Overlays.SearchableList
public const float WIDTH_PADDING = 80;
}
- public abstract class SearchableListOverlay : SearchableListOverlay
+ public abstract class SearchableListOverlay : SearchableListOverlay
+ where THeader : struct, Enum
+ where TTab : struct, Enum
+ where TCategory : struct, Enum
{
private readonly Container scrollContainer;
- protected readonly SearchableListHeader Header;
- protected readonly SearchableListFilterControl Filter;
+ protected readonly SearchableListHeader Header;
+ protected readonly SearchableListFilterControl Filter;
protected readonly FillFlowContainer ScrollFlow;
protected abstract Color4 BackgroundColour { get; }
protected abstract Color4 TrianglesColourLight { get; }
protected abstract Color4 TrianglesColourDark { get; }
- protected abstract SearchableListHeader CreateHeader();
- protected abstract SearchableListFilterControl CreateFilterControl();
+ protected abstract SearchableListHeader CreateHeader();
+ protected abstract SearchableListFilterControl CreateFilterControl();
protected SearchableListOverlay()
{
diff --git a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs b/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs
index f320ef1344..9e7ff1205f 100644
--- a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs
+++ b/osu.Game/Overlays/SearchableList/SlimEnumDropdown.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 osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -11,6 +12,7 @@ using osuTK;
namespace osu.Game.Overlays.SearchableList
{
public class SlimEnumDropdown : OsuEnumDropdown
+ where T : struct, Enum
{
protected override DropdownHeader CreateHeader() => new SlimDropdownHeader();
diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
index 9f09f251c2..c77d14632b 100644
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public class SettingsEnumDropdown : SettingsDropdown
+ where T : struct, Enum
{
protected override OsuDropdown CreateDropdown() => new DropdownControl();
diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs
index 5000156e97..1fa233d9d4 100644
--- a/osu.Game/Overlays/SettingsSubPanel.cs
+++ b/osu.Game/Overlays/SettingsSubPanel.cs
@@ -3,16 +3,14 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Input.Bindings;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
-using osu.Game.Screens.Ranking;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -36,21 +34,21 @@ namespace osu.Game.Overlays
protected override bool DimMainContent => false; // dimming is handled by main overlay
- private class BackButton : OsuClickableContainer, IKeyBindingHandler
+ private class BackButton : OsuButton
{
- private AspectContainer aspect;
-
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(Sidebar.DEFAULT_WIDTH);
- Children = new Drawable[]
+
+ BackgroundColour = Color4.Black;
+
+ AddRange(new Drawable[]
{
- aspect = new AspectContainer
+ new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
new SpriteIcon
@@ -71,34 +69,8 @@ namespace osu.Game.Overlays
},
}
}
- };
+ });
}
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- aspect.ScaleTo(0.75f, 2000, Easing.OutQuint);
- return base.OnMouseDown(e);
- }
-
- protected override bool OnMouseUp(MouseUpEvent e)
- {
- aspect.ScaleTo(1, 1000, Easing.OutElastic);
- return base.OnMouseUp(e);
- }
-
- public bool OnPressed(GlobalAction action)
- {
- switch (action)
- {
- case GlobalAction.Back:
- Click();
- return true;
- }
-
- return false;
- }
-
- public bool OnReleased(GlobalAction action) => false;
}
}
}
diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
index ed5fdf9809..0ff3455f00 100644
--- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
@@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Configuration
{
- public abstract class RulesetConfigManager : DatabasedConfigManager, IRulesetConfigManager
- where T : struct
+ public abstract class RulesetConfigManager : DatabasedConfigManager, IRulesetConfigManager
+ where TLookup : struct, Enum
{
protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index e31c963403..1902de5bda 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
@@ -41,10 +41,10 @@ namespace osu.Game.Rulesets.Difficulty
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
- var clock = new StopwatchClock();
- mods.OfType().ForEach(m => m.ApplyToClock(clock));
+ var track = new TrackVirtual(10000);
+ mods.OfType().ForEach(m => m.ApplyToTrack(track));
- return calculate(playableBeatmap, mods, clock.Rate);
+ return calculate(playableBeatmap, mods, track.Rate);
}
///
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
index 9ab81b9580..ac3b817840 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
@@ -3,8 +3,8 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -35,9 +35,9 @@ namespace osu.Game.Rulesets.Difficulty
protected virtual void ApplyMods(Mod[] mods)
{
- var clock = new StopwatchClock();
- mods.OfType().ForEach(m => m.ApplyToClock(clock));
- TimeRate = clock.Rate;
+ var track = new TrackVirtual(10000);
+ mods.OfType().ForEach(m => m.ApplyToTrack(track));
+ TimeRate = track.Rate;
}
public abstract double Calculate(Dictionary categoryDifficulty = null);
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 805fc2b46f..9ac967ef74 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Edit
where TObject : HitObject
{
protected IRulesetConfigManager Config { get; private set; }
- protected EditorBeatmap EditorBeatmap { get; private set; }
+
+ protected new EditorBeatmap EditorBeatmap { get; private set; }
+
protected readonly Ruleset Ruleset;
[Resolved]
@@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Edit
beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
- EditorBeatmap = new EditorBeatmap(playableBeatmap);
+ base.EditorBeatmap = EditorBeatmap = new EditorBeatmap(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject;
EditorBeatmap.StartTimeChanged += UpdateHitObject;
@@ -333,6 +335,11 @@ namespace osu.Game.Rulesets.Edit
///
public abstract IEnumerable HitObjects { get; }
+ ///
+ /// An editor-specific beatmap, exposing mutation events.
+ ///
+ public IEditorBeatmap EditorBeatmap { get; protected set; }
+
///
/// Whether the user's cursor is currently in an area of the that is valid for placement.
///
diff --git a/osu.Game/Rulesets/Mods/IApplicableToClock.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs
similarity index 69%
rename from osu.Game/Rulesets/Mods/IApplicableToClock.cs
rename to osu.Game/Rulesets/Mods/IApplicableToTrack.cs
index e5767b5fbf..4d6d958e82 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToClock.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs
@@ -1,15 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Timing;
+using osu.Framework.Audio.Track;
namespace osu.Game.Rulesets.Mods
{
///
/// An interface for mods that make adjustments to the track.
///
- public interface IApplicableToClock : IApplicableMod
+ public interface IApplicableToTrack : IApplicableMod
{
- void ApplyToClock(IAdjustableClock clock);
+ void ApplyToTrack(Track track);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs
index 7e6d959119..dcb3cb5597 100644
--- a/osu.Game/Rulesets/Mods/ModDaycore.cs
+++ b/osu.Game/Rulesets/Mods/ModDaycore.cs
@@ -1,9 +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 osu.Framework.Audio;
+using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Timing;
namespace osu.Game.Rulesets.Mods
{
@@ -14,12 +13,9 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => FontAwesome.Solid.Question;
public override string Description => "Whoaaaaa...";
- public override void ApplyToClock(IAdjustableClock clock)
+ public override void ApplyToTrack(Track track)
{
- if (clock is IHasPitchAdjust pitchAdjust)
- pitchAdjust.PitchAdjust *= RateAdjust;
- else
- base.ApplyToClock(clock);
+ track.Frequency.Value *= RateAdjust;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index a5e76e32b1..5e685b040e 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -8,7 +8,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModDoubleTime : ModTimeAdjust, IApplicableToClock
+ public abstract class ModDoubleTime : ModTimeAdjust
{
public override string Name => "Double Time";
public override string Acronym => "DT";
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 27369f4c30..d17ddd2253 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -8,7 +8,7 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModHalfTime : ModTimeAdjust, IApplicableToClock
+ public abstract class ModHalfTime : ModTimeAdjust
{
public override string Name => "Half Time";
public override string Acronym => "HT";
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index dc0fc33088..a4f1ef5a72 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -1,9 +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 osu.Framework.Audio;
+using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Timing;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
@@ -15,12 +14,9 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage Icon => OsuIcon.ModNightcore;
public override string Description => "Uguuuuuuuu...";
- public override void ApplyToClock(IAdjustableClock clock)
+ public override void ApplyToTrack(Track track)
{
- if (clock is IHasPitchAdjust pitchAdjust)
- pitchAdjust.PitchAdjust *= RateAdjust;
- else
- base.ApplyToClock(clock);
+ track.Frequency.Value *= RateAdjust;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeAdjust.cs b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs
index 513883f552..7d0cc2a7c3 100644
--- a/osu.Game/Rulesets/Mods/ModTimeAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs
@@ -2,23 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Audio;
-using osu.Framework.Timing;
+using osu.Framework.Audio.Track;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModTimeAdjust : Mod
+ public abstract class ModTimeAdjust : Mod, IApplicableToTrack
{
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
protected abstract double RateAdjust { get; }
- public virtual void ApplyToClock(IAdjustableClock clock)
+ public virtual void ApplyToTrack(Track track)
{
- if (clock is IHasTempoAdjust tempo)
- tempo.TempoAdjust *= RateAdjust;
- else
- clock.Rate *= RateAdjust;
+ track.Tempo.Value *= RateAdjust;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index e231225e3c..839b2ae36e 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -3,15 +3,14 @@
using System;
using System.Linq;
-using osu.Framework.Audio;
-using osu.Framework.Timing;
+using osu.Framework.Audio.Track;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap
+ public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToTrack, IApplicableToBeatmap
{
///
/// The point in the beatmap at which the final ramping rate should be reached.
@@ -24,11 +23,11 @@ namespace osu.Game.Rulesets.Mods
private double finalRateTime;
private double beginRampTime;
- private IAdjustableClock clock;
+ private Track track;
- public virtual void ApplyToClock(IAdjustableClock clock)
+ public virtual void ApplyToTrack(Track track)
{
- this.clock = clock;
+ this.track = track;
lastAdjust = 1;
@@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mods
public virtual void Update(Playfield playfield)
{
- applyAdjustment((clock.CurrentTime - beginRampTime) / finalRateTime);
+ applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime);
}
private double lastAdjust = 1;
@@ -59,23 +58,8 @@ namespace osu.Game.Rulesets.Mods
{
double adjust = 1 + (Math.Sign(FinalRateAdjustment) * Math.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment));
- switch (clock)
- {
- case IHasPitchAdjust pitch:
- pitch.PitchAdjust /= lastAdjust;
- pitch.PitchAdjust *= adjust;
- break;
-
- case IHasTempoAdjust tempo:
- tempo.TempoAdjust /= lastAdjust;
- tempo.TempoAdjust *= adjust;
- break;
-
- default:
- clock.Rate /= lastAdjust;
- clock.Rate *= adjust;
- break;
- }
+ track.Tempo.Value /= lastAdjust;
+ track.Tempo.Value *= adjust;
lastAdjust = adjust;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
index 545cfe07f8..43e8d01297 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
@@ -3,7 +3,6 @@
using osuTK;
using osu.Game.Audio;
-using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Catch
@@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List> nodeSamples)
{
newCombo |= forceNewCombo;
@@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
X = position.X,
NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
- Path = new SliderPath(pathType, controlPoints, length),
+ Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 5348ff1f02..b5b1e26486 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -115,12 +115,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
}
- // osu-stable special-cased colinear perfect curves to a CurveType.Linear
- static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
-
- if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points))
- pathType = PathType.Linear;
-
int repeatCount = Parsing.ParseInt(split[6]);
if (repeatCount > 9000)
@@ -187,7 +181,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
- result = CreateSlider(pos, combo, comboOffset, points, length, pathType, repeatCount, nodeSamples);
+ result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
// The samples are played when the slider ends, which is the last node
result.Samples = nodeSamples[nodeSamples.Count - 1];
@@ -259,6 +253,44 @@ namespace osu.Game.Rulesets.Objects.Legacy
bankInfo.Filename = split.Length > 4 ? split[4] : null;
}
+ private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
+ {
+ if (type == PathType.PerfectCurve)
+ {
+ if (vertices.Length != 3)
+ type = PathType.Bezier;
+ else if (isLinear(vertices))
+ {
+ // osu-stable special-cased colinear perfect curves to a linear path
+ type = PathType.Linear;
+ }
+ }
+
+ var points = new List(vertices.Length)
+ {
+ new PathControlPoint
+ {
+ Position = { Value = vertices[0] },
+ Type = { Value = type }
+ }
+ };
+
+ for (int i = 1; i < vertices.Length; i++)
+ {
+ if (vertices[i] == vertices[i - 1])
+ {
+ points[points.Count - 1].Type.Value = type;
+ continue;
+ }
+
+ points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
+ }
+
+ return points.ToArray();
+
+ static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
+ }
+
///
/// Creates a legacy Hit-type hit object.
///
@@ -276,11 +308,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// When starting a new combo, the offset of the new combo relative to the current one.
/// The slider control points.
/// The slider length.
- /// The slider curve type.
/// The slider repeat count.
/// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.
/// The hit object.
- protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
+ protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List> nodeSamples);
///
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
index 8012b4230f..f94c4aaa75 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
@@ -3,7 +3,6 @@
using osuTK;
using osu.Game.Audio;
-using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
@@ -26,13 +25,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List> nodeSamples)
{
return new ConvertSlider
{
X = position.X,
- Path = new SliderPath(pathType, controlPoints, length),
+ Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
index 99872e630d..b95ec703b6 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osuTK;
-using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using osu.Game.Audio;
@@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List> nodeSamples)
{
newCombo |= forceNewCombo;
@@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
Position = position,
NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
- Path = new SliderPath(pathType, controlPoints, length),
+ Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
index 9dc0c01932..db65a61c90 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osuTK;
-using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using osu.Game.Audio;
@@ -23,12 +22,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
return new ConvertHit();
}
- protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
+ protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
List> nodeSamples)
{
return new ConvertSlider
{
- Path = new SliderPath(pathType, controlPoints, length),
+ Path = new SliderPath(controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs
new file mode 100644
index 0000000000..0336f94313
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/PathControlPoint.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 System;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Objects
+{
+ public class PathControlPoint : IEquatable
+ {
+ ///
+ /// The position of this .
+ ///
+ public readonly Bindable Position = new Bindable();
+
+ ///
+ /// The type of path segment starting at this .
+ /// If null, this will be a part of the previous path segment.
+ ///
+ public readonly Bindable Type = new Bindable();
+
+ ///
+ /// Invoked when any property of this is changed.
+ ///
+ internal event Action Changed;
+
+ ///
+ /// Creates a new .
+ ///
+ public PathControlPoint()
+ {
+ Position.ValueChanged += _ => Changed?.Invoke();
+ Type.ValueChanged += _ => Changed?.Invoke();
+ }
+
+ ///
+ /// Creates a new with a provided position and type.
+ ///
+ /// The initial position.
+ /// The initial type.
+ public PathControlPoint(Vector2 position, PathType? type = null)
+ : this()
+ {
+ Position.Value = position;
+ Type.Value = type;
+ }
+
+ public bool Equals(PathControlPoint other) => Position.Value == other?.Position.Value && Type.Value == other.Type.Value;
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs
index ae6aad5b9c..86deba3b93 100644
--- a/osu.Game/Rulesets/Objects/SliderPath.cs
+++ b/osu.Game/Rulesets/Objects/SliderPath.cs
@@ -1,68 +1,86 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// 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 Newtonsoft.Json;
+using osu.Framework.Bindables;
+using osu.Framework.Caching;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects
{
- public struct SliderPath : IEquatable
+ public class SliderPath
{
+ ///
+ /// The current version of this . Updated when any change to the path occurs.
+ ///
+ [JsonIgnore]
+ public IBindable Version => version;
+
+ private readonly Bindable version = new Bindable();
+
///
/// The user-set distance of the path. If non-null, will match this value,
/// and the path will be shortened/lengthened to match this length.
///
- public readonly double? ExpectedDistance;
-
- ///
- /// The type of path.
- ///
- public readonly PathType Type;
-
- [JsonProperty]
- private Vector2[] controlPoints;
-
- private List calculatedPath;
- private List cumulativeLength;
-
- private bool isInitialised;
-
- ///
- /// Creates a new .
- ///
- /// The type of path.
- /// The control points of the path.
- /// A user-set distance of the path that may be shorter or longer than the true distance between all
- /// . The path will be shortened/lengthened to match this length.
- /// If null, the path will use the true distance between all .
- [JsonConstructor]
- public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
- {
- this = default;
- this.controlPoints = controlPoints;
-
- Type = type;
- ExpectedDistance = expectedDistance;
-
- ensureInitialised();
- }
+ public readonly Bindable ExpectedDistance = new Bindable();
///
/// The control points of the path.
///
- [JsonIgnore]
- public ReadOnlySpan ControlPoints
+ public readonly BindableList ControlPoints = new BindableList();
+
+ private readonly List calculatedPath = new List();
+ private readonly List cumulativeLength = new List();
+ private readonly Cached pathCache = new Cached();
+
+ private double calculatedLength;
+
+ ///
+ /// Creates a new .
+ ///
+ public SliderPath()
{
- get
+ ExpectedDistance.ValueChanged += _ => invalidate();
+
+ ControlPoints.ItemsAdded += items =>
{
- ensureInitialised();
- return controlPoints.AsSpan();
- }
+ foreach (var c in items)
+ c.Changed += invalidate;
+
+ invalidate();
+ };
+
+ ControlPoints.ItemsRemoved += items =>
+ {
+ foreach (var c in items)
+ c.Changed -= invalidate;
+
+ invalidate();
+ };
+ }
+
+ ///
+ /// Creates a new initialised with a list of control points.
+ ///
+ /// An optional set of s to initialise the path with.
+ /// A user-set distance of the path that may be shorter or longer than the true distance between all control points.
+ /// The path will be shortened/lengthened to match this length. If null, the path will use the true distance between all control points.
+ [JsonConstructor]
+ public SliderPath(PathControlPoint[] controlPoints, double? expectedDistance = null)
+ : this()
+ {
+ ControlPoints.AddRange(controlPoints);
+ ExpectedDistance.Value = expectedDistance;
+ }
+
+ public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
+ : this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? (PathType?)type : null)).ToArray(), expectedDistance)
+ {
}
///
@@ -73,11 +91,23 @@ namespace osu.Game.Rulesets.Objects
{
get
{
- ensureInitialised();
+ ensureValid();
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1];
}
}
+ ///
+ /// The distance of the path prior to lengthening/shortening to account for .
+ ///
+ public double CalculatedDistance
+ {
+ get
+ {
+ ensureValid();
+ return calculatedLength;
+ }
+ }
+
///
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list.
@@ -87,7 +117,7 @@ namespace osu.Game.Rulesets.Objects
/// End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).
public void GetPathToProgress(List path, double p0, double p1)
{
- ensureInitialised();
+ ensureValid();
double d0 = progressToDistance(p0);
double d1 = progressToDistance(p1);
@@ -116,40 +146,73 @@ namespace osu.Game.Rulesets.Objects
///
public Vector2 PositionAt(double progress)
{
- ensureInitialised();
+ ensureValid();
double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d);
}
- private void ensureInitialised()
+ private void invalidate()
{
- if (isInitialised)
- return;
-
- isInitialised = true;
-
- controlPoints ??= Array.Empty();
- calculatedPath = new List();
- cumulativeLength = new List();
-
- calculatePath();
- calculateCumulativeLength();
+ pathCache.Invalidate();
+ version.Value++;
}
- private List calculateSubpath(ReadOnlySpan subControlPoints)
+ private void ensureValid()
{
- switch (Type)
+ if (pathCache.IsValid)
+ return;
+
+ calculatePath();
+ calculateLength();
+
+ pathCache.Validate();
+ }
+
+ private void calculatePath()
+ {
+ calculatedPath.Clear();
+
+ if (ControlPoints.Count == 0)
+ return;
+
+ Vector2[] vertices = new Vector2[ControlPoints.Count];
+ for (int i = 0; i < ControlPoints.Count; i++)
+ vertices[i] = ControlPoints[i].Position.Value;
+
+ int start = 0;
+
+ for (int i = 0; i < ControlPoints.Count; i++)
+ {
+ if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1)
+ continue;
+
+ // The current vertex ends the segment
+ var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
+ var segmentType = ControlPoints[start].Type.Value ?? PathType.Linear;
+
+ foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType))
+ {
+ if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
+ calculatedPath.Add(t);
+ }
+
+ // Start the new segment at the current vertex
+ start = i;
+ }
+ }
+
+ private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type)
+ {
+ switch (type)
{
case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints);
case PathType.PerfectCurve:
- //we can only use CircularArc iff we have exactly three control points and no dissection.
- if (ControlPoints.Length != 3 || subControlPoints.Length != 3)
+ if (subControlPoints.Length != 3)
break;
- // Here we have exactly 3 control points. Attempt to fit a circular arc.
List subpath = PathApproximator.ApproximateCircularArc(subControlPoints);
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
@@ -165,74 +228,49 @@ namespace osu.Game.Rulesets.Objects
return PathApproximator.ApproximateBezier(subControlPoints);
}
- private void calculatePath()
+ private void calculateLength()
{
- calculatedPath.Clear();
-
- // Sliders may consist of various subpaths separated by two consecutive vertices
- // with the same position. The following loop parses these subpaths and computes
- // their shape independently, consecutively appending them to calculatedPath.
-
- int start = 0;
- int end = 0;
-
- for (int i = 0; i < ControlPoints.Length; ++i)
- {
- end++;
-
- if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
- {
- ReadOnlySpan cpSpan = ControlPoints.Slice(start, end - start);
-
- foreach (Vector2 t in calculateSubpath(cpSpan))
- {
- if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
- calculatedPath.Add(t);
- }
-
- start = end;
- }
- }
- }
-
- private void calculateCumulativeLength()
- {
- double l = 0;
-
+ calculatedLength = 0;
cumulativeLength.Clear();
- cumulativeLength.Add(l);
+ cumulativeLength.Add(0);
- for (int i = 0; i < calculatedPath.Count - 1; ++i)
+ for (int i = 0; i < calculatedPath.Count - 1; i++)
{
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
- double d = diff.Length;
-
- // Shorted slider paths that are too long compared to the expected distance
- if (ExpectedDistance.HasValue && ExpectedDistance - l < d)
- {
- calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d);
- calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
-
- l = ExpectedDistance.Value;
- cumulativeLength.Add(l);
- break;
- }
-
- l += d;
- cumulativeLength.Add(l);
+ calculatedLength += diff.Length;
+ cumulativeLength.Add(calculatedLength);
}
- // Lengthen slider paths that are too short compared to the expected distance
- if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1)
+ if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
{
- Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
- double d = diff.Length;
+ // The last length is always incorrect
+ cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
- if (d <= 0)
+ int pathEndIndex = calculatedPath.Count - 1;
+
+ if (calculatedLength > expectedDistance)
+ {
+ // The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
+ while (cumulativeLength.Count > 0 && cumulativeLength[cumulativeLength.Count - 1] >= expectedDistance)
+ {
+ cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
+ calculatedPath.RemoveAt(pathEndIndex--);
+ }
+ }
+
+ if (pathEndIndex <= 0)
+ {
+ // The expected distance is negative or zero
+ // TODO: Perhaps negative path lengths should be disallowed altogether
+ cumulativeLength.Add(0);
return;
+ }
- calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d);
- cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value;
+ // The direction of the segment to shorten or lengthen
+ Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized();
+
+ calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[cumulativeLength.Count - 1]);
+ cumulativeLength.Add(expectedDistance);
}
}
@@ -272,7 +310,5 @@ namespace osu.Game.Rulesets.Objects
double w = (d - d0) / (d1 - d0);
return p0 + (p1 - p0) * (float)w;
}
-
- public bool Equals(SliderPath other) => ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance == other.ExpectedDistance && Type == other.Type;
}
}
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 96275c1274..a856974292 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -511,15 +511,19 @@ namespace osu.Game.Rulesets.UI
public IEnumerable GetAvailableResources() => throw new NotImplementedException();
- public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException();
+ public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException();
- public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException();
+ public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException();
- public BindableDouble Volume => throw new NotImplementedException();
+ public BindableNumber Volume => throw new NotImplementedException();
- public BindableDouble Balance => throw new NotImplementedException();
+ public BindableNumber Balance => throw new NotImplementedException();
- public BindableDouble Frequency => throw new NotImplementedException();
+ public BindableNumber Frequency => throw new NotImplementedException();
+
+ public BindableNumber Tempo => throw new NotImplementedException();
+
+ public IBindable GetAggregate(AdjustableProperty type) => throw new NotImplementedException();
public IBindable AggregateVolume => throw new NotImplementedException();
@@ -527,6 +531,8 @@ namespace osu.Game.Rulesets.UI
public IBindable AggregateFrequency => throw new NotImplementedException();
+ public IBindable AggregateTempo => throw new NotImplementedException();
+
public int PlaybackConcurrency
{
get => throw new NotImplementedException();
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
index 26d9614631..7706e33179 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
@@ -14,12 +14,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
///
/// Represents a part of the summary timeline..
///
- public abstract class TimelinePart : CompositeDrawable
+ public abstract class TimelinePart : Container
{
protected readonly IBindable Beatmap = new Bindable();
private readonly Container timeline;
+ protected override Container Content => timeline;
+
protected TimelinePart()
{
AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both });
@@ -50,8 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1);
}
- protected void Add(Drawable visualisation) => timeline.Add(visualisation);
-
protected virtual void LoadBeatmap(WorkingBeatmap beatmap)
{
timeline.Clear();
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 748c9e2ba3..b4f3b1f610 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
this.adjustableClock = adjustableClock;
- Child = waveform = new WaveformGraph
+ Add(waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Blue.Opacity(0.2f),
@@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
Depth = float.MaxValue
- };
+ });
// We don't want the centre marker to scroll
AddInternal(new CentreMarker());
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
index 863a120fc3..02e5db306d 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -11,17 +12,18 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class TimelineArea : CompositeDrawable
+ public class TimelineArea : Container
{
- private readonly Timeline timeline;
+ private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both };
- public TimelineArea()
+ protected override Container Content => timeline;
+
+ [BackgroundDependencyLoader]
+ private void load()
{
Masking = true;
CornerRadius = 5;
- OsuCheckbox hitObjectsCheckbox;
- OsuCheckbox hitSoundsCheckbox;
OsuCheckbox waveformCheckbox;
InternalChildren = new Drawable[]
@@ -60,8 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Spacing = new Vector2(0, 4),
Children = new[]
{
- hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hit objects" },
- hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hit sounds" },
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
}
}
@@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
},
- timeline = new Timeline { RelativeSizeAxes = Axes.Both }
+ timeline
},
},
ColumnDimensions = new[]
@@ -119,8 +119,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
- hitObjectsCheckbox.Current.Value = true;
- hitSoundsCheckbox.Current.Value = true;
waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs
new file mode 100644
index 0000000000..db4aca75e5
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs
@@ -0,0 +1,108 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Compose.Components.Timeline
+{
+ internal class TimelineHitObjectDisplay : TimelinePart
+ {
+ private IEditorBeatmap beatmap { get; }
+
+ public TimelineHitObjectDisplay(IEditorBeatmap beatmap)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ this.beatmap = beatmap;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ foreach (var h in beatmap.HitObjects)
+ add(h);
+
+ beatmap.HitObjectAdded += add;
+ beatmap.HitObjectRemoved += remove;
+ beatmap.StartTimeChanged += h =>
+ {
+ remove(h);
+ add(h);
+ };
+ }
+
+ private void remove(HitObject h)
+ {
+ foreach (var d in Children.OfType().Where(c => c.HitObject == h))
+ d.Expire();
+ }
+
+ private void add(HitObject h)
+ {
+ var yOffset = Children.Count(d => d.X == h.StartTime);
+
+ Add(new TimelineHitObjectRepresentation(h) { Y = -yOffset * TimelineHitObjectRepresentation.THICKNESS });
+ }
+
+ private class TimelineHitObjectRepresentation : CompositeDrawable
+ {
+ public const float THICKNESS = 3;
+
+ public readonly HitObject HitObject;
+
+ public TimelineHitObjectRepresentation(HitObject hitObject)
+ {
+ HitObject = hitObject;
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+
+ Width = (float)(hitObject.GetEndTime() - hitObject.StartTime);
+
+ X = (float)hitObject.StartTime;
+
+ RelativePositionAxes = Axes.X;
+ RelativeSizeAxes = Axes.X;
+
+ if (hitObject is IHasEndTime)
+ {
+ AddInternal(new Container
+ {
+ CornerRadius = 2,
+ Masking = true,
+ Size = new Vector2(1, THICKNESS),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativePositionAxes = Axes.X,
+ RelativeSizeAxes = Axes.X,
+ Colour = Color4.Black,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ });
+ }
+
+ AddInternal(new Circle
+ {
+ Size = new Vector2(16),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.X,
+ AlwaysPresent = true,
+ Colour = Color4.White,
+ BorderColour = Color4.Black,
+ BorderThickness = THICKNESS,
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 2e9094ebe6..6984716a2c 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -3,32 +3,35 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit.Compose
{
public class ComposeScreen : EditorScreenWithTimeline
{
+ private HitObjectComposer composer;
+
protected override Drawable CreateMainContent()
{
var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();
+ composer = ruleset?.CreateHitObjectComposer();
- var composer = ruleset?.CreateHitObjectComposer();
+ if (ruleset == null || composer == null)
+ return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
- if (composer != null)
- {
- var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
+ var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
- // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
- // full access to all skin sources.
- var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
+ // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
+ // full access to all skin sources.
+ var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
- // load the skinning hierarchy first.
- // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
- return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(ruleset.CreateHitObjectComposer()));
- }
-
- return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
+ // load the skinning hierarchy first.
+ // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
+ return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer));
}
+
+ protected override Drawable CreateTimelineContent() => new TimelineHitObjectDisplay(composer.EditorBeatmap);
}
}
diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
index 752356e8c4..aa8d99b517 100644
--- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
+++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
+ private TimelineArea timelineArea;
+
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{
@@ -64,7 +66,7 @@ namespace osu.Game.Screens.Edit
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
- Child = CreateTimeline()
+ Child = timelineArea = CreateTimelineArea()
},
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
},
@@ -97,11 +99,15 @@ namespace osu.Game.Screens.Edit
{
mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint);
+
+ LoadComponentAsync(CreateTimelineContent(), timelineArea.Add);
});
}
protected abstract Drawable CreateMainContent();
- protected virtual Drawable CreateTimeline() => new TimelineArea { RelativeSizeAxes = Axes.Both };
+ protected virtual Drawable CreateTimelineContent() => new Container();
+
+ protected TimelineArea CreateTimelineArea() => new TimelineArea { RelativeSizeAxes = Axes.Both };
}
}
diff --git a/osu.Game/Screens/Edit/IEditorBeatmap.cs b/osu.Game/Screens/Edit/IEditorBeatmap.cs
index 2f250ba446..3e3418ef79 100644
--- a/osu.Game/Screens/Edit/IEditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/IEditorBeatmap.cs
@@ -23,6 +23,11 @@ namespace osu.Game.Screens.Edit
/// Invoked when a is removed from this .
///
event Action HitObjectRemoved;
+
+ ///
+ /// Invoked when the start time of a in this was changed.
+ ///
+ event Action StartTimeChanged;
}
///
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index ff78d85bf0..2cc03ae453 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -28,9 +28,9 @@ namespace osu.Game.Screens.Play
private readonly IReadOnlyList mods;
///
- /// The original source (usually a 's track).
+ /// The 's track.
///
- private IAdjustableClock sourceClock;
+ private Track track;
public readonly BindableBool IsPaused = new BindableBool();
@@ -72,8 +72,8 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both;
- sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
- (sourceClock as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
+ track = beatmap.Track;
+ track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
@@ -127,11 +127,11 @@ namespace osu.Game.Screens.Play
{
Task.Run(() =>
{
- sourceClock.Reset();
+ track.Reset();
Schedule(() =>
{
- adjustableClock.ChangeSource(sourceClock);
+ adjustableClock.ChangeSource(track);
updateRate();
if (!IsPaused.Value)
@@ -197,13 +197,13 @@ namespace osu.Game.Screens.Play
///
public void StopUsingBeatmapClock()
{
- if (sourceClock != beatmap.Track)
+ if (track != beatmap.Track)
return;
removeSourceClockAdjustments();
- sourceClock = new TrackVirtual(beatmap.Track.Length);
- adjustableClock.ChangeSource(sourceClock);
+ track = new TrackVirtual(beatmap.Track.Length);
+ adjustableClock.ChangeSource(track);
}
protected override void Update()
@@ -214,19 +214,19 @@ namespace osu.Game.Screens.Play
base.Update();
}
+ private bool speedAdjustmentsApplied;
+
private void updateRate()
{
- if (sourceClock == null) return;
+ if (track == null) return;
- sourceClock.ResetSpeedAdjustments();
+ speedAdjustmentsApplied = true;
+ track.ResetSpeedAdjustments();
- if (sourceClock is IHasTempoAdjust tempo)
- tempo.TempoAdjust = UserPlaybackRate.Value;
- else
- sourceClock.Rate = UserPlaybackRate.Value;
+ track.Tempo.Value = UserPlaybackRate.Value;
- foreach (var mod in mods.OfType())
- mod.ApplyToClock(sourceClock);
+ foreach (var mod in mods.OfType())
+ mod.ApplyToTrack(track);
}
protected override void Dispose(bool isDisposing)
@@ -234,13 +234,18 @@ namespace osu.Game.Screens.Play
base.Dispose(isDisposing);
removeSourceClockAdjustments();
- sourceClock = null;
+ track = null;
}
private void removeSourceClockAdjustments()
{
- sourceClock.ResetSpeedAdjustments();
- (sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
+ if (speedAdjustmentsApplied)
+ {
+ track.ResetSpeedAdjustments();
+ speedAdjustmentsApplied = false;
+ }
+
+ track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
}
}
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 87d902b547..57021dfc68 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -55,7 +55,9 @@ namespace osu.Game.Screens.Play
protected override bool PlayResumeSound => false;
- private Task loadTask;
+ protected Task LoadTask { get; private set; }
+
+ protected Task DisposalTask { get; private set; }
private InputManager inputManager;
private IdleTracker idleTracker;
@@ -159,7 +161,7 @@ namespace osu.Game.Screens.Play
player.RestartCount = restartCount;
player.RestartRequested = restartRequested;
- loadTask = LoadComponentAsync(player, _ => info.Loading = false);
+ LoadTask = LoadComponentAsync(player, _ => info.Loading = false);
}
private void contentIn()
@@ -250,7 +252,7 @@ namespace osu.Game.Screens.Play
{
if (!this.IsCurrentScreen()) return;
- loadTask = null;
+ LoadTask = null;
//By default, we want to load the player and never be returned to.
//Note that this may change if the player we load requested a re-run.
@@ -301,7 +303,7 @@ namespace osu.Game.Screens.Play
if (isDisposing)
{
// if the player never got pushed, we should explicitly dispose it.
- loadTask?.ContinueWith(_ => player.Dispose());
+ DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
}
}
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index abcb1f2171..e3ad76ac35 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Screens.Select
}
public struct OptionalRange : IEquatable>
- where T : struct, IComparable
+ where T : struct
{
public bool HasFilter => Max != null || Min != null;
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index ffe1258168..89afc729fe 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select
}
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value)
- where T : struct, IComparable
+ where T : struct
{
switch (op)
{
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index a52edb70db..8f7ad2022d 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -262,8 +262,10 @@ namespace osu.Game.Screens.Select
protected virtual void ApplyFilterToCarousel(FilterCriteria criteria)
{
- if (this.IsCurrentScreen())
- Carousel.Filter(criteria);
+ // if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter).
+ bool shouldDebounce = this.IsCurrentScreen();
+
+ Schedule(() => Carousel.Filter(criteria, shouldDebounce));
}
private DependencyContainer dependencies;
@@ -437,8 +439,6 @@ namespace osu.Game.Screens.Select
{
base.OnEntering(last);
- Carousel.Filter(FilterControl.CreateCriteria(), false);
-
this.FadeInFromZero(250);
FilterControl.Activate();
}
diff --git a/osu.Game/Skinning/SkinConfigManager.cs b/osu.Game/Skinning/SkinConfigManager.cs
index 896444d1d2..682138a2e9 100644
--- a/osu.Game/Skinning/SkinConfigManager.cs
+++ b/osu.Game/Skinning/SkinConfigManager.cs
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Configuration;
namespace osu.Game.Skinning
{
- public class SkinConfigManager : ConfigManager where T : struct
+ public class SkinConfigManager : ConfigManager where TLookup : struct, Enum
{
protected override void PerformLoad()
{
diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs
index 9ca5d60cb0..fda031e6cb 100644
--- a/osu.Game/Skinning/SkinnableDrawable.cs
+++ b/osu.Game/Skinning/SkinnableDrawable.cs
@@ -29,13 +29,13 @@ namespace osu.Game.Skinning
/// A function to create the default skin implementation of this element.
/// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present.
/// How (if at all) the should be resize to fit within our own bounds.
- public SkinnableDrawable(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit)
+ public SkinnableDrawable(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: this(component, allowFallback, confineMode)
{
createDefault = defaultImplementation;
}
- protected SkinnableDrawable(ISkinComponent component, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit)
+ protected SkinnableDrawable(ISkinComponent component, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(allowFallback)
{
this.component = component;
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index e225bfc490..5352928ec6 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Skinning
[Resolved]
private TextureStore textures { get; set; }
- public SkinnableSprite(string textureName, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit)
+ public SkinnableSprite(string textureName, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(new SpriteComponent(textureName), allowFallback, confineMode)
{
}
diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs
index e72f9c9811..567dd348e1 100644
--- a/osu.Game/Skinning/SkinnableSpriteText.cs
+++ b/osu.Game/Skinning/SkinnableSpriteText.cs
@@ -8,7 +8,7 @@ namespace osu.Game.Skinning
{
public class SkinnableSpriteText : SkinnableDrawable, IHasText
{
- public SkinnableSpriteText(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit)
+ public SkinnableSpriteText(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(component, defaultImplementation, allowFallback, confineMode)
{
}
diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs
index 9e12de5833..1c4cdde22d 100644
--- a/osu.Game/Storyboards/Drawables/IFlippable.cs
+++ b/osu.Game/Storyboards/Drawables/IFlippable.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Storyboards.Drawables
///
/// A to which further transforms can be added.
public static TransformSequence TransformFlipH(this T flippable, bool newValue, double delay = 0)
- where T : IFlippable
+ where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay));
///
@@ -49,7 +49,7 @@ namespace osu.Game.Storyboards.Drawables
///
/// A to which further transforms can be added.
public static TransformSequence TransformFlipV(this T flippable, bool newValue, double delay = 0)
- where T : IFlippable
+ where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay));
}
}
diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
index 921a1d9789..ad24ffc7b8 100644
--- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
+++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual
base.Update();
// note that this will override any mod rate application
- Beatmap.Value.Track.TempoAdjust = Clock.Rate;
+ Beatmap.Value.Track.Tempo.Value = Clock.Rate;
}
}
}
diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs
index 23f45e0d0f..707aa61283 100644
--- a/osu.Game/Tests/Visual/ScreenTestScene.cs
+++ b/osu.Game/Tests/Visual/ScreenTestScene.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual
///
public abstract class ScreenTestScene : ManualInputManagerTestScene
{
- private readonly OsuScreenStack stack;
+ protected readonly OsuScreenStack Stack;
private readonly Container content;
@@ -22,16 +22,16 @@ namespace osu.Game.Tests.Visual
{
base.Content.AddRange(new Drawable[]
{
- stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
+ Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
protected void LoadScreen(OsuScreen screen)
{
- if (stack.CurrentScreen != null)
- stack.Exit();
- stack.Push(screen);
+ if (Stack.CurrentScreen != null)
+ Stack.Exit();
+ Stack.Push(screen);
}
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 086359ee41..530d62f583 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -23,10 +23,10 @@
-
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 0eb8d98a63..fb753b8c6f 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -74,7 +74,7 @@
-
+
@@ -82,11 +82,11 @@
-
+
-
+