diff --git a/.github/ISSUE_TEMPLATE/mobile-issues.md b/.github/ISSUE_TEMPLATE/mobile-issues.md new file mode 100644 index 0000000000..f171e80b8b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/mobile-issues.md @@ -0,0 +1,8 @@ +--- +name: Mobile Report +about: ⚠ Due to current development priorities we are not accepting mobile reports at this time (unless you're willing to fix them yourself!) +--- + +⚠ **PLEASE READ** ⚠: Due to prioritising finishing the client for desktop first we are not accepting reports related to mobile platforms for the time being, unless you're willing to fix them. +If you'd like to report a problem or suggest a feature and then work on it, feel free to open an issue and highlight that you'd like to address it yourself in the issue body; mobile pull requests are also welcome. +Otherwise, please check back in the future when the focus of development shifts towards mobile! diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index 6b8daa531f..b6907cf7c0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.MathUtils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; @@ -38,26 +39,34 @@ namespace osu.Game.Rulesets.Osu.Tests [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private TestOsuDistanceSnapGrid grid; + [Cached(typeof(IDistanceSnapProvider))] + private readonly SnapProvider snapProvider = new SnapProvider(); + + private readonly TestOsuDistanceSnapGrid grid; public TestSceneOsuDistanceSnapGrid() { editorBeatmap = new EditorBeatmap(new OsuBeatmap()); - createGrid(); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray + }, + grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), + new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } + }; } [SetUp] public void Setup() => Schedule(() => { - Clear(); - editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); editorBeatmap.ControlPointInfo.TimingPoints.Clear(); editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length }); - - beatDivisor.Value = 1; }); [TestCase(1)] @@ -71,53 +80,11 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestBeatDivisor(int divisor) { AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor); - createGrid(); - } - - [TestCase(100, 100)] - [TestCase(200, 100)] - public void TestBeatLength(float beatLength, float expectedSpacing) - { - AddStep($"set beat length = {beatLength}", () => - { - editorBeatmap.ControlPointInfo.TimingPoints.Clear(); - editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beatLength }); - }); - - createGrid(); - AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing)); - } - - [TestCase(0.5f, 50)] - [TestCase(1, 100)] - [TestCase(1.5f, 150)] - public void TestSpeedMultiplier(float multiplier, float expectedSpacing) - { - AddStep($"set speed multiplier = {multiplier}", () => - { - editorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); - editorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = multiplier }); - }); - - createGrid(); - AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing)); - } - - [TestCase(0.5f, 50)] - [TestCase(1, 100)] - [TestCase(1.5f, 150)] - public void TestSliderMultiplier(float multiplier, float expectedSpacing) - { - AddStep($"set speed multiplier = {multiplier}", () => editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier); - createGrid(); - AddAssert($"spacing = {expectedSpacing}", () => Precision.AlmostEquals(expectedSpacing, grid.DistanceSpacing)); } [Test] public void TestCursorInCentre() { - createGrid(); - AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position))); assertSnappedDistance((float)beat_length); } @@ -125,8 +92,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestCursorBeforeMovementPoint() { - createGrid(); - AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f))); assertSnappedDistance((float)beat_length); } @@ -134,37 +99,17 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestCursorAfterMovementPoint() { - createGrid(); - AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f))); assertSnappedDistance((float)beat_length * 2); } private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => { - Vector2 snappedPosition = grid.GetSnapPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)); - float distance = Vector2.Distance(snappedPosition, grid_position); + Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position; - return Precision.AlmostEquals(expectedDistance, distance); + return Precision.AlmostEquals(expectedDistance, Vector2.Distance(snappedPosition, grid_position)); }); - private void createGrid() - { - AddStep("create grid", () => - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.SlateGray - }, - grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), - new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnapPosition(grid.ToLocalSpace(v)) } - }; - }); - } - private class SnappingCursorContainer : CompositeDrawable { public Func GetSnapPosition; @@ -213,5 +158,20 @@ namespace osu.Game.Rulesets.Osu.Tests { } } + + private class SnapProvider : IDistanceSnapProvider + { + public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); + + public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; + + public float DurationToDistance(double referenceTime, double duration) => 0; + + public double DistanceToDuration(double referenceTime, float distance) => 0; + + public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + + public float GetSnappedDistanceFromDistance(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 3aec7c2872..7afb8fcf49 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -8,7 +9,6 @@ using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public class PathControlPointPiece : BlueprintPiece { + public Action ControlPointsChanged; + private readonly Slider slider; private readonly int index; @@ -96,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (isSegmentSeparatorWithPrevious) newControlPoints[index - 1] = newControlPoints[index]; - slider.Path = new SliderPath(slider.Path.Type, newControlPoints); + ControlPointsChanged?.Invoke(newControlPoints); return 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 24fcc460d1..0385824b27 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -1,14 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Objects; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public class PathControlPointVisualiser : CompositeDrawable { + public Action ControlPointsChanged; + private readonly Slider slider; private readonly Container pieces; @@ -25,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components base.Update(); while (slider.Path.ControlPoints.Length > pieces.Count) - pieces.Add(new PathControlPointPiece(slider, pieces.Count)); + pieces.Add(new PathControlPointPiece(slider, pieces.Count) { ControlPointsChanged = c => ControlPointsChanged?.Invoke(c) }); while (slider.Path.ControlPoints.Length < pieces.Count) pieces.Remove(pieces[pieces.Count - 1]); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index b7b8d0af88..6f5309c2c2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -33,6 +33,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private PlacementState state; + [Resolved(CanBeNull = true)] + private HitObjectComposer composer { get; set; } + public SliderPlacementBlueprint() : base(new Objects.Slider()) { @@ -48,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders bodyPiece = new SliderBodyPiece(), headCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(), - new PathControlPointVisualiser(HitObject), + new PathControlPointVisualiser(HitObject) { ControlPointsChanged = _ => updateSlider() }, }; setState(PlacementState.Initial); @@ -131,8 +134,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray(); - HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints); + Vector2[] newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray(); + + var unsnappedPath = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints); + var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance; + + HitObject.Path = new SliderPath(unsnappedPath.Type, newControlPoints, snappedDistance); bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index fdeffc6f8a..b09f598bcc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -1,7 +1,11 @@ // 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.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -15,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected readonly SliderCircleSelectionBlueprint HeadBlueprint; protected readonly SliderCircleSelectionBlueprint TailBlueprint; + [Resolved(CanBeNull = true)] + private HitObjectComposer composer { get; set; } + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -25,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece = new SliderBodyPiece(), HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), - new PathControlPointVisualiser(sliderObject), + new PathControlPointVisualiser(sliderObject) { ControlPointsChanged = onNewControlPoints }, }; } @@ -36,8 +43,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece.UpdateFrom(HitObject); } + private void onNewControlPoints(Vector2[] controlPoints) + { + var unsnappedPath = new SliderPath(controlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, controlPoints); + var snappedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)unsnappedPath.Distance) ?? (float)unsnappedPath.Distance; + + HitObject.Path = new SliderPath(unsnappedPath.Type, controlPoints, snappedDistance); + } + public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); + protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index bc0f76f000..79cd51a7f4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -1,8 +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 osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -15,15 +13,5 @@ namespace osu.Game.Rulesets.Osu.Edit { Masking = true; } - - protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(time); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(time); - - double scoringDistance = OsuHitObject.BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; - - return (float)(scoringDistance / timingPoint.BeatLength); - } } } diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs new file mode 100644 index 0000000000..e6b0cf992a --- /dev/null +++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs @@ -0,0 +1,195 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Editor +{ + [HeadlessTest] + public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene + { + private TestHitObjectComposer composer; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = composer = new TestHitObjectComposer(); + + BeatDivisor.Value = 1; + + composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; + composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + + composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = 1 }); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 1000 }); + }); + + [TestCase(1)] + [TestCase(2)] + public void TestSliderMultiplier(float multiplier) + { + AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier); + + assertSnapDistance(100 * multiplier); + } + + [TestCase(1)] + [TestCase(2)] + public void TestSpeedMultiplier(float multiplier) + { + AddStep($"set multiplier = {multiplier}", () => + { + composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = multiplier }); + }); + + assertSnapDistance(100 * multiplier); + } + + [TestCase(1)] + [TestCase(2)] + public void TestBeatDivisor(int divisor) + { + AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor); + + assertSnapDistance(100f / divisor); + } + + [Test] + public void TestConvertDurationToDistance() + { + assertDurationToDistance(500, 50); + assertDurationToDistance(1000, 100); + + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + + assertDurationToDistance(500, 100); + assertDurationToDistance(1000, 200); + + AddStep("set beat length = 500", () => + { + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + }); + + assertDurationToDistance(500, 200); + assertDurationToDistance(1000, 400); + } + + [Test] + public void TestConvertDistanceToDuration() + { + assertDistanceToDuration(50, 500); + assertDistanceToDuration(100, 1000); + + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + + assertDistanceToDuration(100, 500); + assertDistanceToDuration(200, 1000); + + AddStep("set beat length = 500", () => + { + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + }); + + assertDistanceToDuration(200, 500); + assertDistanceToDuration(400, 1000); + } + + [Test] + public void TestGetSnappedDurationFromDistance() + { + assertSnappedDuration(50, 0); + assertSnappedDuration(100, 1000); + assertSnappedDuration(150, 1000); + assertSnappedDuration(200, 2000); + assertSnappedDuration(250, 2000); + + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + + assertSnappedDuration(50, 0); + assertSnappedDuration(100, 0); + assertSnappedDuration(150, 0); + assertSnappedDuration(200, 1000); + assertSnappedDuration(250, 1000); + + AddStep("set beat length = 500", () => + { + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + }); + + assertSnappedDuration(50, 0); + assertSnappedDuration(100, 0); + assertSnappedDuration(150, 0); + assertSnappedDuration(200, 500); + assertSnappedDuration(250, 500); + assertSnappedDuration(400, 1000); + } + + [Test] + public void GetSnappedDistanceFromDistance() + { + assertSnappedDistance(50, 0); + assertSnappedDistance(100, 100); + assertSnappedDistance(150, 100); + assertSnappedDistance(200, 200); + assertSnappedDistance(250, 200); + + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + + assertSnappedDistance(50, 0); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); + assertSnappedDistance(200, 200); + assertSnappedDistance(250, 200); + + AddStep("set beat length = 500", () => + { + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Clear(); + composer.EditorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = 500 }); + }); + + assertSnappedDistance(50, 0); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); + assertSnappedDistance(200, 200); + assertSnappedDistance(250, 200); + assertSnappedDistance(400, 400); + } + + private void assertSnapDistance(float expectedDistance) + => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance); + + private void assertDurationToDistance(double duration, float expectedDistance) + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance); + + private void assertDistanceToDuration(float distance, double expectedDuration) + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration); + + private void assertSnappedDuration(float distance, double expectedDuration) + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration); + + private void assertSnappedDistance(float distance, float expectedDistance) + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance); + + private class TestHitObjectComposer : OsuHitObjectComposer + { + public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; + + public TestHitObjectComposer() + : base(new OsuRuleset()) + { + } + } + } +} diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs index a9e5930478..558e1106a7 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -27,27 +25,25 @@ namespace osu.Game.Tests.Visual.Editor [Cached(typeof(IEditorBeatmap))] private readonly EditorBeatmap editorBeatmap; - private TestDistanceSnapGrid grid; + [Cached(typeof(IDistanceSnapProvider))] + private readonly SnapProvider snapProvider = new SnapProvider(); public TestSceneDistanceSnapGrid() { editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length }); - createGrid(); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray + }, + new TestDistanceSnapGrid(new HitObject(), grid_position) + }; } - [SetUp] - public void Setup() => Schedule(() => - { - Clear(); - - editorBeatmap.ControlPointInfo.TimingPoints.Clear(); - editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length }); - - BeatDivisor.Value = 1; - }); - [TestCase(1)] [TestCase(2)] [TestCase(3)] @@ -56,84 +52,13 @@ namespace osu.Game.Tests.Visual.Editor [TestCase(8)] [TestCase(12)] [TestCase(16)] - public void TestInitialBeatDivisor(int divisor) + public void TestBeatDivisor(int divisor) { AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor); - createGrid(); - - float expectedDistance = (float)beat_length / divisor; - AddAssert($"spacing is {expectedDistance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expectedDistance)); - } - - [Test] - public void TestChangeBeatDivisor() - { - createGrid(); - AddStep("set beat divisor = 2", () => BeatDivisor.Value = 2); - - const float expected_distance = (float)beat_length / 2; - AddAssert($"spacing is {expected_distance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expected_distance)); - } - - [TestCase(100)] - [TestCase(200)] - public void TestBeatLength(double beatLength) - { - AddStep($"set beat length = {beatLength}", () => - { - editorBeatmap.ControlPointInfo.TimingPoints.Clear(); - editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beatLength }); - }); - - createGrid(); - AddAssert($"spacing is {beatLength}", () => Precision.AlmostEquals(grid.DistanceSpacing, beatLength)); - } - - [TestCase(1)] - [TestCase(2)] - public void TestGridVelocity(float velocity) - { - createGrid(g => g.Velocity = velocity); - - float expectedDistance = (float)beat_length * velocity; - AddAssert($"spacing is {expectedDistance}", () => Precision.AlmostEquals(grid.DistanceSpacing, expectedDistance)); - } - - [Test] - public void TestGetSnappedTime() - { - createGrid(); - - Vector2 snapPosition = Vector2.Zero; - AddStep("get first tick position", () => snapPosition = grid_position + new Vector2((float)beat_length, 0)); - AddAssert("snap time is 1 beat away", () => Precision.AlmostEquals(beat_length, grid.GetSnapTime(snapPosition), 0.01)); - - createGrid(g => g.Velocity = 2, "with velocity = 2"); - AddAssert("snap time is now 0.5 beats away", () => Precision.AlmostEquals(beat_length / 2, grid.GetSnapTime(snapPosition), 0.01)); - } - - private void createGrid(Action func = null, string description = null) - { - AddStep($"create grid {description ?? string.Empty}", () => - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.SlateGray - }, - grid = new TestDistanceSnapGrid(new HitObject(), grid_position) - }; - - func?.Invoke(grid); - }); } private class TestDistanceSnapGrid : DistanceSnapGrid { - public new float Velocity = 1; - public new float DistanceSpacing => base.DistanceSpacing; public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) @@ -203,11 +128,23 @@ namespace osu.Game.Tests.Visual.Editor } } - protected override float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - => Velocity; + public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition) + => (Vector2.Zero, 0); + } - public override Vector2 GetSnapPosition(Vector2 screenSpacePosition) - => Vector2.Zero; + private class SnapProvider : IDistanceSnapProvider + { + public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); + + public float GetBeatSnapDistanceAt(double referenceTime) => 10; + + public float DurationToDistance(double referenceTime, double duration) => 0; + + public double DistanceToDuration(double referenceTime, float distance) => 0; + + public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + + public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0; } } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs index 0ea73fb3de..b7c7028b52 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Editor { [TestFixture] - public class TestSceneHitObjectComposer : OsuTestScene + public class TestSceneHitObjectComposer : EditorClockTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 436e80d6f5..86bd0ddd11 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -22,7 +22,8 @@ namespace osu.Game.Tests.Visual.Online typeof(HeaderButton), typeof(SortTabControl), typeof(ShowChildrenButton), - typeof(DeletedChildrenPlaceholder) + typeof(DeletedChildrenPlaceholder), + typeof(VotePill) }; protected override bool UseOnlineAPI => true; diff --git a/osu.Game/Graphics/UserInterface/LoadingButton.cs b/osu.Game/Graphics/UserInterface/LoadingButton.cs new file mode 100644 index 0000000000..49ec18ce8e --- /dev/null +++ b/osu.Game/Graphics/UserInterface/LoadingButton.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.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public abstract class LoadingButton : OsuHoverContainer + { + private bool isLoading; + + public bool IsLoading + { + get => isLoading; + set + { + isLoading = value; + + Enabled.Value = !isLoading; + + if (value) + { + loading.Show(); + OnLoadStarted(); + } + else + { + loading.Hide(); + OnLoadFinished(); + } + } + } + + public Vector2 LoadingAnimationSize + { + get => loading.Size; + set => loading.Size = value; + } + + private readonly LoadingAnimation loading; + + protected LoadingButton() + { + AddRange(new[] + { + CreateContent(), + loading = new LoadingAnimation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12) + } + }); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + try + { + return base.OnClick(e); + } + finally + { + // run afterwards as this will disable this button. + IsLoading = true; + } + } + + protected virtual void OnLoadStarted() + { + } + + protected virtual void OnLoadFinished() + { + } + + protected abstract Drawable CreateContent(); + } +} diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index 5296b9dd7f..4931a6aed6 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs @@ -5,8 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; @@ -14,9 +12,9 @@ using System.Collections.Generic; namespace osu.Game.Graphics.UserInterface { - public class ShowMoreButton : OsuHoverContainer + public class ShowMoreButton : LoadingButton { - private const float fade_duration = 200; + private const int duration = 200; private Color4 chevronIconColour; @@ -32,100 +30,55 @@ namespace osu.Game.Graphics.UserInterface set => text.Text = value; } - private bool isLoading; - - public bool IsLoading - { - get => isLoading; - set - { - isLoading = value; - - Enabled.Value = !isLoading; - - if (value) - { - loading.Show(); - content.FadeOut(fade_duration, Easing.OutQuint); - } - else - { - loading.Hide(); - content.FadeIn(fade_duration, Easing.OutQuint); - } - } - } - - private readonly Box background; - private readonly LoadingAnimation loading; - private readonly FillFlowContainer content; - private readonly ChevronIcon leftChevron; - private readonly ChevronIcon rightChevron; - private readonly SpriteText text; - protected override IEnumerable EffectTargets => new[] { background }; + private ChevronIcon leftChevron; + private ChevronIcon rightChevron; + private SpriteText text; + private Box background; + private FillFlowContainer textContainer; + public ShowMoreButton() { AutoSizeAxes = Axes.Both; + } + + protected override Drawable CreateContent() => new CircularContainer + { + Masking = true, + Size = new Vector2(140, 30), Children = new Drawable[] { - new CircularContainer + background = new Box { - Masking = true, - Size = new Vector2(140, 30), + RelativeSizeAxes = Axes.Both, + }, + textContainer = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), Children = new Drawable[] { - background = new Box - { - RelativeSizeAxes = Axes.Both, - }, - content = new FillFlowContainer + leftChevron = new ChevronIcon(), + text = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), - Children = new Drawable[] - { - leftChevron = new ChevronIcon(), - text = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = "show more".ToUpper(), - }, - rightChevron = new ChevronIcon(), - } - }, - loading = new LoadingAnimation - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(12) + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = "show more".ToUpper(), }, + rightChevron = new ChevronIcon(), } } - }; - } - - protected override bool OnClick(ClickEvent e) - { - if (!Enabled.Value) - return false; - - try - { - return base.OnClick(e); } - finally - { - // run afterwards as this will disable this button. - IsLoading = true; - } - } + }; + + protected override void OnLoadStarted() => textContainer.FadeOut(duration, Easing.OutQuint); + + protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint); private class ChevronIcon : SpriteIcon { diff --git a/osu.Game/Online/API/Requests/CommentVoteRequest.cs b/osu.Game/Online/API/Requests/CommentVoteRequest.cs new file mode 100644 index 0000000000..06a3b1126e --- /dev/null +++ b/osu.Game/Online/API/Requests/CommentVoteRequest.cs @@ -0,0 +1,36 @@ +// 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.IO.Network; +using osu.Game.Online.API.Requests.Responses; +using System.Net.Http; + +namespace osu.Game.Online.API.Requests +{ + public class CommentVoteRequest : APIRequest + { + private readonly long id; + private readonly CommentVoteAction action; + + public CommentVoteRequest(long id, CommentVoteAction action) + { + this.id = id; + this.action = action; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = action == CommentVoteAction.Vote ? HttpMethod.Post : HttpMethod.Delete; + return req; + } + + protected override string Target => $@"comments/{id}/vote"; + } + + public enum CommentVoteAction + { + Vote, + UnVote + } +} diff --git a/osu.Game/Online/API/Requests/Responses/Comment.cs b/osu.Game/Online/API/Requests/Responses/Comment.cs index 29abaa74e5..5510e9afff 100644 --- a/osu.Game/Online/API/Requests/Responses/Comment.cs +++ b/osu.Game/Online/API/Requests/Responses/Comment.cs @@ -72,6 +72,8 @@ namespace osu.Game.Online.API.Requests.Responses public bool HasMessage => !string.IsNullOrEmpty(MessageHtml); + public bool IsVoted { get; set; } + public string GetMessage => HasMessage ? WebUtility.HtmlDecode(Regex.Replace(MessageHtml, @"<(.|\n)*?>", string.Empty)) : string.Empty; public int DeletedChildrenCount => ChildComments.Count(c => c.IsDeleted); diff --git a/osu.Game/Online/API/Requests/Responses/CommentBundle.cs b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs index 7063581605..7db3126ade 100644 --- a/osu.Game/Online/API/Requests/Responses/CommentBundle.cs +++ b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs @@ -47,6 +47,22 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"included_comments")] public List IncludedComments { get; set; } + [JsonProperty(@"user_votes")] + private List userVotes + { + set + { + value.ForEach(v => + { + Comments.ForEach(c => + { + if (v == c.Id) + c.IsVoted = true; + }); + }); + } + } + private List users; [JsonProperty(@"users")] diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 89abda92cf..3fb9867f0e 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Comments { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(margin), + Padding = new MarginPadding(margin) { Left = margin + 5 }, Child = content = new GridContainer { RelativeSizeAxes = Axes.X, @@ -81,11 +81,17 @@ namespace osu.Game.Overlays.Comments Spacing = new Vector2(5, 0), Children = new Drawable[] { - votePill = new VotePill(comment.VotesCount) + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AlwaysPresent = true, + Width = 40, + AutoSizeAxes = Axes.Y, + Child = votePill = new VotePill(comment) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } }, new UpdateableAvatar(comment.User) { @@ -333,31 +339,5 @@ namespace osu.Game.Overlays.Comments return parentComment.HasMessage ? parentComment.GetMessage : parentComment.IsDeleted ? @"deleted" : string.Empty; } } - - private class VotePill : CircularContainer - { - public VotePill(int count) - { - AutoSizeAxes = Axes.X; - Height = 20; - Masking = true; - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.05f) - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Horizontal = margin }, - Font = OsuFont.GetFont(size: 14), - Text = $"+{count}" - } - }; - } - } } } diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs new file mode 100644 index 0000000000..e8d9013fd9 --- /dev/null +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -0,0 +1,183 @@ +// 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.Framework.Graphics; +using osu.Game.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Allocation; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using System.Collections.Generic; +using osuTK; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Framework.Bindables; +using System.Linq; + +namespace osu.Game.Overlays.Comments +{ + public class VotePill : LoadingButton, IHasAccentColour + { + private const int duration = 200; + + public Color4 AccentColour { get; set; } + + protected override IEnumerable EffectTargets => null; + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly Comment comment; + private Box background; + private Box hoverLayer; + private CircularContainer borderContainer; + private SpriteText sideNumber; + private OsuSpriteText votesCounter; + private CommentVoteRequest request; + + private readonly BindableBool isVoted = new BindableBool(); + private readonly BindableInt votesCount = new BindableInt(); + + public VotePill(Comment comment) + { + this.comment = comment; + + Action = onAction; + + AutoSizeAxes = Axes.X; + Height = 20; + LoadingAnimationSize = new Vector2(10); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight; + hoverLayer.Colour = Color4.Black.Opacity(0.5f); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + isVoted.Value = comment.IsVoted; + votesCount.Value = comment.VotesCount; + isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : OsuColour.Gray(0.05f), true); + votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true); + } + + private void onAction() + { + request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote); + request.Success += onSuccess; + api.Queue(request); + } + + private void onSuccess(CommentBundle response) + { + var receivedComment = response.Comments.Single(); + isVoted.Value = receivedComment.IsVoted; + votesCount.Value = receivedComment.VotesCount; + IsLoading = false; + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + borderContainer = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0 + } + } + }, + sideNumber = new SpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Text = "+1", + Font = OsuFont.GetFont(size: 14), + Margin = new MarginPadding { Right = 3 }, + Alpha = 0, + }, + votesCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Horizontal = 10 }, + Font = OsuFont.GetFont(size: 14), + AlwaysPresent = true, + } + }, + }; + + protected override void OnLoadStarted() + { + votesCounter.FadeOut(duration, Easing.OutQuint); + updateDisplay(); + } + + protected override void OnLoadFinished() + { + votesCounter.FadeIn(duration, Easing.OutQuint); + + if (IsHovered) + onHoverAction(); + } + + protected override bool OnHover(HoverEvent e) + { + onHoverAction(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + private void updateDisplay() + { + if (isVoted.Value) + { + hoverLayer.FadeTo(IsHovered ? 1 : 0); + sideNumber.Hide(); + } + else + sideNumber.FadeTo(IsHovered ? 1 : 0); + + borderContainer.BorderThickness = IsHovered ? 3 : 0; + } + + private void onHoverAction() + { + if (!IsLoading) + updateDisplay(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 6396301add..5922bfba78 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -14,6 +14,7 @@ using osu.Framework.Logging; using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -39,6 +40,9 @@ namespace osu.Game.Rulesets.Edit [Resolved] protected IFrameBasedClock EditorClock { get; private set; } + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } + private IWorkingBeatmap workingBeatmap; private Beatmap playableBeatmap; private IBeatmapProcessor beatmapProcessor; @@ -246,7 +250,7 @@ namespace osu.Game.Rulesets.Edit public void BeginPlacement(HitObject hitObject) { if (distanceSnapGrid != null) - hitObject.StartTime = GetSnappedTime(hitObject.StartTime, distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position)); + hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time; } public void EndPlacement(HitObject hitObject) @@ -257,9 +261,45 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); - public override Vector2 GetSnappedPosition(Vector2 position) => distanceSnapGrid?.GetSnapPosition(position) ?? position; + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time); - public override double GetSnappedTime(double startTime, Vector2 position) => distanceSnapGrid?.GetSnapTime(position) ?? startTime; + public override float GetBeatSnapDistanceAt(double referenceTime) + { + DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime); + return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / beatDivisor.Value); + } + + public override float DurationToDistance(double referenceTime, double duration) + { + double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value; + return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime)); + } + + public override double DistanceToDuration(double referenceTime, float distance) + { + double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value; + return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength; + } + + public override double GetSnappedDurationFromDistance(double referenceTime, float distance) + => beatSnap(referenceTime, DistanceToDuration(referenceTime, distance)); + + public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + => DurationToDistance(referenceTime, beatSnap(referenceTime, DistanceToDuration(referenceTime, distance))); + + /// + /// Snaps a duration to the closest beat of a timing point applicable at the reference time. + /// + /// The time of the timing point which resides in. + /// The duration to snap. + /// A value that represents snapped to the closest beat of the timing point. + private double beatSnap(double referenceTime, double duration) + { + double beatLength = EditorBeatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength / beatDivisor.Value; + + // A 1ms offset prevents rounding errors due to minute variations in duration + return (int)((duration + 1) / beatLength) * beatLength; + } protected override void Dispose(bool isDisposing) { @@ -274,7 +314,8 @@ namespace osu.Game.Rulesets.Edit } [Cached(typeof(HitObjectComposer))] - public abstract class HitObjectComposer : CompositeDrawable + [Cached(typeof(IDistanceSnapProvider))] + public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider { internal HitObjectComposer() { @@ -310,8 +351,11 @@ namespace osu.Game.Rulesets.Edit [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; - public abstract Vector2 GetSnappedPosition(Vector2 position); - - public abstract double GetSnappedTime(double startTime, Vector2 screenSpacePosition); + public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); + public abstract float GetBeatSnapDistanceAt(double referenceTime); + public abstract float DurationToDistance(double referenceTime, double duration); + public abstract double DistanceToDuration(double referenceTime, float distance); + public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); + public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); } } diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs new file mode 100644 index 0000000000..c6e61f68da --- /dev/null +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + public interface IDistanceSnapProvider + { + (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); + + /// + /// Retrieves the distance between two points within a timing point that are one beat length apart. + /// + /// The time of the timing point. + /// The distance between two points residing in the timing point that are one beat length apart. + float GetBeatSnapDistanceAt(double referenceTime); + + /// + /// Converts a duration to a distance. + /// + /// The time of the timing point which resides in. + /// The duration to convert. + /// A value that represents as a distance in the timing point. + float DurationToDistance(double referenceTime, double duration); + + /// + /// Converts a distance to a duration. + /// + /// The time of the timing point which resides in. + /// The distance to convert. + /// A value that represents as a duration in the timing point. + double DistanceToDuration(double referenceTime, float distance); + + /// + /// Converts a distance to a snapped duration. + /// + /// The time of the timing point which resides in. + /// The distance to convert. + /// A value that represents as a duration snapped to the closest beat of the timing point. + double GetSnappedDurationFromDistance(double referenceTime, float distance); + + /// + /// Converts an unsnapped distance to a snapped distance. + /// + /// The time of the timing point which resides in. + /// The distance to convert. + /// A value that represents snapped to the closest beat of the timing point. + float GetSnappedDistanceFromDistance(double referenceTime, float distance); + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 30f0f94128..a145dea6af 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition(Vector2 screenSpacePosition) { - Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition)); + Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition), 0).position; Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition); currentPlacement.UpdatePosition(snappedScreenSpacePosition); @@ -358,13 +358,13 @@ namespace osu.Game.Screens.Edit.Compose.Components // The final movement position, relative to screenSpaceMovementStartPosition Vector2 movePosition = startPosition + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - Vector2 snappedPosition = composer.GetSnappedPosition(ToLocalSpace(movePosition)); + (Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); // Move the hitobjects selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition))); // Apply the start time at the newly snapped-to position - double offset = composer.GetSnappedTime(draggedObject.StartTime, snappedPosition) - draggedObject.StartTime; + double offset = snappedTime - draggedObject.StartTime; foreach (HitObject obj in selectionHandler.SelectedHitObjects) obj.StartTime += offset; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index a644e51c13..f45115e1e4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public override Vector2 GetSnapPosition(Vector2 position) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) { Vector2 direction = position - CentrePosition; @@ -76,7 +76,9 @@ namespace osu.Game.Screens.Edit.Compose.Components int radialCount = Math.Max(1, (int)Math.Round(distance / radius)); Vector2 normalisedDirection = direction * new Vector2(1f / distance); - return CentrePosition + normalisedDirection * radialCount * radius; + Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius; + + return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - CentrePosition).Length)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 096ff0a6dd..193474093f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -6,9 +6,8 @@ using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -20,16 +19,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public abstract class DistanceSnapGrid : CompositeDrawable { - /// - /// The velocity of the beatmap at the point of placement in pixels per millisecond. - /// - protected double Velocity { get; private set; } - /// /// The spacing between each tick of the beat snapping grid. /// protected float DistanceSpacing { get; private set; } + /// + /// The snapping time at . + /// + protected double StartTime { get; private set; } + /// /// The position which the grid is centred on. /// The first beat snapping tick is located at + in the desired direction. @@ -39,6 +38,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] protected OsuColour Colours { get; private set; } + [Resolved] + protected IDistanceSnapProvider SnapProvider { get; private set; } + [Resolved] private IEditorBeatmap beatmap { get; set; } @@ -48,22 +50,18 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Cached gridCache = new Cached(); private readonly HitObject hitObject; - private double startTime; - private double beatLength; - protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) { this.hitObject = hitObject; - this.CentrePosition = centrePosition; + CentrePosition = centrePosition; RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load() { - startTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime; - beatLength = beatmap.ControlPointInfo.TimingPointAt(startTime).BeatLength; + StartTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime; } protected override void LoadComplete() @@ -75,8 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateSpacing() { - Velocity = GetVelocity(startTime, beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); - DistanceSpacing = (float)(beatLength / beatDivisor.Value * Velocity); + DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); gridCache.Invalidate(); } @@ -105,28 +102,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected abstract void CreateContent(Vector2 centrePosition); - /// - /// Retrieves the velocity of gameplay at a point in time in pixels per millisecond. - /// - /// The time to retrieve the velocity at. - /// The beatmap's at the point in time. - /// The beatmap's at the point in time. - /// The velocity. - protected abstract float GetVelocity(double time, ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty); - /// /// Snaps a position to this grid. /// /// The original position in coordinate space local to this . - /// The snapped position in coordinate space local to this . - public abstract Vector2 GetSnapPosition(Vector2 position); - - /// - /// Retrieves the time at a snapped position. - /// - /// The snapped position in coordinate space local to this . - /// The time at the snapped position. - public double GetSnapTime(Vector2 position) => startTime + (position - CentrePosition).Length / Velocity; + /// A tuple containing the snapped position in coordinate space local to this and the respective time value. + public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position); /// /// Retrieves the applicable colour for a beat index.