diff --git a/osu.Android.props b/osu.Android.props
index eaad4daf35..f0f16d3763 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index aac77c9c1c..fd18907d96 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -14,7 +15,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
@@ -43,12 +43,18 @@ namespace osu.Game.Rulesets.Mania.Tests
});
}
+ protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
+ {
+ var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
+ var pos = column.ScreenSpacePositionAtTime(time);
+
+ return new ManiaSnapResult(pos, time, column);
+ }
+
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject);
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
index b598893e8c..35fe596e98 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
@@ -7,7 +7,6 @@ using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
@@ -18,11 +17,9 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock();
- private readonly Column column;
-
protected ManiaSelectionBlueprintTestScene()
{
- Add(column = new Column(0)
+ Add(new Column(0)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -31,8 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
}
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..ce9546415f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
@@ -0,0 +1,70 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [Cached(typeof(IManiaHitObjectComposer))]
+ public class TestSceneManiaBeatSnapGrid : EditorClockTestScene, IManiaHitObjectComposer
+ {
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
+
+ [Cached(typeof(EditorBeatmap))]
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()));
+
+ private readonly ManiaBeatSnapGrid beatSnapGrid;
+
+ public TestSceneManiaBeatSnapGrid()
+ {
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
+ editorBeatmap.ControlPointInfo.Add(10000, new TimingControlPoint { BeatLength = 200 });
+
+ BeatDivisor.Value = 3;
+
+ // Some sane defaults
+ scrollingInfo.Algorithm.Algorithm = ScrollVisualisationMethod.Constant;
+ scrollingInfo.Direction.Value = ScrollingDirection.Up;
+ scrollingInfo.TimeRange.Value = 1000;
+
+ Children = new Drawable[]
+ {
+ Playfield = new ManiaPlayfield(new List
+ {
+ new StageDefinition { Columns = 4 },
+ new StageDefinition { Columns = 3 }
+ })
+ {
+ Clock = new FramedClock(new StopwatchClock())
+ },
+ beatSnapGrid = new ManiaBeatSnapGrid()
+ };
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ // We're providing a constant scroll algorithm.
+ float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight;
+ double timeValue = scrollingInfo.TimeRange.Value * relativePosition;
+
+ beatSnapGrid.SelectionTimeRange = (timeValue, timeValue);
+
+ return true;
+ }
+
+ public ManiaPlayfield Playfield { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
index 6274bb1005..1a3fa29d4a 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void TestDragOffscreenSelectionVerticallyUpScroll()
{
DrawableHitObject lastObject = null;
+ double originalTime = 0;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Up);
@@ -49,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
@@ -64,19 +66,20 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse downwards", () =>
{
- InputManager.MoveMouseTo(lastObject, new Vector2(0, 20));
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
- AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
}
[Test]
public void TestDragOffscreenSelectionVerticallyDownScroll()
{
DrawableHitObject lastObject = null;
+ double originalTime = 0;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down);
@@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
@@ -99,13 +103,13 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse upwards", () =>
{
- InputManager.MoveMouseTo(lastObject, new Vector2(0, -20));
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
- AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
}
[Test]
@@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Tests
};
for (int i = 0; i < 10; i++)
- EditorBeatmap.Add(new Note { StartTime = 100 * i });
+ EditorBeatmap.Add(new Note { StartTime = 125 * i });
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index c63e30e98a..b757c17a48 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -2,8 +2,10 @@
// 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.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
@@ -17,6 +19,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private readonly EditNotePiece headPiece;
private readonly EditNotePiece tailPiece;
+ [Resolved]
+ private IManiaHitObjectComposer composer { get; set; }
+
public HoldNotePlacementBlueprint()
: base(new HoldNote())
{
@@ -36,8 +41,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column != null)
{
- headPiece.Y = PositionAt(HitObject.StartTime);
- tailPiece.Y = PositionAt(HitObject.EndTime);
+ headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
+ tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
}
var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y));
@@ -59,23 +64,28 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
- base.UpdatePosition(screenSpacePosition);
+ base.UpdatePosition(result);
if (PlacementActive)
{
- var endTime = TimeAt(screenSpacePosition);
-
- HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
- HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ if (result.Time is double endTime)
+ {
+ HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
+ HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ }
}
else
{
- headPiece.Width = tailPiece.Width = SnappedWidth;
- headPiece.X = tailPiece.X = SnappedMousePosition.X;
+ if (result is ManiaSnapResult maniaResult)
+ {
+ headPiece.Width = tailPiece.Width = maniaResult.Column.DrawWidth;
+ headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
- originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition);
+ if (result.Time is double startTime)
+ originalStartTime = HitObject.StartTime = startTime;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 43d43ef252..1737c4d2e5 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -77,6 +77,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
- public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 3fb03d642f..d173da9d9a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -1,43 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public abstract class ManiaPlacementBlueprint : PlacementBlueprint,
- IRequireHighFrequencyMousePosition // the playfield could be moving behind us
+ public abstract class ManiaPlacementBlueprint : PlacementBlueprint
where T : ManiaHitObject
{
protected new T HitObject => (T)base.HitObject;
- protected Column Column;
+ private Column column;
- ///
- /// The current mouse position, snapped to the closest column.
- ///
- protected Vector2 SnappedMousePosition { get; private set; }
+ public Column Column
+ {
+ get => column;
+ set
+ {
+ if (value == column)
+ return;
- ///
- /// The width of the closest column to the current mouse position.
- ///
- protected float SnappedWidth { get; private set; }
-
- [Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
+ column = value;
+ HitObject.Column = column.Index;
+ }
+ }
protected ManiaPlacementBlueprint(T hitObject)
: base(hitObject)
@@ -51,105 +42,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return false;
if (Column == null)
- return base.OnMouseDown(e);
+ return false;
- HitObject.Column = Column.Index;
- BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
+ BeginPlacement(true);
return true;
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
+ base.UpdatePosition(result);
+
if (!PlacementActive)
- Column = ColumnAt(screenSpacePosition);
-
- if (Column == null) return;
-
- SnappedWidth = Column.DrawWidth;
-
- // Snap to the column
- var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
- SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
- }
-
- protected double TimeAt(Vector2 screenSpacePosition)
- {
- if (Column == null)
- return 0;
-
- var hitObjectContainer = Column.HitObjectContainer;
-
- // If we're scrolling downwards, a position of 0 is actually further away from the hit target
- // so we need to flip the vertical coordinate in the hitobject container's space
- var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
-
- return scrollingInfo.Algorithm.TimeAt(hitObjectPos,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- hitObjectContainer.DrawHeight);
- }
-
- protected float PositionAt(double time)
- {
- var pos = scrollingInfo.Algorithm.PositionAt(time,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- Column.HitObjectContainer.DrawHeight);
-
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- pos = Column.HitObjectContainer.DrawHeight - pos;
-
- return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
- }
-
- protected Column ColumnAt(Vector2 screenSpacePosition)
- => composer.ColumnAt(screenSpacePosition);
-
- ///
- /// Converts a mouse position to a hitobject position.
- ///
- ///
- /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
- ///
- /// The mouse position.
- /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.
- private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return mousePosition;
- }
-
- ///
- /// Converts a hitobject position to a mouse position.
- ///
- /// The hitobject position.
- /// The resulting mouse position, anchored at the centre of the hitobject.
- private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- hitObjectPosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- hitObjectPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return hitObjectPosition;
+ Column = (result as ManiaSnapResult)?.Column;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index a4c0791253..5f6db2e6dd 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK.Input;
@@ -11,22 +12,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class NotePlacementBlueprint : ManiaPlacementBlueprint
{
+ private readonly EditNotePiece piece;
+
public NotePlacementBlueprint()
: base(new Note())
{
- Origin = Anchor.Centre;
+ RelativeSizeAxes = Axes.Both;
- AutoSizeAxes = Axes.Y;
-
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
+ InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
}
- protected override void Update()
+ public override void UpdatePosition(SnapResult result)
{
- base.Update();
+ base.UpdatePosition(result);
- Width = SnappedWidth;
- Position = SnappedMousePosition;
+ if (result is ManiaSnapResult maniaResult)
+ {
+ piece.Width = maniaResult.Column.DrawWidth;
+ piece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ }
}
protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
index f64bab1fae..3818d0e15d 100644
--- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
@@ -2,14 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mania.UI;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
public interface IManiaHitObjectComposer
{
- Column ColumnAt(Vector2 screenSpacePosition);
-
- int TotalColumns { get; }
+ ManiaPlayfield Playfield { get; }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..b5b6c08fca
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ ///
+ /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
+ ///
+ public class ManiaBeatSnapGrid : Component
+ {
+ private const double visible_range = 750;
+
+ ///
+ /// The range of time values of the current selection.
+ ///
+ public (double start, double end)? SelectionTimeRange
+ {
+ set
+ {
+ if (value == selectionTimeRange)
+ return;
+
+ selectionTimeRange = value;
+ lineCache.Invalidate();
+ }
+ }
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ [Resolved]
+ private Bindable working { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ [Resolved]
+ private BindableBeatDivisor beatDivisor { get; set; }
+
+ private readonly List grids = new List();
+
+ private readonly Cached lineCache = new Cached();
+
+ private (double start, double end)? selectionTimeRange;
+
+ [BackgroundDependencyLoader]
+ private void load(IManiaHitObjectComposer composer)
+ {
+ foreach (var stage in composer.Playfield.Stages)
+ {
+ foreach (var column in stage.Columns)
+ {
+ var lineContainer = new ScrollingHitObjectContainer();
+
+ grids.Add(lineContainer);
+ column.UnderlayElements.Add(lineContainer);
+ }
+ }
+
+ beatDivisor.BindValueChanged(_ => createLines(), true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!lineCache.IsValid)
+ {
+ lineCache.Validate();
+ createLines();
+ }
+ }
+
+ private readonly Stack availableLines = new Stack();
+
+ private void createLines()
+ {
+ foreach (var grid in grids)
+ {
+ foreach (var line in grid.Objects.OfType())
+ availableLines.Push(line);
+
+ grid.Clear(false);
+ }
+
+ if (selectionTimeRange == null)
+ return;
+
+ var range = selectionTimeRange.Value;
+
+ var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
+
+ double time = timingPoint.Time;
+ int beat = 0;
+
+ // progress time until in the visible range.
+ while (time < range.start - visible_range)
+ {
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ beat++;
+ }
+
+ while (time < range.end + visible_range)
+ {
+ var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
+
+ // switch to the next timing point if we have reached it.
+ if (nextTimingPoint.Time > timingPoint.Time)
+ {
+ beat = 0;
+ time = nextTimingPoint.Time;
+ timingPoint = nextTimingPoint;
+ }
+
+ Color4 colour = BindableBeatDivisor.GetColourFor(
+ BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
+
+ foreach (var grid in grids)
+ {
+ if (!availableLines.TryPop(out var line))
+ line = new DrawableGridLine();
+
+ line.HitObject.StartTime = time;
+ line.Colour = colour;
+
+ grid.Add(line);
+ }
+
+ beat++;
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ }
+
+ foreach (var grid in grids)
+ {
+ // required to update ScrollingHitObjectContainer's cache.
+ grid.UpdateSubTree();
+
+ foreach (var line in grid.Objects.OfType())
+ {
+ time = line.HitObject.StartTime;
+
+ if (time >= range.start && time <= range.end)
+ line.Alpha = 1;
+ else
+ {
+ double timeSeparation = time < range.start ? range.start - time : time - range.end;
+ line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
+ }
+ }
+ }
+ }
+
+ private class DrawableGridLine : DrawableHitObject
+ {
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ private readonly IBindable direction = new Bindable();
+
+ public DrawableGridLine()
+ : base(new HitObject())
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 2;
+
+ AddInternal(new Box { RelativeSizeAxes = Axes.Both });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ Origin = Anchor = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopLeft
+ : Anchor.BottomLeft;
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ // don't perform any fading – we are handling that ourselves.
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ LifetimeEnd = HitObject.StartTime + visible_range;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index dfa933baad..683e921cbf 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -6,9 +6,12 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Input;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
@@ -20,18 +23,26 @@ namespace osu.Game.Rulesets.Mania.Edit
public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
{
private DrawableManiaEditRuleset drawableRuleset;
+ private ManiaBeatSnapGrid beatSnapGrid;
+ private InputManager inputManager;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(beatSnapGrid = new ManiaBeatSnapGrid());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
private DependencyContainer dependencies;
@@ -42,28 +53,22 @@ namespace osu.Game.Rulesets.Mania.Edit
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
- public int TotalColumns => Playfield.TotalColumns;
-
- public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
- var hoc = Playfield.GetColumn(0).HitObjectContainer;
+ var column = Playfield.GetColumnByPosition(screenSpacePosition);
- float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y;
+ if (column == null)
+ return new SnapResult(screenSpacePosition, null);
- if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
- {
- // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
- // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
- // so when scrolling downwards the coordinates need to be flipped.
- targetPosition = hoc.DrawHeight - targetPosition;
- }
+ double targetTime = column.TimeAtScreenSpacePosition(screenSpacePosition);
- double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition,
- EditorClock.CurrentTime,
- drawableRuleset.ScrollingInfo.TimeRange.Value,
- hoc.DrawHeight);
+ // apply beat snapping
+ targetTime = BeatSnapProvider.SnapTime(targetTime);
- return base.GetSnappedPosition(position, targetTime);
+ // convert back to screen space
+ screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime);
+
+ return new ManiaSnapResult(screenSpacePosition, targetTime, column);
}
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
@@ -83,5 +88,28 @@ namespace osu.Game.Rulesets.Mania.Edit
new NoteCompositionTool(),
new HoldNoteCompositionTool()
};
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (BlueprintContainer.CurrentTool is SelectTool)
+ {
+ if (EditorBeatmap.SelectedHitObjects.Any())
+ {
+ beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
+ }
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ else
+ {
+ var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
+ if (result.Time is double time)
+ beatSnapGrid.SelectionTimeRange = (time, time);
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 55245198c8..4ea71652bc 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Edit
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
- var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);
+ var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
if (currentColumn == null)
return;
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
- columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
+ columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType())
obj.Column += columnDelta;
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs
new file mode 100644
index 0000000000..b94f5e51c4
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ public class ManiaSnapResult : SnapResult
+ {
+ public readonly Column Column;
+
+ public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column)
+ : base(screenSpacePosition, time)
+ {
+ Column = column;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 506a07f26b..be31954099 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -37,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
+ public Container UnderlayElements => hitObjectArea.UnderlayElements;
+
public Column(int index)
{
Index = index;
@@ -140,5 +143,54 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
+
+ ///
+ /// Given a time, return the screen space position within this column.
+ ///
+ public Vector2 ScreenSpacePositionAtTime(double time)
+ {
+ var pos = ScrollingInfo.Algorithm.PositionAt(time, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight);
+
+ switch (ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
+ // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
+ // so when scrolling downwards the coordinates need to be flipped.
+ pos = HitObjectContainer.DrawHeight - pos;
+
+ // Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
+ pos -= DefaultNotePiece.NOTE_HEIGHT / 2;
+ break;
+
+ case ScrollingDirection.Up:
+ pos += DefaultNotePiece.NOTE_HEIGHT / 2;
+ break;
+ }
+
+ return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos));
+ }
+
+ ///
+ /// Given a position in screen space, return the time within this column.
+ ///
+ public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
+ {
+ // convert to local space of column so we can snap and fetch correct location.
+ Vector2 localPosition = HitObjectContainer.ToLocalSpace(screenSpacePosition);
+
+ switch (ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ // as above
+ localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y;
+ break;
+ }
+
+ // offset for the fact that blueprints are centered, as above.
+ localPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
+
+ return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index cb79bf7f43..b365ae45a9 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class ColumnHitObjectArea : HitObjectArea
{
public readonly Container Explosions;
+
+ public readonly Container UnderlayElements;
+
private readonly Drawable hitTarget;
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
@@ -19,6 +22,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
AddRangeInternal(new[]
{
+ UnderlayElements = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 2,
+ },
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index f3f843f366..94b5ee9486 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -108,13 +107,6 @@ namespace osu.Game.Rulesets.Mania.UI
private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition);
-
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 1af7d06998..271e432e8d 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.UI
[Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
+ public IReadOnlyList Stages => stages;
+
private readonly List stages = new List();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
index c182aa5d63..0d0be2953b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
- [Cached(typeof(IDistanceSnapProvider))]
+ [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
private TestOsuDistanceSnapGrid grid;
@@ -172,9 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests
}
}
- private class SnapProvider : IDistanceSnapProvider
+ private class SnapProvider : IPositionSnapProvider
{
- public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index dad199715e..3dbbdcc5d0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -5,7 +5,6 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
@@ -40,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e);
}
- public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
+ public override void UpdatePosition(SnapResult result)
+ {
+ base.UpdatePosition(result);
+ HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
+ }
}
}
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 d0c1eb5317..c06904c0c2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
- private IDistanceSnapProvider snapProvider { get; set; }
+ private IPositionSnapProvider snapProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@@ -162,11 +162,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
- (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
- Vector2 movementDelta = snappedPosition - slider.Position;
+ var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition);
+
+ Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position;
slider.Position += movementDelta;
- slider.StartTime = snappedTime;
+ slider.StartTime = result?.Time ?? slider.StartTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index ac30f5a762..4b99cc23ed 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -67,13 +67,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
+ base.UpdatePosition(result);
+
switch (state)
{
case PlacementState.Initial:
BeginPlacement();
- HitObject.Position = ToLocalSpace(screenSpacePosition);
+ HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
case PlacementState.Body:
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index b7074b7ee5..6633136673 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
};
- public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index 74b563d922..cc4ed0eccf 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
@@ -60,9 +59,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
-
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 38026517d9..06ccd45cb8 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Audio;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@@ -145,6 +146,9 @@ namespace osu.Game.Rulesets.Taiko.UI
centreHit.Colour = colours.Pink;
}
+ [Resolved(canBeNull: true)]
+ private GameplayClock gameplayClock { get; set; }
+
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
@@ -157,14 +161,16 @@ namespace osu.Game.Rulesets.Taiko.UI
target = centreHit;
back = centre;
- drumSample.Centre?.Play();
+ if (gameplayClock?.IsSeeking != true)
+ drumSample.Centre?.Play();
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
- drumSample.Rim?.Play();
+ if (gameplayClock?.IsSeeking != true)
+ drumSample.Rim?.Play();
}
if (target != null)
diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs
new file mode 100644
index 0000000000..e663e1128e
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs
@@ -0,0 +1,73 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Tests.NonVisual
+{
+ public class BarLineGeneratorTest
+ {
+ [Test]
+ public void TestRoundingErrorCompensation()
+ {
+ // The aim of this test is to make sure bar line generation compensates for floating-point errors.
+ // The premise of the test is that we have a single timing point that should result in bar lines
+ // that start at a time point that is a whole number every seventh beat.
+
+ // The fact it's every seventh beat is important - it's a number indivisible by 2, which makes
+ // it susceptible to rounding inaccuracies. In fact this was originally spotted in cases of maps
+ // that met exactly this criteria.
+
+ const int beat_length_numerator = 2000;
+ const int beat_length_denominator = 7;
+ const TimeSignatures signature = TimeSignatures.SimpleQuadruple;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitObject { StartTime = 0 },
+ new HitObject { StartTime = 120_000 }
+ },
+ ControlPointInfo = new ControlPointInfo()
+ };
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint
+ {
+ BeatLength = (double)beat_length_numerator / beat_length_denominator,
+ TimeSignature = signature
+ });
+
+ var barLines = new BarLineGenerator(beatmap).BarLines;
+
+ for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
+ {
+ var barLine = barLines[i * beat_length_denominator];
+ var expectedTime = beat_length_numerator * (int)signature * i;
+
+ // every seventh bar's start time should be at least greater than the whole number we expect.
+ // It cannot be less, as that can affect overlapping scroll algorithms
+ // (the previous timing point might be chosen incorrectly if this is not the case)
+ Assert.GreaterOrEqual(barLine.StartTime, expectedTime);
+
+ // on the other side, make sure we don't stray too far from the expected time either.
+ Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
+
+ // check major/minor lines for good measure too
+ Assert.AreEqual(i % (int)signature == 0, barLine.Major);
+ }
+ }
+
+ private class BarLine : IBarLine
+ {
+ public double StartTime { get; set; }
+ public bool Major { get; set; }
+ }
+ }
+}
diff --git a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
index 1f0c069f8d..bd578dcbc4 100644
--- a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
+++ b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
@@ -29,8 +29,22 @@ namespace osu.Game.Tests.ScrollAlgorithms
[Test]
public void TestDisplayStartTime()
{
- // Sequential scroll algorithm approximates the start time
- // This should be fixed in the future
+ // easy cases - time range adjusted for velocity fits within control point duration
+ Assert.AreEqual(2500, algorithm.GetDisplayStartTime(5000, 0, 2500, 1)); // 5000 - (2500 / 1)
+ Assert.AreEqual(13750, algorithm.GetDisplayStartTime(15000, 0, 2500, 1)); // 15000 - (2500 / 2)
+ Assert.AreEqual(20000, algorithm.GetDisplayStartTime(25000, 0, 2500, 1)); // 25000 - (2500 / 0.5)
+
+ // hard case - time range adjusted for velocity exceeds control point duration
+
+ // 1st multiplier point takes 10000 / 2500 = 4 scroll lengths
+ // 2nd multiplier point takes 10000 / (2500 / 2) = 8 scroll lengths
+ // 3rd multiplier point takes 2500 / (2500 * 2) = 0.5 scroll lengths up to hitobject start
+
+ // absolute position of the hitobject = 1000 * (4 + 8 + 0.5) = 12500
+ // minus one scroll length allowance = 12500 - 1000 = 11500 = 11.5 [scroll lengths]
+ // therefore the start time lies within the second multiplier point (because 11.5 < 4 + 8)
+ // its exact time position is = 10000 + 7.5 * (2500 / 2) = 19375
+ Assert.AreEqual(19375, algorithm.GetDisplayStartTime(22500, 0, 2500, 1000));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 417d16fdb0..8190cf5f89 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap;
- [Cached(typeof(IDistanceSnapProvider))]
+ [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
public TestSceneDistanceSnapGrid()
@@ -151,9 +151,9 @@ namespace osu.Game.Tests.Visual.Editing
=> (Vector2.Zero, 0);
}
- private class SnapProvider : IDistanceSnapProvider
+ private class SnapProvider : IPositionSnapProvider
{
- public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 0d15e495e3..2f15e549f7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -77,19 +78,18 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
- setUpHitObjects();
+ hitObjectSpawnDelegate?.Cancel();
});
- private void setUpHitObjects()
+ private void setUpHitObjects() => AddStep("set up hit objects", () =>
{
scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate)
addHitObject(Time.Current + i);
- hitObjectSpawnDelegate?.Cancel();
hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true);
- }
+ });
private IList testControlPoints => new List
{
@@ -101,6 +101,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestScrollAlgorithms()
{
+ setUpHitObjects();
+
AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
@@ -113,6 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestConstantScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
// scroll container time range must be less than the rate of spawning hitobjects
// otherwise the hitobjects will spawn already partly visible on screen and look wrong
@@ -122,14 +126,40 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSequentialScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
+ [Test]
+ public void TestSlowSequentialScroll()
+ {
+ AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
+ AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range));
+ AddStep("add control points", () => addControlPoints(
+ new List
+ {
+ new MultiplierControlPoint { Velocity = 0.1 }
+ },
+ Time.Current + time_range));
+
+ // All of the hit objects added below should be immediately visible on screen
+ AddStep("add hit objects", () =>
+ {
+ for (int i = 0; i < 20; ++i)
+ {
+ addHitObject(Time.Current + time_range * (2 + 0.1 * i));
+ }
+ });
+ }
+
[Test]
public void TestOverlappingScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
@@ -221,7 +251,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestDrawableControlPoint : DrawableHitObject
{
public TestDrawableControlPoint(ScrollingDirection direction, double time)
- : base(new HitObject { StartTime = time })
+ : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Centre;
@@ -252,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestDrawableHitObject : DrawableHitObject
{
public TestDrawableHitObject(double time)
- : base(new HitObject { StartTime = time })
+ : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Custom;
OriginPosition = new Vector2(75 / 4.0f);
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 7aaf0ca08d..b286c054e9 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps
///
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
///
- public partial class BeatmapManager : DownloadableArchiveModelManager
+ public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable
{
///
/// Fired when a single difficulty has been hidden.
@@ -433,6 +433,11 @@ namespace osu.Game.Beatmaps
return endTime - startTime;
}
+ public void Dispose()
+ {
+ onlineLookupQueue?.Dispose();
+ }
+
///
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
///
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 2c79a664c5..d47d37806e 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
- private class BeatmapOnlineLookupQueue
+ private class BeatmapOnlineLookupQueue : IDisposable
{
private readonly IAPIProvider api;
private readonly Storage storage;
@@ -180,6 +180,11 @@ namespace osu.Game.Beatmaps
return false;
}
+ public void Dispose()
+ {
+ cacheDownloadRequest?.Dispose();
+ }
+
[Serializable]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private class CachedOnlineBeatmapLookup
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index c367c3b636..453587df18 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -337,6 +337,7 @@ namespace osu.Game
{
base.Dispose(isDisposing);
RulesetStore?.Dispose();
+ BeatmapManager?.Dispose();
contextFactory.FlushConnections();
}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 883288d6d7..b437d81054 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Edit
private IAdjustableClock adjustableClock { get; set; }
[Resolved]
- private IBeatSnapProvider beatSnapProvider { get; set; }
+ protected IBeatSnapProvider BeatSnapProvider { get; private set; }
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
@@ -244,9 +244,6 @@ namespace osu.Game.Rulesets.Edit
public void BeginPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = hitObject;
-
- if (distanceSnapGrid != null)
- hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time;
}
public void EndPlacement(HitObject hitObject, bool commit)
@@ -257,7 +254,8 @@ namespace osu.Game.Rulesets.Edit
{
EditorBeatmap.Add(hitObject);
- adjustableClock.Seek(hitObject.GetEndTime());
+ if (adjustableClock.CurrentTime < hitObject.StartTime)
+ adjustableClock.Seek(hitObject.StartTime);
}
showGridFor(Enumerable.Empty());
@@ -265,40 +263,48 @@ namespace osu.Game.Rulesets.Edit
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
- public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time);
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ if (distanceSnapGrid == null) return new SnapResult(screenSpacePosition, null);
+
+ // TODO: move distance snap grid to OsuHitObjectComposer.
+ (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
+
+ return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time);
+ }
public override float GetBeatSnapDistanceAt(double referenceTime)
{
DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
- return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / beatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
}
public override float DurationToDistance(double referenceTime, double duration)
{
- double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
}
public override double DistanceToDuration(double referenceTime, float distance)
{
- double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
- => beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
+ => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
{
- var snappedEndTime = beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime);
+ var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime);
return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
}
}
[Cached(typeof(HitObjectComposer))]
- [Cached(typeof(IDistanceSnapProvider))]
- public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider
+ [Cached(typeof(IPositionSnapProvider))]
+ public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
internal HitObjectComposer()
{
@@ -323,7 +329,7 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null;
- public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
+ public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public abstract float GetBeatSnapDistanceAt(double referenceTime);
diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
similarity index 87%
rename from osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
rename to osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
index c6e61f68da..c854c06031 100644
--- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
@@ -5,9 +5,14 @@ using osuTK;
namespace osu.Game.Rulesets.Edit
{
- public interface IDistanceSnapProvider
+ public interface IPositionSnapProvider
{
- (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
+ ///
+ /// Given a position, find a valid time snap.
+ ///
+ /// The screen-space position to be snapped.
+ /// The time and position post-snapping.
+ SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
///
/// Retrieves the distance between two points within a timing point that are one beat length apart.
diff --git a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
index b4ae3f3fba..8202d3a1d1 100644
--- a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Edit
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
- public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index fb1eb7adbf..e71ccc33a4 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -61,11 +61,9 @@ namespace osu.Game.Rulesets.Edit
///
/// Signals that the placement of has started.
///
- /// The start time of at the placement point. If null, the current clock time is used.
/// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.
- protected void BeginPlacement(double? startTime = null, bool commitStart = false)
+ protected void BeginPlacement(bool commitStart = false)
{
- HitObject.StartTime = startTime ?? EditorClock.CurrentTime;
placementHandler.BeginPlacement(HitObject);
PlacementActive |= commitStart;
}
@@ -83,11 +81,18 @@ namespace osu.Game.Rulesets.Edit
PlacementActive = false;
}
+ [Resolved(canBeNull: true)]
+ private IFrameBasedClock editorClock { get; set; }
+
///
/// Updates the position of this to a new screen-space position.
///
- /// The screen-space position.
- public abstract void UpdatePosition(Vector2 screenSpacePosition);
+ /// The snap result information.
+ public virtual void UpdatePosition(SnapResult snapResult)
+ {
+ if (!PlacementActive)
+ HitObject.StartTime = snapResult.Time ?? editorClock?.CurrentTime ?? Time.Current;
+ }
///
/// Invokes ,
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index e6a63eae4f..71256093d5 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Edit
///
/// The screen-space point that causes this to be selected.
///
- public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
+ public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
///
/// The screen-space quad that outlines this for selections.
diff --git a/osu.Game/Rulesets/Edit/SnapResult.cs b/osu.Game/Rulesets/Edit/SnapResult.cs
new file mode 100644
index 0000000000..5d07d7b233
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/SnapResult.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+
+namespace osu.Game.Rulesets.Edit
+{
+ ///
+ /// The result of a position/time snapping process.
+ ///
+ public class SnapResult
+ {
+ ///
+ /// The screen space position, potentially altered for snapping.
+ ///
+ public Vector2 ScreenSpacePosition;
+
+ ///
+ /// The resultant time for snapping, if a value could be attained.
+ ///
+ public double? Time;
+
+ public SnapResult(Vector2 screenSpacePosition, double? time)
+ {
+ ScreenSpacePosition = screenSpacePosition;
+ Time = time;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index 5588e9c0b7..9556b52735 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
@@ -46,6 +47,16 @@ namespace osu.Game.Rulesets.Objects
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
{
+ var roundedTime = Math.Round(t, MidpointRounding.AwayFromZero);
+
+ // in the case of some bar lengths, rounding errors can cause t to be slightly less than
+ // the expected whole number value due to floating point inaccuracies.
+ // if this is the case, apply rounding.
+ if (Precision.AlmostEquals(t, roundedTime))
+ {
+ t = roundedTime;
+ }
+
BarLines.Add(new TBarLine
{
StartTime = t,
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 33ea02c22f..44afb7a227 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -11,13 +11,13 @@ using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading;
-using osu.Framework.Audio;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Configuration;
+using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@@ -96,8 +96,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
protected virtual float SamplePlaybackPosition => 0.5f;
- private readonly BindableDouble balanceAdjust = new BindableDouble();
-
private BindableList samplesBindable;
private Bindable startTimeBindable;
private Bindable userPositionalHitSounds;
@@ -173,7 +171,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
- Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust);
AddInternal(Samples);
}
@@ -260,7 +257,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
- if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null)
+ if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null))
Expire();
// apply any custom state overrides
@@ -352,6 +349,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
}
+ [Resolved(canBeNull: true)]
+ private GameplayClock gameplayClock { get; set; }
+
///
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
@@ -360,8 +360,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
const float balance_adjust_amount = 0.4f;
- balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
- Samples?.Play();
+ if (Samples != null && gameplayClock?.IsSeeking != true)
+ {
+ Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
+ Samples.Play();
+ }
}
protected override void Update()
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index 3ba28aad45..bc9401a095 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -29,14 +29,16 @@ namespace osu.Game.Rulesets.UI
///
internal bool FrameStablePlayback = true;
- [Cached]
- public GameplayClock GameplayClock { get; }
+ public GameplayClock GameplayClock => stabilityGameplayClock;
+
+ [Cached(typeof(GameplayClock))]
+ private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
{
RelativeSizeAxes = Axes.Both;
- GameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
+ stabilityGameplayClock = new StabilityGameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
this.gameplayStartTime = gameplayStartTime;
}
@@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.UI
{
if (clock != null)
{
- parentGameplayClock = clock;
+ stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock;
GameplayClock.IsPaused.BindTo(clock.IsPaused);
}
}
@@ -187,5 +189,17 @@ namespace osu.Game.Rulesets.UI
}
public ReplayInputHandler ReplayInputHandler { get; set; }
+
+ private class StabilityGameplayClock : GameplayClock
+ {
+ public IFrameBasedClock ParentGameplayClock;
+
+ public StabilityGameplayClock(FramedClock underlyingClock)
+ : base(underlyingClock)
+ {
+ }
+
+ public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200;
+ }
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
index 41f9ebdb82..0052c877f6 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
@@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
{
- double adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength);
- return adjustedTime - timeRange - 1000;
+ return TimeAt(-(scrollLength + offset), originTime, timeRange, scrollLength);
}
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index bf2203e176..fd143a3687 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected readonly IBindable Direction = new Bindable();
[Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
+ protected IScrollingInfo ScrollingInfo { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
- Direction.BindTo(scrollingInfo.Direction);
+ Direction.BindTo(ScrollingInfo.Direction);
}
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 8910684463..e38df3d812 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly BindableList selectedHitObjects = new BindableList();
[Resolved(canBeNull: true)]
- private IDistanceSnapProvider snapProvider { get; set; }
+ private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer()
{
@@ -326,7 +326,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
foreach (var blueprint in SelectionBlueprints)
{
- if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
+ if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Select();
else
blueprint.Deselect();
@@ -384,7 +384,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
- movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct
+ movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
}
///
@@ -405,16 +405,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// Retrieve a snapped position.
- (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
+ var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects.
- if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
+ if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
return true;
- // Apply the start time at the newly snapped-to position
- double offset = snappedTime - draggedObject.StartTime;
- foreach (HitObject obj in selectionHandler.SelectedHitObjects)
- obj.StartTime += offset;
+ if (result.Time.HasValue)
+ {
+ // Apply the start time at the newly snapped-to position
+ double offset = result.Time.Value - draggedObject.StartTime;
+ foreach (HitObject obj in selectionHandler.SelectedHitObjects)
+ obj.StartTime += offset;
+ }
return true;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 0eb77a8561..0b5d8262fd 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -11,7 +11,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -65,12 +64,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
createPlacement();
}
- private void updatePlacementPosition(Vector2 screenSpacePosition)
+ private void updatePlacementPosition()
{
- Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition), 0).position;
- Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition);
+ var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
- currentPlacement.UpdatePosition(snappedScreenSpacePosition);
+ currentPlacement.UpdatePosition(snapResult);
}
#endregion
@@ -85,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
removePlacement();
if (currentPlacement != null)
- updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
+ updatePlacementPosition();
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
@@ -117,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
- updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
+ updatePlacementPosition();
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 3a42938fc1..8a92a2011d 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected OsuColour Colours { get; private set; }
[Resolved]
- protected IDistanceSnapProvider SnapProvider { get; private set; }
+ protected IPositionSnapProvider SnapProvider { get; private set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 25f3cfc285..61ed1743a9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -17,9 +17,9 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- [Cached(typeof(IDistanceSnapProvider))]
+ [Cached(typeof(IPositionSnapProvider))]
[Cached]
- public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider
+ public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{
public readonly Bindable WaveformVisible = new Bindable();
public readonly IBindable Beatmap = new Bindable();
@@ -181,11 +181,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
- public double GetTimeFromScreenSpacePosition(Vector2 position)
- => getTimeFromPosition(Content.ToLocalSpace(position));
-
- public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) =>
- (position, beatSnapProvider.SnapTime(getTimeFromPosition(position)));
+ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
+ new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length;
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 16ba3ba89a..dd2f7a833e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
- public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft;
+ public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public class DragBar : Container
{
@@ -275,32 +275,33 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
OnDragHandled?.Invoke(e);
- var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition);
-
- switch (hitObject)
+ if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
{
- case IHasRepeats repeatHitObject:
- // find the number of repeats which can fit in the requested time.
- var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
- var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
+ switch (hitObject)
+ {
+ case IHasRepeats repeatHitObject:
+ // find the number of repeats which can fit in the requested time.
+ var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
+ var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
- if (proposedCount == repeatHitObject.RepeatCount)
- return;
+ if (proposedCount == repeatHitObject.RepeatCount)
+ return;
- repeatHitObject.RepeatCount = proposedCount;
- break;
+ repeatHitObject.RepeatCount = proposedCount;
+ break;
- case IHasEndTime endTimeHitObject:
- var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
+ case IHasEndTime endTimeHitObject:
+ var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
- if (endTimeHitObject.EndTime == snappedTime)
- return;
+ if (endTimeHitObject.EndTime == snappedTime)
+ return;
- endTimeHitObject.EndTime = snappedTime;
- break;
+ endTimeHitObject.EndTime = snappedTime;
+ break;
+ }
+
+ beatmap.UpdateHitObject(hitObject);
}
-
- beatmap.UpdateHitObject(hitObject);
}
protected override void OnDragEnd(DragEndEvent e)
diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs
index d5f75f6ad1..4f2cf5005c 100644
--- a/osu.Game/Screens/Play/GameplayClock.cs
+++ b/osu.Game/Screens/Play/GameplayClock.cs
@@ -31,6 +31,11 @@ namespace osu.Game.Screens.Play
public bool IsRunning => underlyingClock.IsRunning;
+ ///
+ /// Whether an ongoing seek operation is active.
+ ///
+ public virtual bool IsSeeking => false;
+
public void ProcessFrame()
{
// we do not want to process the underlying clock.
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index a78c04ecd4..30320c89a6 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -4,11 +4,11 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
namespace osu.Game.Skinning
@@ -17,25 +17,32 @@ namespace osu.Game.Skinning
{
private readonly ISampleInfo[] hitSamples;
- private List<(AdjustableProperty property, BindableDouble bindable)> adjustments;
-
- private SampleChannel[] channels;
-
[Resolved]
private ISampleStore samples { get; set; }
+ public SkinnableSound(ISampleInfo hitSamples)
+ : this(new[] { hitSamples })
+ {
+ }
+
public SkinnableSound(IEnumerable hitSamples)
{
this.hitSamples = hitSamples.ToArray();
- }
-
- public SkinnableSound(ISampleInfo hitSamples)
- {
- this.hitSamples = new[] { hitSamples };
+ InternalChild = samplesContainer = new AudioContainer();
}
private bool looping;
+ private readonly AudioContainer samplesContainer;
+
+ public BindableNumber Volume => samplesContainer.Volume;
+
+ public BindableNumber Balance => samplesContainer.Balance;
+
+ public BindableNumber Frequency => samplesContainer.Frequency;
+
+ public BindableNumber Tempo => samplesContainer.Tempo;
+
public bool Looping
{
get => looping;
@@ -45,33 +52,23 @@ namespace osu.Game.Skinning
looping = value;
- channels?.ForEach(c => c.Looping = looping);
+ samplesContainer.ForEach(c => c.Looping = looping);
}
}
- public void Play() => channels?.ForEach(c => c.Play());
-
- public void Stop() => channels?.ForEach(c => c.Stop());
-
- public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable)
+ public void Play() => samplesContainer.ForEach(c =>
{
- if (adjustments == null) adjustments = new List<(AdjustableProperty, BindableDouble)>();
+ if (c.AggregateVolume.Value > 0)
+ c.Play();
+ });
- adjustments.Add((type, adjustBindable));
- channels?.ForEach(c => c.AddAdjustment(type, adjustBindable));
- }
-
- public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable)
- {
- adjustments?.Remove((type, adjustBindable));
- channels?.ForEach(c => c.RemoveAdjustment(type, adjustBindable));
- }
+ public void Stop() => samplesContainer.ForEach(c => c.Stop());
public override bool IsPresent => Scheduler.HasPendingTasks;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
- channels = hitSamples.Select(s =>
+ var channels = hitSamples.Select(s =>
{
var ch = skin.GetSample(s);
@@ -88,27 +85,12 @@ namespace osu.Game.Skinning
{
ch.Looping = looping;
ch.Volume.Value = s.Volume / 100.0;
-
- if (adjustments != null)
- {
- foreach (var (property, bindable) in adjustments)
- ch.AddAdjustment(property, bindable);
- }
}
return ch;
- }).Where(c => c != null).ToArray();
- }
+ }).Where(c => c != null);
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (channels != null)
- {
- foreach (var c in channels)
- c.Dispose();
- }
+ samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
}
}
}
diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
index dc67d28f63..feecea473c 100644
--- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
+++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
@@ -71,9 +71,12 @@ namespace osu.Game.Tests.Visual
{
base.Update();
- currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position);
+ currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint));
}
+ protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>
+ new SnapResult(InputManager.CurrentState.Mouse.Position, null);
+
public override void Add(Drawable drawable)
{
base.Add(drawable);
@@ -81,7 +84,7 @@ namespace osu.Game.Tests.Visual
if (drawable is PlacementBlueprint blueprint)
{
blueprint.Show();
- blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position);
+ blueprint.UpdatePosition(SnapForBlueprint(blueprint));
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9112dfe46e..010ef8578a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 3f0630af5f..88b0c7dd8a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+