diff --git a/osu.Android.props b/osu.Android.props
index 8b31be3f12..d1860acbf9 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -62,6 +62,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 732231b0d9..9cdf045b5b 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit
editorClock = clock;
}
- public override void HandleMovement(MoveSelectionEvent moveEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Edit
performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent);
- base.HandleMovement(moveEvent);
+ return true;
}
///
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
new file mode 100644
index 0000000000..94ca2d4cd1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -0,0 +1,230 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneFollowPoints : OsuTestScene
+ {
+ private Container hitObjectContainer;
+ private FollowPointRenderer followPointRenderer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
+ followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
+ };
+ });
+
+ [Test]
+ public void TestAddObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestAddMultipleObjects()
+ {
+ addMultipleObjectsStep();
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveEndObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(4));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveStartObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveMiddleObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(2));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestMoveObject()
+ {
+ addMultipleObjectsStep();
+
+ AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
+
+ assertGroups();
+ }
+
+ [TestCase(0, 0)] // Start -> Start
+ [TestCase(0, 2)] // Start -> Middle
+ [TestCase(0, 5)] // Start -> End
+ [TestCase(2, 0)] // Middle -> Start
+ [TestCase(1, 3)] // Middle -> Middle (forwards)
+ [TestCase(3, 1)] // Middle -> Middle (backwards)
+ [TestCase(4, 0)] // End -> Start
+ [TestCase(4, 2)] // End -> Middle
+ [TestCase(4, 4)] // End -> End
+ public void TestReorderObjects(int startIndex, int endIndex)
+ {
+ addMultipleObjectsStep();
+
+ reorderObjectStep(startIndex, endIndex);
+
+ assertGroups();
+ }
+
+ private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
+ {
+ new HitCircle { Position = new Vector2(100, 100) },
+ new HitCircle { Position = new Vector2(200, 200) },
+ new HitCircle { Position = new Vector2(300, 300) },
+ new HitCircle { Position = new Vector2(400, 400) },
+ new HitCircle { Position = new Vector2(500, 500) },
+ });
+
+ private void addObjectsStep(Func ctorFunc)
+ {
+ AddStep("add hitobjects", () =>
+ {
+ var objects = ctorFunc();
+
+ for (int i = 0; i < objects.Length; i++)
+ {
+ objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
+ objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ DrawableOsuHitObject drawableObject = null;
+
+ switch (objects[i])
+ {
+ case HitCircle circle:
+ drawableObject = new DrawableHitCircle(circle);
+ break;
+
+ case Slider slider:
+ drawableObject = new DrawableSlider(slider);
+ break;
+
+ case Spinner spinner:
+ drawableObject = new DrawableSpinner(spinner);
+ break;
+ }
+
+ hitObjectContainer.Add(drawableObject);
+ followPointRenderer.AddFollowPoints(drawableObject);
+ }
+ });
+ }
+
+ private void removeObjectStep(Func getFunc)
+ {
+ AddStep("remove hitobject", () =>
+ {
+ var drawableObject = getFunc?.Invoke();
+
+ hitObjectContainer.Remove(drawableObject);
+ followPointRenderer.RemoveFollowPoints(drawableObject);
+ });
+ }
+
+ private void reorderObjectStep(int startIndex, int endIndex)
+ {
+ AddStep($"move object {startIndex} to {endIndex}", () =>
+ {
+ DrawableOsuHitObject toReorder = getObject(startIndex);
+
+ double targetTime;
+ if (endIndex < hitObjectContainer.Count)
+ targetTime = getObject(endIndex).HitObject.StartTime - 1;
+ else
+ targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
+
+ hitObjectContainer.Remove(toReorder);
+ toReorder.HitObject.StartTime = targetTime;
+ hitObjectContainer.Add(toReorder);
+ });
+ }
+
+ private void assertGroups()
+ {
+ AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
+ AddAssert("group endpoints are correct", () =>
+ {
+ for (int i = 0; i < hitObjectContainer.Count; i++)
+ {
+ DrawableOsuHitObject expectedStart = getObject(i);
+ DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
+
+ if (getGroup(i).Start != expectedStart)
+ throw new AssertionException($"Object {i} expected to be the start of group {i}.");
+
+ if (getGroup(i).End != expectedEnd)
+ throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
+ }
+
+ return true;
+ });
+ }
+
+ private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
+
+ private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
+
+ private class TestHitObjectContainer : Container
+ {
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ var osuX = (DrawableOsuHitObject)x;
+ var osuY = (DrawableOsuHitObject)y;
+
+ int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
+
+ if (compare == 0)
+ return base.Compare(x, y);
+
+ return compare;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
index a9a6097182..eff4d919b0 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
- private readonly TestOsuDistanceSnapGrid grid;
+ private TestOsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
+ editorBeatmap.ControlPointInfo.Clear();
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
Children = new Drawable[]
{
@@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
- }
-
- [SetUp]
- public void Setup() => Schedule(() =>
- {
- editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
- editorBeatmap.ControlPointInfo.Clear();
- editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
});
[TestCase(1)]
@@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
assertSnappedDistance((float)beat_length * 2);
}
+ [Test]
+ public void TestLimitedDistance()
+ {
+ AddStep("create limited grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
+ new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
+ };
+ });
+
+ AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
+ assertSnappedDistance((float)beat_length * 2);
+ }
+
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
{
Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
@@ -152,8 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public new float DistanceSpacing => base.DistanceSpacing;
- public TestOsuDistanceSnapGrid(OsuHitObject hitObject)
- : base(hitObject)
+ public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
+ : base(hitObject, nextHitObject)
{
}
}
@@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
- public float DurationToDistance(double referenceTime, double duration) => 0;
+ public float DurationToDistance(double referenceTime, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => 0;
+ public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
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 e2ea6a12d7..0353ba241c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Container marker;
private readonly Drawable markerRing;
- private bool isClicked;
-
[Resolved]
private OsuColour colours { get; set; }
@@ -101,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
markerRing.Alpha = IsSelected.Value ? 1 : 0;
Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
- if (IsHovered || isClicked || IsSelected.Value)
+ if (IsHovered || IsSelected.Value)
colour = Color4.White;
marker.Colour = colour;
}
@@ -127,21 +125,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnMouseDown(MouseDownEvent e)
{
- isClicked = true;
- return true;
+ if (RequestSelection != null)
+ {
+ RequestSelection.Invoke(Index);
+ return true;
+ }
+
+ return false;
}
- protected override bool OnMouseUp(MouseUpEvent e)
- {
- isClicked = false;
- return true;
- }
+ protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
- protected override bool OnClick(ClickEvent e)
- {
- RequestSelection?.Invoke(Index);
- return true;
- }
+ protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) => true;
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 b70c11427a..6962736157 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -2,27 +2,36 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class PathControlPointVisualiser : CompositeDrawable
+ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler
{
public Action ControlPointsChanged;
internal readonly Container Pieces;
private readonly Slider slider;
+ private readonly bool allowSelection;
private InputManager inputManager;
- public PathControlPointVisualiser(Slider slider)
+ [Resolved(CanBeNull = true)]
+ private IPlacementHandler placementHandler { get; set; }
+
+ public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
+ this.allowSelection = allowSelection;
RelativeSizeAxes = Axes.Both;
@@ -42,11 +51,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
while (slider.Path.ControlPoints.Length > Pieces.Count)
{
- Pieces.Add(new PathControlPointPiece(slider, Pieces.Count)
+ var piece = new PathControlPointPiece(slider, Pieces.Count)
{
ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
- RequestSelection = selectPiece
- });
+ };
+
+ if (allowSelection)
+ piece.RequestSelection = selectPiece;
+
+ Pieces.Add(piece);
}
while (slider.Path.ControlPoints.Length < Pieces.Count)
@@ -70,5 +83,51 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
piece.IsSelected.Value = piece.Index == index;
}
}
+
+ public bool OnPressed(PlatformAction action)
+ {
+ switch (action.ActionMethod)
+ {
+ case PlatformActionMethod.Delete:
+ var newControlPoints = new List();
+
+ foreach (var piece in Pieces)
+ {
+ if (!piece.IsSelected.Value)
+ newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
+ }
+
+ // Ensure that there are any points to be deleted
+ if (newControlPoints.Count == slider.Path.ControlPoints.Length)
+ return false;
+
+ // If there are 0 remaining control points, treat the slider as being deleted
+ if (newControlPoints.Count == 0)
+ {
+ 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
+ slider.Position = 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;
+ }
+
+ return false;
+ }
+
+ public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index d28cf7b492..78f4c4d992 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Size = body.Size;
OriginPosition = body.PathOffset;
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 6f5309c2c2..9c0afada29 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bodyPiece = new SliderBodyPiece(),
headCirclePiece = new HitCirclePiece(),
tailCirclePiece = new HitCirclePiece(),
- new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() },
+ new PathControlPointVisualiser(HitObject, false) { ControlPointsChanged = _ => updateSlider() },
};
setState(PlacementState.Initial);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index f612ba9dfc..25362820a3 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -33,7 +33,7 @@ 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) { ControlPointsChanged = onNewControlPoints },
+ ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
};
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
index 79cd51a7f4..9b00204d51 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
@@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
- public OsuDistanceSnapGrid(OsuHitObject hitObject)
- : base(hitObject, hitObject.StackedEndPosition)
+ public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
+ : base(hitObject, nextHitObject, hitObject.StackedEndPosition)
{
Masking = true;
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index fcf2772219..812afaaa24 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.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.Game.Beatmaps;
@@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList();
if (objects.Count == 0)
+ return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
+
+ double minTime = objects.Min(h => h.StartTime);
+ return createGrid(h => h.StartTime < minTime, objects.Count + 1);
+ }
+
+ ///
+ /// Creates a grid from the last matching a predicate to a target .
+ ///
+ /// A predicate that matches s where the grid can start from.
+ /// Only the last matching the predicate is used.
+ /// An offset from the selected via at which the grid should stop.
+ /// The from a selected to a target .
+ private OsuDistanceSnapGrid createGrid(Func sourceSelector, int targetOffset = 1)
+ {
+ if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
+
+ int sourceIndex = -1;
+
+ for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{
- var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
+ if (!sourceSelector(EditorBeatmap.HitObjects[i]))
+ break;
- if (lastObject == null)
- return null;
-
- return new OsuDistanceSnapGrid(lastObject);
+ sourceIndex = i;
}
- else
- {
- double minTime = objects.Min(h => h.StartTime);
- var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
+ if (sourceIndex == -1)
+ return null;
- if (lastObject == null)
- return null;
+ OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
+ OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
- return new OsuDistanceSnapGrid(lastObject);
- }
+ return new OsuDistanceSnapGrid(sourceObject, targetObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 472267eb66..9418565907 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -4,13 +4,34 @@
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
- public override void HandleMovement(MoveSelectionEvent moveEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
+ Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
+ Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+
+ // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
+ foreach (var h in SelectedHitObjects.OfType())
+ {
+ if (h is Spinner)
+ {
+ // Spinners don't support position adjustments
+ continue;
+ }
+
+ // Stacking is not considered
+ minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
+ maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
+ }
+
+ if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
+ return false;
+
foreach (var h in SelectedHitObjects.OfType())
{
if (h is Spinner)
@@ -22,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += moveEvent.InstantDelta;
}
- base.HandleMovement(moveEvent);
+ return true;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
deleted file mode 100644
index 9106f4c7bd..0000000000
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
-using System.Collections.Generic;
-
-namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
-{
- ///
- /// Connects hit objects visually, for example with follow points.
- ///
- public abstract class ConnectionRenderer : LifetimeManagementContainer
- where T : HitObject
- {
- ///
- /// Hit objects to create connections for
- ///
- public abstract IEnumerable HitObjects { get; set; }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index 89ffddf4cb..db34ae1d87 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -12,6 +12,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
+ ///
+ /// A single follow point positioned between two adjacent s.
+ ///
public class FollowPoint : Container
{
private const float width = 8;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
new file mode 100644
index 0000000000..1e032eb977
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -0,0 +1,140 @@
+// 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 JetBrains.Annotations;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
+{
+ ///
+ /// Visualises the s between two s.
+ ///
+ public class FollowPointConnection : CompositeDrawable
+ {
+ // Todo: These shouldn't be constants
+ private const int spacing = 32;
+ private const double preempt = 800;
+
+ ///
+ /// The start time of .
+ ///
+ public readonly Bindable StartTime = new Bindable();
+
+ ///
+ /// The which s will exit from.
+ ///
+ [NotNull]
+ public readonly DrawableOsuHitObject Start;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The which s will exit from.
+ public FollowPointConnection([NotNull] DrawableOsuHitObject start)
+ {
+ Start = start;
+
+ RelativeSizeAxes = Axes.Both;
+
+ StartTime.BindTo(Start.HitObject.StartTimeBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ bindEvents(Start);
+ }
+
+ private DrawableOsuHitObject end;
+
+ ///
+ /// The which s will enter.
+ ///
+ [CanBeNull]
+ public DrawableOsuHitObject End
+ {
+ get => end;
+ set
+ {
+ end = value;
+
+ if (end != null)
+ bindEvents(end);
+
+ if (IsLoaded)
+ scheduleRefresh();
+ else
+ refresh();
+ }
+ }
+
+ private void bindEvents(DrawableOsuHitObject drawableObject)
+ {
+ drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
+ drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
+ }
+
+ private void scheduleRefresh() => Scheduler.AddOnce(refresh);
+
+ private void refresh()
+ {
+ ClearInternal();
+
+ if (End == null)
+ return;
+
+ OsuHitObject osuStart = Start.HitObject;
+ OsuHitObject osuEnd = End.HitObject;
+
+ if (osuEnd.NewCombo)
+ return;
+
+ if (osuStart is Spinner || osuEnd is Spinner)
+ return;
+
+ Vector2 startPosition = osuStart.EndPosition;
+ Vector2 endPosition = osuEnd.Position;
+ double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
+ double endTime = osuEnd.StartTime;
+
+ Vector2 distanceVector = endPosition - startPosition;
+ int distance = (int)distanceVector.Length;
+ float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
+ double duration = endTime - startTime;
+
+ for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
+ {
+ float fraction = (float)d / distance;
+ Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
+ Vector2 pointEndPosition = startPosition + fraction * distanceVector;
+ double fadeOutTime = startTime + fraction * duration;
+ double fadeInTime = fadeOutTime - preempt;
+
+ FollowPoint fp;
+
+ AddInternal(fp = new FollowPoint
+ {
+ Position = pointStartPosition,
+ Rotation = rotation,
+ Alpha = 0,
+ Scale = new Vector2(1.5f * osuEnd.Scale),
+ });
+
+ using (fp.BeginAbsoluteSequence(fadeInTime))
+ {
+ fp.FadeIn(osuEnd.TimeFadeIn);
+ fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
+ fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
+ fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
+ }
+
+ fp.Expire(true);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index a269b87c75..be192080f9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -1,121 +1,110 @@
// 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 osuTK;
+using System.Linq;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Objects.Types;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
- public class FollowPointRenderer : ConnectionRenderer
+ ///
+ /// Visualises connections between s.
+ ///
+ public class FollowPointRenderer : CompositeDrawable
{
- private int pointDistance = 32;
-
///
- /// Determines how much space there is between points.
+ /// All the s contained by this .
///
- public int PointDistance
- {
- get => pointDistance;
- set
- {
- if (pointDistance == value) return;
+ internal IReadOnlyList Connections => connections;
- pointDistance = value;
- update();
- }
- }
-
- private int preEmpt = 800;
-
- ///
- /// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
- ///
- public int PreEmpt
- {
- get => preEmpt;
- set
- {
- if (preEmpt == value) return;
-
- preEmpt = value;
- update();
- }
- }
-
- private IEnumerable hitObjects;
-
- public override IEnumerable HitObjects
- {
- get => hitObjects;
- set
- {
- hitObjects = value;
- update();
- }
- }
+ private readonly List connections = new List();
public override bool RemoveCompletedTransforms => false;
- private void update()
+ ///
+ /// Adds the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to add s for.
+ public void AddFollowPoints(DrawableOsuHitObject hitObject)
+ => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
+
+ ///
+ /// Removes the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to remove s for.
+ public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
+
+ ///
+ /// Adds a to this .
+ ///
+ /// The to add.
+ /// The index of in .
+ private void addConnection(FollowPointConnection connection)
{
- ClearInternal();
+ AddInternal(connection);
- if (hitObjects == null)
- return;
+ // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
+ int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
- OsuHitObject prevHitObject = null;
-
- foreach (var currHitObject in hitObjects)
+ if (index < connections.Count - 1)
{
- if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
- {
- Vector2 startPosition = prevHitObject.EndPosition;
- Vector2 endPosition = currHitObject.Position;
- double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
- double endTime = currHitObject.StartTime;
+ // Update the connection's end point to the next connection's start point
+ // h1 -> -> -> h2
+ // connection nextGroup
- Vector2 distanceVector = endPosition - startPosition;
- int distance = (int)distanceVector.Length;
- float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
- double duration = endTime - startTime;
-
- for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
- {
- float fraction = (float)d / distance;
- Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
- Vector2 pointEndPosition = startPosition + fraction * distanceVector;
- double fadeOutTime = startTime + fraction * duration;
- double fadeInTime = fadeOutTime - PreEmpt;
-
- FollowPoint fp;
-
- AddInternal(fp = new FollowPoint
- {
- Position = pointStartPosition,
- Rotation = rotation,
- Alpha = 0,
- Scale = new Vector2(1.5f * currHitObject.Scale),
- });
-
- using (fp.BeginAbsoluteSequence(fadeInTime))
- {
- fp.FadeIn(currHitObject.TimeFadeIn);
- fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
- }
-
- fp.Expire(true);
- }
- }
-
- prevHitObject = currHitObject;
+ FollowPointConnection nextConnection = connections[index + 1];
+ connection.End = nextConnection.Start;
}
+ else
+ {
+ // The end point may be non-null during re-ordering
+ connection.End = null;
+ }
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the current connection's start point
+ // h1 -> -> -> h2
+ // prevGroup connection
+
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.Start;
+ }
+ }
+
+ ///
+ /// Removes a from this .
+ ///
+ /// The to remove.
+ /// Whether was removed.
+ private void removeGroup(FollowPointConnection connection)
+ {
+ RemoveInternal(connection);
+
+ int index = connections.IndexOf(connection);
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the next connection's start point
+ // h1 -> -> -> h2 -> -> -> h3
+ // prevGroup connection nextGroup
+ // The current connection's end point is used since there may not be a next connection
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.End;
+ }
+
+ connections.Remove(connection);
+ }
+
+ private void onStartTimeChanged(FollowPointConnection connection)
+ {
+ // Naive but can be improved if performance becomes an issue
+ removeGroup(connection);
+ addConnection(connection);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 69e53d6eea..6d1ea4bbfc 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI;
-using System.Linq;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
- private readonly ConnectionRenderer connectionLayer;
+ private readonly FollowPointRenderer followPoints;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
- connectionLayer = new FollowPointRenderer
+ followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
@@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
};
base.Add(h);
+
+ followPoints.AddFollowPoints((DrawableOsuHitObject)h);
}
- public override void PostProcess()
+ public override bool Remove(DrawableHitObject h)
{
- connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType();
+ bool result = base.Remove(h);
+
+ if (result)
+ followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
+
+ return result;
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
index b8c31d5dbb..e4c987923c 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
@@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
+ }
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
Children = new Drawable[]
{
new Box
@@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
},
new TestDistanceSnapGrid(new HitObject(), grid_position)
};
- }
+ });
[TestCase(1)]
[TestCase(2)]
@@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
}
+ [Test]
+ public void TestLimitedDistance()
+ {
+ AddStep("create limited grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
+ };
+ });
+ }
+
private class TestDistanceSnapGrid : DistanceSnapGrid
{
public new float DistanceSpacing => base.DistanceSpacing;
- public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
- : base(hitObject, centrePosition)
+ public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
+ : base(hitObject, nextHitObject, centrePosition)
{
}
@@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
int beatIndex = 0;
- for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
- public float DurationToDistance(double referenceTime, double duration) => 0;
+ public float DurationToDistance(double referenceTime, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => 0;
+ public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index a8c2362910..6e5b3b93e9 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -10,9 +10,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
}
}
- private class StartStopButton : Button
+ private class StartStopButton : OsuButton
{
private IAdjustableClock adjustableClock;
private bool started;
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
new file mode 100644
index 0000000000..121853d8d0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Timing;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+ [TestFixture]
+ public class TestSceneTimingScreen : EditorClockTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(ControlPointTable),
+ typeof(ControlPointSettings),
+ typeof(Section<>),
+ typeof(TimingSection),
+ typeof(EffectSection),
+ typeof(SampleSection),
+ typeof(DifficultySection),
+ typeof(RowAttribute)
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Child = new TimingScreen();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 64022b2410..6e8975f11b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -237,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
AddStep("exit", () => Player.Exit());
confirmExited();
+ confirmNoTrackAdjustments();
}
private void confirmPaused()
@@ -258,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
}
+ private void confirmNoTrackAdjustments()
+ {
+ AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
+ }
+
private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause());
private void resume() => AddStep("resume", () => Player.Resume());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
index bf26892539..7790126db5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
@@ -3,11 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages;
@@ -27,7 +32,8 @@ namespace osu.Game.Tests.Visual.Gameplay
typeof(ScoreResultsPage),
typeof(RetryButton),
typeof(ReplayDownloadButton),
- typeof(LocalLeaderboardPage)
+ typeof(LocalLeaderboardPage),
+ typeof(TestPlayer)
};
[BackgroundDependencyLoader]
@@ -43,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
+ }
- LoadScreen(new SoloResults(new ScoreInfo
+ private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
+ {
+ TotalScore = 2845370,
+ Accuracy = 0.98,
+ MaxCombo = 123,
+ Rank = ScoreRank.A,
+ Date = DateTimeOffset.Now,
+ Statistics = new Dictionary
{
- TotalScore = 2845370,
- Accuracy = 0.98,
- MaxCombo = 123,
- Rank = ScoreRank.A,
- Date = DateTimeOffset.Now,
- Statistics = new Dictionary
+ { HitResult.Great, 50 },
+ { HitResult.Good, 20 },
+ { HitResult.Meh, 50 },
+ { HitResult.Miss, 1 }
+ },
+ User = new User
+ {
+ Username = "peppy",
+ }
+ });
+
+ [Test]
+ public void ResultsWithoutPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
+ {
+ RelativeSizeAxes = Axes.Both
+ });
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
+ }
+
+ [Test]
+ public void ResultsWithPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay present", () => screen.RetryOverlay != null);
+ }
+
+ private class TestResultsContainer : Container
+ {
+ [Cached(typeof(Player))]
+ private readonly Player player = new TestPlayer();
+
+ public TestResultsContainer(IScreen screen)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new OsuScreenStack(screen)
{
- { HitResult.Great, 50 },
- { HitResult.Good, 20 },
- { HitResult.Meh, 50 },
- { HitResult.Miss, 1 }
- },
- User = new User
- {
- Username = "peppy",
- }
- }));
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+ }
+
+ private class TestSoloResults : SoloResults
+ {
+ public HotkeyRetryOverlay RetryOverlay;
+
+ public TestSoloResults(ScoreInfo score)
+ : base(score)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RetryOverlay = InternalChildren.OfType().SingleOrDefault();
+ }
}
}
}
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index dbfa70704b..21552882ef 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -215,7 +215,7 @@ namespace osu.Game.Tournament
foreach (var r in ladder.Rounds)
foreach (var b in r.Beatmaps)
- if (b.BeatmapInfo == null)
+ if (b.BeatmapInfo == null && b.ID > 0)
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API);
diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs
index 22ce7d4711..937ad7e45a 100644
--- a/osu.Game/Audio/PreviewTrack.cs
+++ b/osu.Game/Audio/PreviewTrack.cs
@@ -13,11 +13,13 @@ namespace osu.Game.Audio
{
///
/// Invoked when this has stopped playing.
+ /// Not invoked in a thread-safe context.
///
public event Action Stopped;
///
/// Invoked when this has started playing.
+ /// Not invoked in a thread-safe context.
///
public event Action Started;
@@ -29,7 +31,7 @@ namespace osu.Game.Audio
{
track = GetTrack();
if (track != null)
- track.Completed += () => Schedule(Stop);
+ track.Completed += Stop;
}
///
@@ -93,6 +95,7 @@ namespace osu.Game.Audio
hasStarted = false;
track.Stop();
+
Stopped?.Invoke();
}
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index e12c46ef16..fad2b5a5e8 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -46,18 +46,18 @@ namespace osu.Game.Audio
{
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
- track.Started += () =>
+ track.Started += () => Schedule(() =>
{
current?.Stop();
current = track;
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
- };
+ });
- track.Stopped += () =>
+ track.Stopped += () => Schedule(() =>
{
current = null;
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
- };
+ });
return track;
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index c00b04b660..51b3377394 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -43,6 +43,11 @@ namespace osu.Game.Beatmaps.ControlPoints
set => BeatLengthBindable.Value = value;
}
+ ///
+ /// The BPM at this control point.
+ ///
+ public double BPM => 60000 / BeatLength;
+
public override bool EquivalentTo(ControlPoint other) =>
other is TimingControlPoint otherTyped
&& TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index 15068d81c0..61391b7102 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -8,9 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Graphics;
-using osu.Framework.Logging;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Notifications;
using osu.Game.Users;
namespace osu.Game.Graphics.Containers
@@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
}
private OsuGame game;
- private ChannelManager channelManager;
- private Action showNotImplementedError;
[BackgroundDependencyLoader(true)]
- private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
+ private void load(OsuGame game)
{
// will be null in tests
this.game = game;
- this.channelManager = channelManager;
-
- showNotImplementedError = () => notifications?.Post(new SimpleNotification
- {
- Text = @"This link type is not yet supported!",
- Icon = FontAwesome.Solid.LifeRing,
- });
}
public void AddLinks(string text, List links)
@@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
- AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
+ AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length;
}
AddText(text.Substring(previousLinkEnd));
}
- public IEnumerable AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action creationParameters = null)
- => createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
+ public void AddLink(string text, string url, Action creationParameters = null) =>
+ createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
- public IEnumerable AddLink(string text, Action action, string tooltipText = null, Action creationParameters = null)
- => createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
+ public void AddLink(string text, Action action, string tooltipText = null, Action creationParameters = null)
+ => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
- public IEnumerable AddLink(IEnumerable text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
+ public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action creationParameters = null)
+ => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
+
+ public void AddLink(IEnumerable text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{
foreach (var t in text)
AddArbitraryDrawable(t);
- return createLink(text, null, url, linkType, linkArgument, tooltipText);
+ createLink(text, new LinkDetails(action, linkArgument), tooltipText);
}
- public IEnumerable AddUserLink(User user, Action creationParameters = null)
- => createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
+ public void AddUserLink(User user, Action creationParameters = null)
+ => createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
- private IEnumerable createLink(IEnumerable drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null)
+ private void createLink(IEnumerable drawables, LinkDetails link, string tooltipText, Action action = null)
{
AddInternal(new DrawableLinkCompiler(drawables.OfType().ToList())
{
RelativeSizeAxes = Axes.Both,
- TooltipText = tooltipText ?? (url != text ? url : string.Empty),
- Action = action ?? (() =>
+ TooltipText = tooltipText,
+ Action = () =>
{
- switch (linkType)
- {
- case LinkAction.OpenBeatmap:
- // TODO: proper query params handling
- if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId))
- game?.ShowBeatmap(beatmapId);
- break;
-
- case LinkAction.OpenBeatmapSet:
- if (int.TryParse(linkArgument, out int setId))
- game?.ShowBeatmapSet(setId);
- break;
-
- case LinkAction.OpenChannel:
- try
- {
- channelManager?.OpenChannel(linkArgument);
- }
- catch (ChannelNotFoundException)
- {
- Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
- }
-
- break;
-
- case LinkAction.OpenEditorTimestamp:
- case LinkAction.JoinMultiplayerMatch:
- case LinkAction.Spectate:
- showNotImplementedError?.Invoke();
- break;
-
- case LinkAction.External:
- game?.OpenUrlExternally(url);
- break;
-
- case LinkAction.OpenUserProfile:
- if (long.TryParse(linkArgument, out long userId))
- game?.ShowUser(userId);
- break;
-
- default:
- throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
- }
- }),
+ if (action != null)
+ action();
+ else
+ game.HandleLink(link);
+ },
});
-
- return drawables;
}
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 4124d2ad58..2750e61f0d 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,10 +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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
@@ -19,53 +21,104 @@ namespace osu.Game.Graphics.UserInterface
///
public class OsuButton : Button
{
- private Box hover;
+ public string Text
+ {
+ get => SpriteText?.Text;
+ set
+ {
+ if (SpriteText != null)
+ SpriteText.Text = value;
+ }
+ }
+
+ private Color4? backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ set
+ {
+ backgroundColour = value;
+ Background.FadeColour(value);
+ }
+ }
+
+ protected override Container Content { get; }
+
+ protected Box Hover;
+ protected Box Background;
+ protected SpriteText SpriteText;
public OsuButton()
{
Height = 40;
- Content.Masking = true;
- Content.CornerRadius = 5;
+ AddInternal(Content = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ CornerRadius = 5,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Background = new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Hover = new Box
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White.Opacity(.1f),
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue
+ },
+ SpriteText = CreateText(),
+ new HoverClickSounds(HoverSampleSet.Loud),
+ }
+ });
+
+ Enabled.BindValueChanged(enabledChanged, true);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = colours.BlueDark;
-
- AddRange(new Drawable[]
- {
- hover = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Blending = BlendingParameters.Additive,
- Colour = Color4.White.Opacity(0.1f),
- Alpha = 0,
- Depth = -1
- },
- new HoverClickSounds(HoverSampleSet.Loud),
- });
+ if (backgroundColour == null)
+ BackgroundColour = colours.BlueDark;
Enabled.ValueChanged += enabledChanged;
Enabled.TriggerChange();
}
- private void enabledChanged(ValueChangedEvent e)
+ protected override bool OnClick(ClickEvent e)
{
- this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
+ if (Enabled.Value)
+ {
+ Debug.Assert(backgroundColour != null);
+ Background.FlashColour(backgroundColour.Value, 200);
+ }
+
+ return base.OnClick(e);
}
protected override bool OnHover(HoverEvent e)
{
- hover.FadeIn(200);
- return true;
+ if (Enabled.Value)
+ Hover.FadeIn(200, Easing.OutQuint);
+
+ return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- hover.FadeOut(200);
base.OnHoverLost(e);
+
+ Hover.FadeOut(300);
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -80,12 +133,17 @@ namespace osu.Game.Graphics.UserInterface
return base.OnMouseUp(e);
}
- protected override SpriteText CreateText() => new OsuSpriteText
+ protected virtual SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.Bold)
};
+
+ private void enabledChanged(ValueChangedEvent e)
+ {
+ this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
+ }
}
}
diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs
index 9033e7529d..35f38ea7e8 100644
--- a/osu.Game/IO/Archives/ZipArchiveReader.cs
+++ b/osu.Game/IO/Archives/ZipArchiveReader.cs
@@ -1,30 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
-using SharpCompress.Common;
namespace osu.Game.IO.Archives
{
public sealed class ZipArchiveReader : ArchiveReader
{
- ///
- /// List of substrings that indicate a file should be ignored during the import process
- /// (usually due to representing no useful data and being autogenerated by the OS).
- ///
- private static readonly string[] filename_ignore_list =
- {
- // Mac-specific
- "__MACOSX",
- ".DS_Store",
- // Windows-specific
- "Thumbs.db"
- };
-
private readonly Stream archiveStream;
private readonly ZipArchive archive;
@@ -58,9 +44,7 @@ namespace osu.Game.IO.Archives
archiveStream.Dispose();
}
- private static bool canBeIgnored(IEntry entry) => filename_ignore_list.Any(ignoredName => entry.Key.IndexOf(ignoredName, StringComparison.OrdinalIgnoreCase) >= 0);
-
- public override IEnumerable Filenames => archive.Entries.Where(e => !canBeIgnored(e)).Select(e => e.Key).ToArray();
+ public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
public override Stream GetUnderlyingStream() => archiveStream;
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 3ffff281f8..717de18c14 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
- var details = getLinkDetails(linkText);
+ var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
@@ -98,7 +98,7 @@ namespace osu.Game.Online.Chat
var linkText = m.Groups["link"].Value;
var indexLength = linkText.Length;
- var details = getLinkDetails(linkText);
+ var details = GetLinkDetails(linkText);
var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
// sometimes an already-processed formatted link can reduce to a simple URL, too
@@ -109,7 +109,7 @@ namespace osu.Game.Online.Chat
}
}
- private static LinkDetails getLinkDetails(string url)
+ public static LinkDetails GetLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
@@ -255,17 +255,17 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text;
}
}
+ }
- public class LinkDetails
+ public class LinkDetails
+ {
+ public LinkAction Action;
+ public string Argument;
+
+ public LinkDetails(LinkAction action, string argument)
{
- public LinkAction Action;
- public string Argument;
-
- public LinkDetails(LinkAction action, string argument)
- {
- Action = action;
- Argument = argument;
- }
+ Action = action;
+ Argument = argument;
}
}
@@ -279,6 +279,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch,
Spectate,
OpenUserProfile,
+ Custom
}
public class Link : IComparable
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 9387482f14..623db07938 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
using Humanizer;
+using osu.Game.Online.API;
namespace osu.Game.Online.Leaderboards
{
@@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
private readonly ScoreInfo score;
private readonly int rank;
+ private readonly bool allowHighlight;
private Box background;
private Container content;
@@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
private List statisticsLabels;
- public LeaderboardScore(ScoreInfo score, int rank)
+ public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
{
this.score = score;
this.rank = rank;
+ this.allowHighlight = allowHighlight;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(IAPIProvider api, OsuColour colour)
{
var user = score.User;
@@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
+ Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha,
},
},
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 4dcc181bea..1f823e6eba 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -215,31 +215,102 @@ namespace osu.Game
private ExternalLinkOpener externalLinkOpener;
- public void OpenUrlExternally(string url)
+ ///
+ /// Handle an arbitrary URL. Displays via in-game overlays where possible.
+ /// This can be called from a non-thread-safe non-game-loaded state.
+ ///
+ /// The URL to load.
+ public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
+
+ ///
+ /// Handle a specific .
+ /// This can be called from a non-thread-safe non-game-loaded state.
+ ///
+ /// The link to load.
+ public void HandleLink(LinkDetails link) => Schedule(() =>
+ {
+ switch (link.Action)
+ {
+ case LinkAction.OpenBeatmap:
+ // TODO: proper query params handling
+ if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
+ ShowBeatmap(beatmapId);
+ break;
+
+ case LinkAction.OpenBeatmapSet:
+ if (int.TryParse(link.Argument, out int setId))
+ ShowBeatmapSet(setId);
+ break;
+
+ case LinkAction.OpenChannel:
+ ShowChannel(link.Argument);
+ break;
+
+ case LinkAction.OpenEditorTimestamp:
+ case LinkAction.JoinMultiplayerMatch:
+ case LinkAction.Spectate:
+ waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
+ {
+ Text = @"This link type is not yet supported!",
+ Icon = FontAwesome.Solid.LifeRing,
+ }));
+ break;
+
+ case LinkAction.External:
+ OpenUrlExternally(link.Argument);
+ break;
+
+ case LinkAction.OpenUserProfile:
+ if (long.TryParse(link.Argument, out long userId))
+ ShowUser(userId);
+ break;
+
+ default:
+ throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
+ }
+ });
+
+ public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{
if (url.StartsWith("/"))
url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url);
- }
+ });
+
+ ///
+ /// Open a specific channel in chat.
+ ///
+ /// The channel to display.
+ public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
+ {
+ try
+ {
+ channelManager.OpenChannel(channel);
+ }
+ catch (ChannelNotFoundException)
+ {
+ Logger.Log($"The requested channel \"{channel}\" does not exist");
+ }
+ });
///
/// Show a beatmap set as an overlay.
///
/// The set to display.
- public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId);
+ public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
///
/// Show a user's profile as an overlay.
///
/// The user to display.
- public void ShowUser(long userId) => userProfile.ShowUser(userId);
+ public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
///
/// Show a beatmap's set as an overlay, displaying the given beatmap.
///
/// The beatmap to show.
- public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
+ public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
///
/// Present a beatmap at song select immediately.
@@ -397,6 +468,23 @@ namespace osu.Game
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
}
+ ///
+ /// Wait for the game (and target component) to become loaded and then run an action.
+ ///
+ /// A function to retrieve a (potentially not-yet-constructed) target instance.
+ /// The action to perform on the instance when load is confirmed.
+ /// The type of the target instance.
+ private void waitForReady(Func retrieveInstance, Action action)
+ where T : Drawable
+ {
+ var instance = retrieveInstance();
+
+ if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
+ Schedule(() => waitForReady(retrieveInstance, action));
+ else
+ action(instance);
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index bce1be5941..d8488b21ab 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
t.Font = fontLarge;
t.Colour = entryColour;
});
- title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External,
+ title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
creationParameters: t =>
{
t.Font = fontLarge;
@@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour;
});
else if (entry.GithubUser.GithubUrl != null)
- title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t =>
+ title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{
t.Font = fontMedium;
t.Colour = entryColour;
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 6b79f2af07..4f73cbfacd 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -168,12 +168,13 @@ namespace osu.Game.Overlays
},
}
},
- progressBar = new ProgressBar
+ progressBar = new HoverableProgressBar
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
- Height = progress_height,
+ Height = progress_height / 2,
FillColour = colours.Yellow,
+ BackgroundColour = colours.YellowDarker.Opacity(0.5f),
OnSeek = musicController.SeekTo
}
},
@@ -401,5 +402,20 @@ namespace osu.Game.Overlays
return base.OnDragEnd(e);
}
}
+
+ private class HoverableProgressBar : ProgressBar
+ {
+ protected override bool OnHover(HoverEvent e)
+ {
+ this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ this.ResizeHeightTo(progress_height / 2, 500, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index a94f76e7af..68836bc6b3 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.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 osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@@ -9,21 +8,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
- public class SidebarButton : Button
+ public class SidebarButton : OsuButton
{
private readonly SpriteIcon drawableIcon;
private readonly SpriteText headerText;
private readonly Box selectionIndicator;
private readonly Container text;
- public new Action Action;
private SettingsSection section;
@@ -62,12 +58,11 @@ namespace osu.Game.Overlays.Settings
public SidebarButton()
{
- BackgroundColour = OsuColour.Gray(60);
- Background.Alpha = 0;
-
Height = Sidebar.DEFAULT_WIDTH;
RelativeSizeAxes = Axes.X;
+ BackgroundColour = Color4.Black;
+
AddRange(new Drawable[]
{
text = new Container
@@ -99,7 +94,6 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
- new HoverClickSounds(HoverSampleSet.Loud),
});
}
@@ -108,23 +102,5 @@ namespace osu.Game.Overlays.Settings
{
selectionIndicator.Colour = colours.Yellow;
}
-
- protected override bool OnClick(ClickEvent e)
- {
- Action?.Invoke(section);
- return base.OnClick(e);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- Background.FadeTo(0.4f, 200);
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- Background.FadeTo(0, 200);
- base.OnHoverLost(e);
- }
}
}
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index d028664fe0..2948231c4b 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -123,9 +123,9 @@ namespace osu.Game.Overlays
var button = new SidebarButton
{
Section = section,
- Action = s =>
+ Action = () =>
{
- SectionsContainer.ScrollTo(s);
+ SectionsContainer.ScrollTo(section);
Sidebar.State = ExpandedState.Contracted;
},
};
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 0701513933..2923411ce1 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Edit
}
}
+ // When not selected, input is only required for the blueprint itself to receive IsHovering
+ protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
+
///
/// Selects this , causing it to become visible.
///
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index a145dea6af..c4d8176c7a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -254,6 +254,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
Debug.Assert(!clickSelectionBegan);
+ // If a select blueprint is already hovered, disallow changes in selection.
+ // Exception is made when holding control, as deselection should still be allowed.
+ if (!e.CurrentState.Keyboard.ControlPressed &&
+ selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
+ return;
+
foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints)
{
if (blueprint.IsHovered)
@@ -361,7 +367,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
(Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects
- selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition)));
+ if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition))))
+ return true;
// Apply the start time at the newly snapped-to position
double offset = snappedTime - draggedObject.StartTime;
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index f45115e1e4..0f2bae6305 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{
- protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
- : base(hitObject, centrePosition)
+ protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition)
+ : base(hitObject, nextHitObject, centrePosition)
{
}
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
float maxDistance = new Vector2(dx, dy).Length;
- int requiredCircles = (int)(maxDistance / DistanceSpacing);
+ int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing));
for (int i = 0; i < requiredCircles; i++)
{
@@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
{
- Vector2 direction = position - CentrePosition;
+ if (MaxIntervals == 0)
+ return (CentrePosition, StartTime);
+ Vector2 direction = position - CentrePosition;
if (direction == Vector2.Zero)
direction = new Vector2(0.001f, 0.001f);
float distance = direction.Length;
float radius = DistanceSpacing;
- int radialCount = Math.Max(1, (int)Math.Round(distance / radius));
+ int radialCount = MathHelper.Clamp((int)Math.Round(distance / radius), 1, MaxIntervals);
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 193474093f..475b6e7274 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
@@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
protected double StartTime { get; private set; }
+ ///
+ /// The maximum number of distance snapping intervals allowed.
+ ///
+ protected int MaxIntervals { get; private set; }
+
///
/// The position which the grid is centred on.
/// The first beat snapping tick is located at + in the desired direction.
@@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly Cached gridCache = new Cached();
private readonly HitObject hitObject;
+ private readonly HitObject nextHitObject;
- protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
+ protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition)
{
this.hitObject = hitObject;
+ this.nextHitObject = nextHitObject;
CentrePosition = centrePosition;
+
RelativeSizeAxes = Axes.Both;
}
@@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
+
+ if (nextHitObject == null)
+ MaxIntervals = int.MaxValue;
+ else
+ {
+ // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
+ double maxDuration = nextHitObject.StartTime - StartTime + 1;
+ MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
+ }
+
gridCache.Invalidate();
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index d7821eff07..44bf22cfe1 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -8,21 +8,21 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.States;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
-using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
///
/// A component which outlines s and handles movement of selections.
///
- public class SelectionHandler : CompositeDrawable
+ public class SelectionHandler : CompositeDrawable, IKeyBindingHandler
{
public const float BORDER_RADIUS = 2;
@@ -68,26 +68,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handles the selected s being moved.
///
/// The move event.
- public virtual void HandleMovement(MoveSelectionEvent moveEvent)
- {
- }
+ /// Whether any s were moved.
+ public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
- protected override bool OnKeyDown(KeyDownEvent e)
+ public bool OnPressed(PlatformAction action)
{
- if (e.Repeat)
- return base.OnKeyDown(e);
-
- switch (e.Key)
+ switch (action.ActionMethod)
{
- case Key.Delete:
+ case PlatformActionMethod.Delete:
foreach (var h in selectedBlueprints.ToList())
placementHandler.Delete(h.DrawableObject.HitObject);
return true;
}
- return base.OnKeyDown(e);
+ return false;
}
+ public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
+
#endregion
#region Selection Handling
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
new file mode 100644
index 0000000000..e1182d9fa4
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class ControlPointSettings : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray3,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = createSections()
+ },
+ }
+ };
+ }
+
+ private IReadOnlyList createSections() => new Drawable[]
+ {
+ new TimingSection(),
+ new DifficultySection(),
+ new SampleSection(),
+ new EffectSection(),
+ };
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
new file mode 100644
index 0000000000..96e3ab48f2
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
@@ -0,0 +1,247 @@
+// 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.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class ControlPointTable : TableContainer
+ {
+ private const float horizontal_inset = 20;
+ private const float row_height = 25;
+ private const int text_size = 14;
+
+ private readonly FillFlowContainer backgroundFlow;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ public ControlPointTable()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Padding = new MarginPadding { Horizontal = horizontal_inset };
+ RowSize = new Dimension(GridSizeMode.Absolute, row_height);
+
+ AddInternal(backgroundFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 1f,
+ Padding = new MarginPadding { Horizontal = -horizontal_inset },
+ Margin = new MarginPadding { Top = row_height }
+ });
+ }
+
+ public IEnumerable ControlGroups
+ {
+ set
+ {
+ Content = null;
+ backgroundFlow.Clear();
+
+ if (value?.Any() != true)
+ return;
+
+ foreach (var group in value)
+ {
+ backgroundFlow.Add(new RowBackground(group));
+ }
+
+ Columns = createHeaders();
+ Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
+ }
+ }
+
+ private TableColumn[] createHeaders()
+ {
+ var columns = new List
+ {
+ new TableColumn(string.Empty, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
+ new TableColumn("Time", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
+ new TableColumn("Attributes", Anchor.Centre),
+ };
+
+ return columns.ToArray();
+ }
+
+ private Drawable[] createContent(int index, ControlPointGroup group) => new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = $"#{index + 1}",
+ Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
+ Margin = new MarginPadding(10)
+ },
+ new OsuSpriteText
+ {
+ Text = $"{group.Time:n0}ms",
+ Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
+ },
+ new ControlGroupAttributes(group),
+ };
+
+ private class ControlGroupAttributes : CompositeDrawable
+ {
+ private readonly IBindableList controlPoints;
+
+ private readonly FillFlowContainer fill;
+
+ public ControlGroupAttributes(ControlPointGroup group)
+ {
+ InternalChild = fill = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(2)
+ };
+
+ controlPoints = group.ControlPoints.GetBoundCopy();
+ controlPoints.ItemsAdded += _ => createChildren();
+ controlPoints.ItemsRemoved += _ => createChildren();
+
+ createChildren();
+ }
+
+ private void createChildren()
+ {
+ fill.ChildrenEnumerable = controlPoints.Select(createAttribute).Where(c => c != null);
+ }
+
+ private Drawable createAttribute(ControlPoint controlPoint)
+ {
+ switch (controlPoint)
+ {
+ case TimingControlPoint timing:
+ return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
+
+ case DifficultyControlPoint difficulty:
+
+ return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
+
+ case EffectControlPoint effect:
+ return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
+
+ case SampleControlPoint sample:
+ return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
+ }
+
+ return null;
+ }
+ }
+
+ protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
+
+ private class HeaderText : OsuSpriteText
+ {
+ public HeaderText(string text)
+ {
+ Text = text.ToUpper();
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black);
+ }
+ }
+
+ public class RowBackground : OsuClickableContainer
+ {
+ private readonly ControlPointGroup controlGroup;
+ private const int fade_duration = 100;
+
+ private readonly Box hoveredBackground;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ public RowBackground(ControlPointGroup controlGroup)
+ {
+ this.controlGroup = controlGroup;
+ RelativeSizeAxes = Axes.X;
+ Height = 25;
+
+ AlwaysPresent = true;
+
+ CornerRadius = 3;
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ hoveredBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ };
+
+ Action = () => selectedGroup.Value = controlGroup;
+ }
+
+ private Color4 colourHover;
+ private Color4 colourSelected;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ hoveredBackground.Colour = colourHover = colours.BlueDarker;
+ colourSelected = colours.YellowDarker;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
+ }
+
+ private bool selected;
+
+ protected bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (value == selected)
+ return;
+
+ selected = value;
+ updateState();
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
+
+ if (selected || IsHovered)
+ hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
+ else
+ hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
new file mode 100644
index 0000000000..58a7f97e5f
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class DifficultySection : Section
+ {
+ private SettingsSlider multiplier;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new[]
+ {
+ multiplier = new SettingsSlider
+ {
+ LabelText = "Speed Multiplier",
+ Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
+ RelativeSizeAxes = Axes.X,
+ }
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
+ }
+ }
+
+ protected override DifficultyControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
+
+ return new DifficultyControlPoint
+ {
+ SpeedMultiplier = reference.SpeedMultiplier,
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
new file mode 100644
index 0000000000..71e7f42713
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class EffectSection : Section
+ {
+ private LabelledSwitchButton kiai;
+ private LabelledSwitchButton omitBarLine;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new[]
+ {
+ kiai = new LabelledSwitchButton { Label = "Kiai Time" },
+ omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ kiai.Current = point.NewValue.KiaiModeBindable;
+ omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
+ }
+ }
+
+ protected override EffectControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
+
+ return new EffectControlPoint
+ {
+ KiaiMode = reference.KiaiMode,
+ OmitFirstBarLine = reference.OmitFirstBarLine
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs
new file mode 100644
index 0000000000..be8f693683
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs
@@ -0,0 +1,60 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class RowAttribute : CompositeDrawable, IHasTooltip
+ {
+ private readonly string header;
+ private readonly Func content;
+
+ public RowAttribute(string header, Func content)
+ {
+ this.header = header;
+ this.content = content;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AutoSizeAxes = Axes.X;
+
+ Height = 20;
+
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+
+ Masking = true;
+ CornerRadius = 5;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Yellow,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Padding = new MarginPadding(2),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
+ Text = header,
+ Colour = colours.Gray3
+ },
+ };
+ }
+
+ public string TooltipText => content();
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs
new file mode 100644
index 0000000000..4665c77991
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class SampleSection : Section
+ {
+ private LabelledTextBox bank;
+ private SettingsSlider volume;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new Drawable[]
+ {
+ bank = new LabelledTextBox
+ {
+ Label = "Bank Name",
+ },
+ volume = new SettingsSlider
+ {
+ Bindable = new SampleControlPoint().SampleVolumeBindable,
+ LabelText = "Volume",
+ }
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ bank.Current = point.NewValue.SampleBankBindable;
+ volume.Bindable = point.NewValue.SampleVolumeBindable;
+ }
+ }
+
+ protected override SampleControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
+
+ return new SampleControlPoint
+ {
+ SampleBank = reference.SampleBank,
+ SampleVolume = reference.SampleVolume,
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs
new file mode 100644
index 0000000000..ccf1582486
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/Section.cs
@@ -0,0 +1,130 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal abstract class Section : CompositeDrawable
+ where T : ControlPoint
+ {
+ private OsuCheckbox checkbox;
+ private Container content;
+
+ protected FillFlowContainer Flow { get; private set; }
+
+ protected Bindable ControlPoint { get; } = new Bindable();
+
+ private const float header_height = 20;
+
+ [Resolved]
+ protected IBindable Beatmap { get; private set; }
+
+ [Resolved]
+ protected Bindable SelectedGroup { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeDuration = 200;
+ AutoSizeEasing = Easing.OutQuint;
+ AutoSizeAxes = Axes.Y;
+
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray1,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = header_height,
+ Children = new Drawable[]
+ {
+ checkbox = new OsuCheckbox
+ {
+ LabelText = typeof(T).Name.Replace(typeof(ControlPoint).Name, string.Empty)
+ }
+ }
+ },
+ content = new Container
+ {
+ Y = header_height,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray2,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Flow = new FillFlowContainer
+ {
+ Padding = new MarginPadding(10),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ },
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ checkbox.Current.BindValueChanged(selected =>
+ {
+ if (selected.NewValue)
+ {
+ if (SelectedGroup.Value == null)
+ {
+ checkbox.Current.Value = false;
+ return;
+ }
+
+ if (ControlPoint.Value == null)
+ SelectedGroup.Value.Add(ControlPoint.Value = CreatePoint());
+ }
+ else
+ {
+ if (ControlPoint.Value != null)
+ {
+ SelectedGroup.Value.Remove(ControlPoint.Value);
+ ControlPoint.Value = null;
+ }
+ }
+
+ content.BypassAutoSizeAxes = selected.NewValue ? Axes.None : Axes.Y;
+ }, true);
+
+ SelectedGroup.BindValueChanged(points =>
+ {
+ ControlPoint.Value = points.NewValue?.ControlPoints.OfType().FirstOrDefault();
+ checkbox.Current.Value = ControlPoint.Value != null;
+ }, true);
+
+ ControlPoint.BindValueChanged(OnControlPointChanged, true);
+ }
+
+ protected abstract void OnControlPointChanged(ValueChangedEvent point);
+
+ protected abstract T CreatePoint();
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index 9ded4207e5..d9da3ff92d 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -1,13 +1,151 @@
// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
namespace osu.Game.Screens.Edit.Timing
{
- public class TimingScreen : EditorScreen
+ public class TimingScreen : EditorScreenWithTimeline
{
- public TimingScreen()
+ [Cached]
+ private Bindable selectedGroup = new Bindable();
+
+ [Resolved]
+ private IAdjustableClock clock { get; set; }
+
+ protected override Drawable CreateMainContent() => new GridContainer
{
- Child = new ScreenWhiteBox.UnderConstructionMessage("Timing mode");
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 200),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new ControlPointList(),
+ new ControlPointSettings(),
+ },
+ }
+ };
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected =>
+ {
+ if (selected.NewValue != null)
+ clock.Seek(selected.NewValue.Time);
+ });
+ }
+
+ public class ControlPointList : CompositeDrawable
+ {
+ private OsuButton deleteButton;
+ private ControlPointTable table;
+
+ private IBindableList controlGroups;
+
+ [Resolved]
+ private IFrameBasedClock clock { get; set; }
+
+ [Resolved]
+ protected IBindable Beatmap { get; private set; }
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = table = new ControlPointTable(),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding(10),
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ deleteButton = new OsuButton
+ {
+ Text = "-",
+ Size = new Vector2(30, 30),
+ Action = delete,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ new OsuButton
+ {
+ Text = "+",
+ Action = addNew,
+ Size = new Vector2(30, 30),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
+
+ controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
+ controlGroups.ItemsAdded += _ => createContent();
+ controlGroups.ItemsRemoved += _ => createContent();
+ createContent();
+ }
+
+ private void createContent() => table.ControlGroups = controlGroups;
+
+ private void delete()
+ {
+ if (selectedGroup.Value == null)
+ return;
+
+ Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
+
+ selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
+ }
+
+ private void addNew()
+ {
+ selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs
new file mode 100644
index 0000000000..906644ce14
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class TimingSection : Section
+ {
+ private SettingsSlider bpm;
+ private SettingsEnumDropdown timeSignature;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new Drawable[]
+ {
+ bpm = new BPMSlider
+ {
+ Bindable = new TimingControlPoint().BeatLengthBindable,
+ LabelText = "BPM",
+ },
+ timeSignature = new SettingsEnumDropdown
+ {
+ LabelText = "Time Signature"
+ },
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ bpm.Bindable = point.NewValue.BeatLengthBindable;
+ timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
+ }
+ }
+
+ protected override TimingControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time);
+
+ return new TimingControlPoint
+ {
+ BeatLength = reference.BeatLength,
+ TimeSignature = reference.TimeSignature
+ };
+ }
+
+ private class BPMSlider : SettingsSlider
+ {
+ private readonly BindableDouble beatLengthBindable = new BindableDouble();
+
+ private BindableDouble bpmBindable;
+
+ public override Bindable Bindable
+ {
+ get => base.Bindable;
+ set
+ {
+ // incoming will be beatlength
+
+ beatLengthBindable.UnbindBindings();
+ beatLengthBindable.BindTo(value);
+
+ base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
+ {
+ MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
+ MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
+ Default = beatLengthToBpm(beatLengthBindable.Default),
+ };
+
+ bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
+ }
+ }
+
+ private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
index e096fb33da..f79cac7649 100644
--- a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
+++ b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
Font = OsuFont.GetFont(size: TextSize),
}
- }, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
+ }, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
}
}
}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index f2efbe6073..2f2028ff53 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -162,16 +162,12 @@ namespace osu.Game.Screens.Play
if (sourceClock != beatmap.Track)
return;
+ removeSourceClockAdjustments();
+
sourceClock = new TrackVirtual(beatmap.Track.Length);
adjustableClock.ChangeSource(sourceClock);
}
- public void ResetLocalAdjustments()
- {
- // In the case of replays, we may have changed the playback rate.
- UserPlaybackRate.Value = 1;
- }
-
protected override void Update()
{
if (!IsPaused.Value)
@@ -198,6 +194,14 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+
+ removeSourceClockAdjustments();
+ sourceClock = null;
+ }
+
+ private void removeSourceClockAdjustments()
+ {
+ sourceClock.ResetSpeedAdjustments();
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index a3c39d9cc1..a9b0649fab 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -536,8 +536,6 @@ namespace osu.Game.Screens.Play
return true;
}
- GameplayClockContainer.ResetLocalAdjustments();
-
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer.StopUsingBeatmapClock();
diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs
index 3640197dad..d063988b3f 100644
--- a/osu.Game/Screens/Ranking/Results.cs
+++ b/osu.Game/Screens/Ranking/Results.cs
@@ -116,146 +116,147 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- InternalChildren = new Drawable[]
+ InternalChild = new AspectContainer
{
- new AspectContainer
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Height = overscan,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Y,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Height = overscan,
- Children = new Drawable[]
+ circleOuterBackground = new CircularContainer
{
- circleOuterBackground = new CircularContainer
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
+ new Box
{
- new Box
- {
- Alpha = 0.2f,
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- }
- }
- },
- circleOuter = new CircularContainer
- {
- Size = new Vector2(circle_outer_scale),
- EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.Black.Opacity(0.4f),
- Type = EdgeEffectType.Shadow,
- Radius = 15,
- },
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
- backgroundParallax = new ParallaxContainer
- {
- RelativeSizeAxes = Axes.Both,
- ParallaxAmount = 0.01f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
- {
- new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.2f,
- Texture = Beatmap.Value.Background,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- FillMode = FillMode.Fill
- }
- }
- },
- modeChangeButtons = new ResultModeTabControl
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- RelativeSizeAxes = Axes.X,
- Height = 50,
- Margin = new MarginPadding { Bottom = 110 },
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.BottomCentre,
- Text = $"{Score.MaxCombo}x",
- RelativePositionAxes = Axes.X,
- Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
- X = 0.1f,
- Colour = colours.BlueDarker,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopCentre,
- Text = "max combo",
- Font = OsuFont.GetFont(size: 20),
- RelativePositionAxes = Axes.X,
- X = 0.1f,
- Colour = colours.Gray6,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.BottomCentre,
- Text = $"{Score.Accuracy:P2}",
- Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
- RelativePositionAxes = Axes.X,
- X = 0.9f,
- Colour = colours.BlueDarker,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopCentre,
- Text = "accuracy",
- Font = OsuFont.GetFont(size: 20),
- RelativePositionAxes = Axes.X,
- X = 0.9f,
- Colour = colours.Gray6,
- },
- }
- },
- circleInner = new CircularContainer
- {
- Size = new Vector2(0.6f),
- EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.Black.Opacity(0.4f),
- Type = EdgeEffectType.Shadow,
- Radius = 15,
- },
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
+ Alpha = 0.2f,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
}
}
+ },
+ circleOuter = new CircularContainer
+ {
+ Size = new Vector2(circle_outer_scale),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.4f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 15,
+ },
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ },
+ backgroundParallax = new ParallaxContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ParallaxAmount = 0.01f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.2f,
+ Texture = Beatmap.Value.Background,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ FillMode = FillMode.Fill
+ }
+ }
+ },
+ modeChangeButtons = new ResultModeTabControl
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ Margin = new MarginPadding { Bottom = 110 },
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.BottomCentre,
+ Text = $"{Score.MaxCombo}x",
+ RelativePositionAxes = Axes.X,
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
+ X = 0.1f,
+ Colour = colours.BlueDarker,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.TopCentre,
+ Text = "max combo",
+ Font = OsuFont.GetFont(size: 20),
+ RelativePositionAxes = Axes.X,
+ X = 0.1f,
+ Colour = colours.Gray6,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.BottomCentre,
+ Text = $"{Score.Accuracy:P2}",
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
+ RelativePositionAxes = Axes.X,
+ X = 0.9f,
+ Colour = colours.BlueDarker,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.TopCentre,
+ Text = "accuracy",
+ Font = OsuFont.GetFont(size: 20),
+ RelativePositionAxes = Axes.X,
+ X = 0.9f,
+ Colour = colours.Gray6,
+ },
+ }
+ },
+ circleInner = new CircularContainer
+ {
+ Size = new Vector2(0.6f),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.4f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 15,
+ },
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ },
+ }
}
- },
- new HotkeyRetryOverlay
+ }
+ };
+
+ if (player != null)
+ {
+ AddInternal(new HotkeyRetryOverlay
{
Action = () =>
{
@@ -263,8 +264,8 @@ namespace osu.Game.Screens.Ranking
player?.Restart();
},
- },
- };
+ });
+ }
var pages = CreateResultPages();
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 337d46ecdd..3ef1fe5bc5 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return req;
}
- protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index)
+ protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope)
{
Action = () => ScoreSelected?.Invoke(model)
};
diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
index da8f676cd0..a787eb5629 100644
--- a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
+++ b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (newScore == null)
return;
- LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position)
+ LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position, false)
{
Action = () => ScoreSelected?.Invoke(newScore.Score)
}, drawableScore =>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 0cb09d9b14..dbe6559c40 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 719aced705..4010ef1d74 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -118,8 +118,8 @@
-
-
+
+
diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs
index 9ef21e014c..164a182ebe 100644
--- a/osu.iOS/AppDelegate.cs
+++ b/osu.iOS/AppDelegate.cs
@@ -4,7 +4,7 @@
using System.Threading.Tasks;
using Foundation;
using osu.Framework.iOS;
-using osu.Game;
+using osu.Framework.Threading;
using UIKit;
namespace osu.iOS
@@ -16,9 +16,12 @@ namespace osu.iOS
protected override Framework.Game CreateGame() => game = new OsuGameIOS();
- public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
+ public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
- Task.Run(() => game.Import(url.Path));
+ if (url.IsFileUrl)
+ Task.Run(() => game.Import(url.Path));
+ else
+ Task.Run(() => game.HandleLink(url.AbsoluteString));
return true;
}
}
diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist
index a118b329aa..5ceccdf99f 100644
--- a/osu.iOS/Info.plist
+++ b/osu.iOS/Info.plist
@@ -14,6 +14,8 @@
0.1.0
LSRequiresIPhoneOS
+ LSSupportsOpeningDocumentsInPlace
+
MinimumOSVersion
10.0
UIDeviceFamily
@@ -32,9 +34,9 @@
UIStatusBarHidden
NSCameraUsageDescription
- We don't really use the camera.
- NSMicrophoneUsageDescription
- We don't really use the microphone.
+ We don't really use the camera.
+ NSMicrophoneUsageDescription
+ We don't really use the microphone.
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
@@ -109,5 +111,17 @@
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ osu
+ osump
+
+ CFBundleTypeRole
+ Editor
+
+