diff --git a/.github/ISSUE_TEMPLATE/00-mobile-issues.md b/.github/ISSUE_TEMPLATE/00-mobile-issues.md
new file mode 100644
index 0000000000..f171e80b8b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/00-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/.github/ISSUE_TEMPLATE/bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/bug-issues.md
rename to .github/ISSUE_TEMPLATE/01-bug-issues.md
diff --git a/.github/ISSUE_TEMPLATE/crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/crash-issues.md
rename to .github/ISSUE_TEMPLATE/02-crash-issues.md
diff --git a/.github/ISSUE_TEMPLATE/feature-request-issues.md b/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/feature-request-issues.md
rename to .github/ISSUE_TEMPLATE/03-feature-request-issues.md
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..69baeee60c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: osu!stable issues
+ url: https://github.com/ppy/osu-stable-issues
+ about: For issues regarding osu!stable (not osu!lazer), open them here.
diff --git a/.github/ISSUE_TEMPLATE/missing-for-live-issues.md b/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
deleted file mode 100644
index 5822da9c65..0000000000
--- a/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Missing for Live
-about: Features which are available in osu!stable but not yet in osu!lazer.
----
-**Describe the missing feature:**
-
-**Proposal designs of the feature:**
diff --git a/osu.Android.props b/osu.Android.props
index 43c1302e54..d1860acbf9 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -62,6 +62,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 7a9b61c60c..0369b6db4e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void load()
{
var controlPointInfo = new ControlPointInfo();
- controlPointInfo.TimingPoints.Add(new TimingControlPoint());
+ controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 732231b0d9..9cdf045b5b 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit
editorClock = clock;
}
- public override void HandleMovement(MoveSelectionEvent moveEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Edit
performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent);
- base.HandleMovement(moveEvent);
+ return true;
}
///
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
new file mode 100644
index 0000000000..94ca2d4cd1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -0,0 +1,230 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneFollowPoints : OsuTestScene
+ {
+ private Container hitObjectContainer;
+ private FollowPointRenderer followPointRenderer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
+ followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
+ };
+ });
+
+ [Test]
+ public void TestAddObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveObject()
+ {
+ addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestAddMultipleObjects()
+ {
+ addMultipleObjectsStep();
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveEndObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(4));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveStartObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(0));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestRemoveMiddleObject()
+ {
+ addMultipleObjectsStep();
+
+ removeObjectStep(() => getObject(2));
+
+ assertGroups();
+ }
+
+ [Test]
+ public void TestMoveObject()
+ {
+ addMultipleObjectsStep();
+
+ AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
+
+ assertGroups();
+ }
+
+ [TestCase(0, 0)] // Start -> Start
+ [TestCase(0, 2)] // Start -> Middle
+ [TestCase(0, 5)] // Start -> End
+ [TestCase(2, 0)] // Middle -> Start
+ [TestCase(1, 3)] // Middle -> Middle (forwards)
+ [TestCase(3, 1)] // Middle -> Middle (backwards)
+ [TestCase(4, 0)] // End -> Start
+ [TestCase(4, 2)] // End -> Middle
+ [TestCase(4, 4)] // End -> End
+ public void TestReorderObjects(int startIndex, int endIndex)
+ {
+ addMultipleObjectsStep();
+
+ reorderObjectStep(startIndex, endIndex);
+
+ assertGroups();
+ }
+
+ private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
+ {
+ new HitCircle { Position = new Vector2(100, 100) },
+ new HitCircle { Position = new Vector2(200, 200) },
+ new HitCircle { Position = new Vector2(300, 300) },
+ new HitCircle { Position = new Vector2(400, 400) },
+ new HitCircle { Position = new Vector2(500, 500) },
+ });
+
+ private void addObjectsStep(Func ctorFunc)
+ {
+ AddStep("add hitobjects", () =>
+ {
+ var objects = ctorFunc();
+
+ for (int i = 0; i < objects.Length; i++)
+ {
+ objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
+ objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ DrawableOsuHitObject drawableObject = null;
+
+ switch (objects[i])
+ {
+ case HitCircle circle:
+ drawableObject = new DrawableHitCircle(circle);
+ break;
+
+ case Slider slider:
+ drawableObject = new DrawableSlider(slider);
+ break;
+
+ case Spinner spinner:
+ drawableObject = new DrawableSpinner(spinner);
+ break;
+ }
+
+ hitObjectContainer.Add(drawableObject);
+ followPointRenderer.AddFollowPoints(drawableObject);
+ }
+ });
+ }
+
+ private void removeObjectStep(Func getFunc)
+ {
+ AddStep("remove hitobject", () =>
+ {
+ var drawableObject = getFunc?.Invoke();
+
+ hitObjectContainer.Remove(drawableObject);
+ followPointRenderer.RemoveFollowPoints(drawableObject);
+ });
+ }
+
+ private void reorderObjectStep(int startIndex, int endIndex)
+ {
+ AddStep($"move object {startIndex} to {endIndex}", () =>
+ {
+ DrawableOsuHitObject toReorder = getObject(startIndex);
+
+ double targetTime;
+ if (endIndex < hitObjectContainer.Count)
+ targetTime = getObject(endIndex).HitObject.StartTime - 1;
+ else
+ targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
+
+ hitObjectContainer.Remove(toReorder);
+ toReorder.HitObject.StartTime = targetTime;
+ hitObjectContainer.Add(toReorder);
+ });
+ }
+
+ private void assertGroups()
+ {
+ AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
+ AddAssert("group endpoints are correct", () =>
+ {
+ for (int i = 0; i < hitObjectContainer.Count; i++)
+ {
+ DrawableOsuHitObject expectedStart = getObject(i);
+ DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
+
+ if (getGroup(i).Start != expectedStart)
+ throw new AssertionException($"Object {i} expected to be the start of group {i}.");
+
+ if (getGroup(i).End != expectedEnd)
+ throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
+ }
+
+ return true;
+ });
+ }
+
+ private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
+
+ private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
+
+ private class TestHitObjectContainer : Container
+ {
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ var osuX = (DrawableOsuHitObject)x;
+ var osuY = (DrawableOsuHitObject)y;
+
+ int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
+
+ if (compare == 0)
+ return base.Compare(x, y);
+
+ return compare;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
index 6b8daa531f..eff4d919b0 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,33 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
+ [Cached(typeof(IDistanceSnapProvider))]
+ private readonly SnapProvider snapProvider = new SnapProvider();
+
private TestOsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
-
- createGrid();
}
[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 });
+ editorBeatmap.ControlPointInfo.Clear();
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
- beatDivisor.Value = 1;
+ 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 }
+ };
});
[TestCase(1)]
@@ -71,53 +79,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 +91,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,23 +98,14 @@ 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}", () =>
+ [Test]
+ public void TestLimitedDistance()
{
- Vector2 snappedPosition = grid.GetSnapPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position));
- float distance = Vector2.Distance(snappedPosition, grid_position);
-
- return Precision.AlmostEquals(expectedDistance, distance);
- });
-
- private void createGrid()
- {
- AddStep("create grid", () =>
+ AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
@@ -159,12 +114,22 @@ namespace osu.Game.Rulesets.Osu.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
- new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnapPosition(grid.ToLocalSpace(v)) }
+ grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
+ new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
});
+
+ AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
+ assertSnappedDistance((float)beat_length * 2);
}
+ private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
+ {
+ Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
+
+ return Precision.AlmostEquals(expectedDistance, Vector2.Distance(snappedPosition, grid_position));
+ });
+
private class SnappingCursorContainer : CompositeDrawable
{
public Func GetSnapPosition;
@@ -208,10 +173,25 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public new float DistanceSpacing => base.DistanceSpacing;
- public TestOsuDistanceSnapGrid(OsuHitObject hitObject)
- : base(hitObject)
+ public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
+ : base(hitObject, nextHitObject)
{
}
}
+
+ 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) => (float)duration;
+
+ public double DistanceToDuration(double referenceTime, float distance) => distance;
+
+ public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+
+ public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 4893ebfdd4..a955911bd5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
var cpi = new ControlPointInfo();
- cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
+ cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 2eb783233a..5f75cbabec 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -313,10 +313,6 @@ namespace osu.Game.Rulesets.Osu.Tests
}, 25),
}
},
- ControlPointInfo =
- {
- DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } }
- },
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
@@ -324,6 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
index 5df0b70f12..dde2aa53e0 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -85,6 +86,93 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositions();
}
+ [Test]
+ public void TestSingleControlPointSelection()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, true);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestSingleControlPointDeselectionViaOtherControlPoint()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ moveMouseToControlPoint(1);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, true);
+ }
+
+ [Test]
+ public void TestSingleControlPointDeselectionViaClickOutside()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestMultipleControlPointSelection()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+ checkControlPointSelected(0, true);
+ checkControlPointSelected(1, true);
+ }
+
+ [Test]
+ public void TestMultipleControlPointDeselectionViaOtherControlPoint()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ moveMouseToControlPoint(2);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
+ [Test]
+ public void TestMultipleControlPointDeselectionViaClickOutside()
+ {
+ moveMouseToControlPoint(0);
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ moveMouseToControlPoint(1);
+ AddStep("ctrl + click", () =>
+ {
+ InputManager.PressKey(Key.ControlLeft);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.ControlLeft);
+ });
+
+ AddStep("move mouse outside control point", () => InputManager.MoveMouseTo(drawableObject));
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+ checkControlPointSelected(0, false);
+ checkControlPointSelected(1, false);
+ }
+
private void moveHitObject()
{
AddStep("move hitobject", () =>
@@ -104,11 +192,24 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(blueprint.TailBlueprint.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
}
+ private void moveMouseToControlPoint(int index)
+ {
+ AddStep($"move mouse to control point {index}", () =>
+ {
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index];
+ InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
+ });
+ }
+
+ private void checkControlPointSelected(int index, bool selected)
+ => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
+
private class TestSliderBlueprint : SliderSelectionBlueprint
{
public new SliderBodyPiece BodyPiece => base.BodyPiece;
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
+ public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
public TestSliderBlueprint(DrawableSlider slider)
: base(slider)
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..0353ba241c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -1,26 +1,33 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointPiece : BlueprintPiece
{
- private readonly Slider slider;
- private readonly int index;
+ public Action RequestSelection;
+ public Action ControlPointsChanged;
+ public readonly BindableBool IsSelected = new BindableBool();
+ public readonly int Index;
+
+ private readonly Slider slider;
private readonly Path path;
- private readonly CircularContainer marker;
+ private readonly Container marker;
+ private readonly Drawable markerRing;
[Resolved]
private OsuColour colours { get; set; }
@@ -28,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointPiece(Slider slider, int index)
{
this.slider = slider;
- this.index = index;
+ Index = index;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -40,13 +47,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Anchor = Anchor.Centre,
PathRadius = 1
},
- marker = new CircularContainer
+ marker = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(10),
- Masking = true,
- Child = new Box { RelativeSizeAxes = Axes.Both }
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(10),
+ },
+ markerRing = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(14),
+ Masking = true,
+ BorderThickness = 2,
+ BorderColour = Color4.White,
+ Alpha = 0,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
}
};
}
@@ -55,30 +85,66 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
- Position = slider.StackedPosition + slider.Path.ControlPoints[index];
+ Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
- marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ updateMarkerDisplay();
+ updateConnectingPath();
+ }
+ ///
+ /// Updates the state of the circular control point marker.
+ ///
+ private void updateMarkerDisplay()
+ {
+ markerRing.Alpha = IsSelected.Value ? 1 : 0;
+
+ Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+ if (IsHovered || IsSelected.Value)
+ colour = Color4.White;
+ marker.Colour = colour;
+ }
+
+ ///
+ /// Updates the path connecting this control point to the previous one.
+ ///
+ private void updateConnectingPath()
+ {
path.ClearVertices();
- if (index != slider.Path.ControlPoints.Length - 1)
+ if (Index != slider.Path.ControlPoints.Length - 1)
{
path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
+ path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
+ // The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (RequestSelection != null)
+ {
+ RequestSelection.Invoke(Index);
+ return true;
+ }
+
+ return false;
+ }
+
+ protected override bool OnMouseUp(MouseUpEvent e) => RequestSelection != null;
+
+ protected override bool OnClick(ClickEvent e) => RequestSelection != null;
+
protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.Path.ControlPoints.ToArray();
- if (index == 0)
+ if (Index == 0)
{
// Special handling for the head - only the position of the slider changes
slider.Position += e.Delta;
@@ -88,15 +154,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
newControlPoints[i] -= e.Delta;
}
else
- newControlPoints[index] += e.Delta;
+ newControlPoints[Index] += e.Delta;
if (isSegmentSeparatorWithNext)
- newControlPoints[index + 1] = newControlPoints[index];
+ newControlPoints[Index + 1] = newControlPoints[Index];
if (isSegmentSeparatorWithPrevious)
- newControlPoints[index - 1] = newControlPoints[index];
+ newControlPoints[Index - 1] = newControlPoints[Index];
- slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
+ ControlPointsChanged?.Invoke(newControlPoints);
return true;
}
@@ -105,8 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
- private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
+ private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
- private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
+ private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 24fcc460d1..6962736157 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -1,33 +1,133 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
- public class PathControlPointVisualiser : CompositeDrawable
+ public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler
{
+ public Action ControlPointsChanged;
+
+ internal readonly Container Pieces;
private readonly Slider slider;
+ private readonly bool allowSelection;
- private readonly Container pieces;
+ private InputManager inputManager;
- public PathControlPointVisualiser(Slider slider)
+ [Resolved(CanBeNull = true)]
+ private IPlacementHandler placementHandler { get; set; }
+
+ public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
+ this.allowSelection = allowSelection;
- InternalChild = pieces = new Container { RelativeSizeAxes = Axes.Both };
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = Pieces = new Container { RelativeSizeAxes = Axes.Both };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
}
protected override void Update()
{
base.Update();
- while (slider.Path.ControlPoints.Length > pieces.Count)
- pieces.Add(new PathControlPointPiece(slider, pieces.Count));
- while (slider.Path.ControlPoints.Length < pieces.Count)
- pieces.Remove(pieces[pieces.Count - 1]);
+ while (slider.Path.ControlPoints.Length > Pieces.Count)
+ {
+ var piece = new PathControlPointPiece(slider, Pieces.Count)
+ {
+ ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
+ };
+
+ if (allowSelection)
+ piece.RequestSelection = selectPiece;
+
+ Pieces.Add(piece);
+ }
+
+ while (slider.Path.ControlPoints.Length < Pieces.Count)
+ Pieces.Remove(Pieces[Pieces.Count - 1]);
}
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ foreach (var piece in Pieces)
+ piece.IsSelected.Value = false;
+ return false;
+ }
+
+ private void selectPiece(int index)
+ {
+ if (inputManager.CurrentState.Keyboard.ControlPressed)
+ Pieces[index].IsSelected.Toggle();
+ else
+ {
+ foreach (var piece in Pieces)
+ piece.IsSelected.Value = piece.Index == index;
+ }
+ }
+
+ public bool OnPressed(PlatformAction action)
+ {
+ switch (action.ActionMethod)
+ {
+ case PlatformActionMethod.Delete:
+ var newControlPoints = new List();
+
+ foreach (var piece in Pieces)
+ {
+ if (!piece.IsSelected.Value)
+ newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
+ }
+
+ // Ensure that there are any points to be deleted
+ if (newControlPoints.Count == slider.Path.ControlPoints.Length)
+ return false;
+
+ // If there are 0 remaining control points, treat the slider as being deleted
+ if (newControlPoints.Count == 0)
+ {
+ placementHandler?.Delete(slider);
+ return true;
+ }
+
+ // Make control points relative
+ Vector2 first = newControlPoints[0];
+ for (int i = 0; i < newControlPoints.Count; i++)
+ newControlPoints[i] = newControlPoints[i] - first;
+
+ // The slider's position defines the position of the first control point, and all further control points are relative to that point
+ slider.Position = slider.Position + first;
+
+ // Since pieces are re-used, they will not point to the deleted control points while remaining selected
+ foreach (var piece in Pieces)
+ piece.IsSelected.Value = false;
+
+ ControlPointsChanged?.Invoke(newControlPoints.ToArray());
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index d28cf7b492..78f4c4d992 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -43,5 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Size = body.Size;
OriginPosition = body.PathOffset;
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index b7b8d0af88..9c0afada29 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, false) { 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..25362820a3 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;
@@ -14,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected readonly SliderBodyPiece BodyPiece;
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
+ protected readonly PathControlPointVisualiser ControlPointVisualiser;
+
+ [Resolved(CanBeNull = true)]
+ private HitObjectComposer composer { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
@@ -25,7 +33,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),
+ ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) { ControlPointsChanged = onNewControlPoints },
};
}
@@ -36,8 +44,20 @@ 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);
+
+ UpdateHitObject();
+ }
+
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..9b00204d51 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;
@@ -10,20 +8,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
- public OsuDistanceSnapGrid(OsuHitObject hitObject)
- : base(hitObject, hitObject.StackedEndPosition)
+ public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
+ : base(hitObject, nextHitObject, hitObject.StackedEndPosition)
{
Masking = true;
}
-
- 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.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index fcf2772219..812afaaa24 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList();
if (objects.Count == 0)
+ return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
+
+ double minTime = objects.Min(h => h.StartTime);
+ return createGrid(h => h.StartTime < minTime, objects.Count + 1);
+ }
+
+ ///
+ /// Creates a grid from the last matching a predicate to a target .
+ ///
+ /// A predicate that matches s where the grid can start from.
+ /// Only the last matching the predicate is used.
+ /// An offset from the selected via at which the grid should stop.
+ /// The from a selected to a target .
+ private OsuDistanceSnapGrid createGrid(Func sourceSelector, int targetOffset = 1)
+ {
+ if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
+
+ int sourceIndex = -1;
+
+ for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{
- var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
+ if (!sourceSelector(EditorBeatmap.HitObjects[i]))
+ break;
- if (lastObject == null)
- return null;
-
- return new OsuDistanceSnapGrid(lastObject);
+ sourceIndex = i;
}
- else
- {
- double minTime = objects.Min(h => h.StartTime);
- var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
+ if (sourceIndex == -1)
+ return null;
- if (lastObject == null)
- return null;
+ OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
+ OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
- return new OsuDistanceSnapGrid(lastObject);
- }
+ return new OsuDistanceSnapGrid(sourceObject, targetObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 472267eb66..9418565907 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -4,13 +4,34 @@
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
- public override void HandleMovement(MoveSelectionEvent moveEvent)
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
+ Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
+ Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+
+ // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
+ foreach (var h in SelectedHitObjects.OfType())
+ {
+ if (h is Spinner)
+ {
+ // Spinners don't support position adjustments
+ continue;
+ }
+
+ // Stacking is not considered
+ minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
+ maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
+ }
+
+ if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
+ return false;
+
foreach (var h in SelectedHitObjects.OfType())
{
if (h is Spinner)
@@ -22,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += moveEvent.InstantDelta;
}
- base.HandleMovement(moveEvent);
+ return true;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
deleted file mode 100644
index 9106f4c7bd..0000000000
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/ConnectionRenderer.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
-using System.Collections.Generic;
-
-namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
-{
- ///
- /// Connects hit objects visually, for example with follow points.
- ///
- public abstract class ConnectionRenderer : LifetimeManagementContainer
- where T : HitObject
- {
- ///
- /// Hit objects to create connections for
- ///
- public abstract IEnumerable HitObjects { get; set; }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index 89ffddf4cb..db34ae1d87 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -12,6 +12,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
+ ///
+ /// A single follow point positioned between two adjacent s.
+ ///
public class FollowPoint : Container
{
private const float width = 8;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
new file mode 100644
index 0000000000..1e032eb977
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -0,0 +1,140 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
+{
+ ///
+ /// Visualises the s between two s.
+ ///
+ public class FollowPointConnection : CompositeDrawable
+ {
+ // Todo: These shouldn't be constants
+ private const int spacing = 32;
+ private const double preempt = 800;
+
+ ///
+ /// The start time of .
+ ///
+ public readonly Bindable StartTime = new Bindable();
+
+ ///
+ /// The which s will exit from.
+ ///
+ [NotNull]
+ public readonly DrawableOsuHitObject Start;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The which s will exit from.
+ public FollowPointConnection([NotNull] DrawableOsuHitObject start)
+ {
+ Start = start;
+
+ RelativeSizeAxes = Axes.Both;
+
+ StartTime.BindTo(Start.HitObject.StartTimeBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ bindEvents(Start);
+ }
+
+ private DrawableOsuHitObject end;
+
+ ///
+ /// The which s will enter.
+ ///
+ [CanBeNull]
+ public DrawableOsuHitObject End
+ {
+ get => end;
+ set
+ {
+ end = value;
+
+ if (end != null)
+ bindEvents(end);
+
+ if (IsLoaded)
+ scheduleRefresh();
+ else
+ refresh();
+ }
+ }
+
+ private void bindEvents(DrawableOsuHitObject drawableObject)
+ {
+ drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
+ drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
+ }
+
+ private void scheduleRefresh() => Scheduler.AddOnce(refresh);
+
+ private void refresh()
+ {
+ ClearInternal();
+
+ if (End == null)
+ return;
+
+ OsuHitObject osuStart = Start.HitObject;
+ OsuHitObject osuEnd = End.HitObject;
+
+ if (osuEnd.NewCombo)
+ return;
+
+ if (osuStart is Spinner || osuEnd is Spinner)
+ return;
+
+ Vector2 startPosition = osuStart.EndPosition;
+ Vector2 endPosition = osuEnd.Position;
+ double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
+ double endTime = osuEnd.StartTime;
+
+ Vector2 distanceVector = endPosition - startPosition;
+ int distance = (int)distanceVector.Length;
+ float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
+ double duration = endTime - startTime;
+
+ for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
+ {
+ float fraction = (float)d / distance;
+ Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
+ Vector2 pointEndPosition = startPosition + fraction * distanceVector;
+ double fadeOutTime = startTime + fraction * duration;
+ double fadeInTime = fadeOutTime - preempt;
+
+ FollowPoint fp;
+
+ AddInternal(fp = new FollowPoint
+ {
+ Position = pointStartPosition,
+ Rotation = rotation,
+ Alpha = 0,
+ Scale = new Vector2(1.5f * osuEnd.Scale),
+ });
+
+ using (fp.BeginAbsoluteSequence(fadeInTime))
+ {
+ fp.FadeIn(osuEnd.TimeFadeIn);
+ fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
+ fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
+ fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
+ }
+
+ fp.Expire(true);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index a269b87c75..be192080f9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -1,121 +1,110 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
-using osuTK;
+using System.Linq;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Objects.Types;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
- public class FollowPointRenderer : ConnectionRenderer
+ ///
+ /// Visualises connections between s.
+ ///
+ public class FollowPointRenderer : CompositeDrawable
{
- private int pointDistance = 32;
-
///
- /// Determines how much space there is between points.
+ /// All the s contained by this .
///
- public int PointDistance
- {
- get => pointDistance;
- set
- {
- if (pointDistance == value) return;
+ internal IReadOnlyList Connections => connections;
- pointDistance = value;
- update();
- }
- }
-
- private int preEmpt = 800;
-
- ///
- /// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
- ///
- public int PreEmpt
- {
- get => preEmpt;
- set
- {
- if (preEmpt == value) return;
-
- preEmpt = value;
- update();
- }
- }
-
- private IEnumerable hitObjects;
-
- public override IEnumerable HitObjects
- {
- get => hitObjects;
- set
- {
- hitObjects = value;
- update();
- }
- }
+ private readonly List connections = new List();
public override bool RemoveCompletedTransforms => false;
- private void update()
+ ///
+ /// Adds the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to add s for.
+ public void AddFollowPoints(DrawableOsuHitObject hitObject)
+ => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
+
+ ///
+ /// Removes the s around a .
+ /// This includes s leading into , and s exiting .
+ ///
+ /// The to remove s for.
+ public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
+
+ ///
+ /// Adds a to this .
+ ///
+ /// The to add.
+ /// The index of in .
+ private void addConnection(FollowPointConnection connection)
{
- ClearInternal();
+ AddInternal(connection);
- if (hitObjects == null)
- return;
+ // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
+ int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
- OsuHitObject prevHitObject = null;
-
- foreach (var currHitObject in hitObjects)
+ if (index < connections.Count - 1)
{
- if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
- {
- Vector2 startPosition = prevHitObject.EndPosition;
- Vector2 endPosition = currHitObject.Position;
- double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
- double endTime = currHitObject.StartTime;
+ // Update the connection's end point to the next connection's start point
+ // h1 -> -> -> h2
+ // connection nextGroup
- Vector2 distanceVector = endPosition - startPosition;
- int distance = (int)distanceVector.Length;
- float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
- double duration = endTime - startTime;
-
- for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
- {
- float fraction = (float)d / distance;
- Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
- Vector2 pointEndPosition = startPosition + fraction * distanceVector;
- double fadeOutTime = startTime + fraction * duration;
- double fadeInTime = fadeOutTime - PreEmpt;
-
- FollowPoint fp;
-
- AddInternal(fp = new FollowPoint
- {
- Position = pointStartPosition,
- Rotation = rotation,
- Alpha = 0,
- Scale = new Vector2(1.5f * currHitObject.Scale),
- });
-
- using (fp.BeginAbsoluteSequence(fadeInTime))
- {
- fp.FadeIn(currHitObject.TimeFadeIn);
- fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
-
- fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
- }
-
- fp.Expire(true);
- }
- }
-
- prevHitObject = currHitObject;
+ FollowPointConnection nextConnection = connections[index + 1];
+ connection.End = nextConnection.Start;
}
+ else
+ {
+ // The end point may be non-null during re-ordering
+ connection.End = null;
+ }
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the current connection's start point
+ // h1 -> -> -> h2
+ // prevGroup connection
+
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.Start;
+ }
+ }
+
+ ///
+ /// Removes a from this .
+ ///
+ /// The to remove.
+ /// Whether was removed.
+ private void removeGroup(FollowPointConnection connection)
+ {
+ RemoveInternal(connection);
+
+ int index = connections.IndexOf(connection);
+
+ if (index > 0)
+ {
+ // Update the previous connection's end point to the next connection's start point
+ // h1 -> -> -> h2 -> -> -> h3
+ // prevGroup connection nextGroup
+ // The current connection's end point is used since there may not be a next connection
+ FollowPointConnection previousConnection = connections[index - 1];
+ previousConnection.End = connection.End;
+ }
+
+ connections.Remove(connection);
+ }
+
+ private void onStartTimeChanged(FollowPointConnection connection)
+ {
+ // Naive but can be improved if performance becomes an issue
+ removeGroup(connection);
+ addConnection(connection);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
index 62c4ba5ee3..7c94568835 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Skinning;
@@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Children = new Drawable[]
{
- new CircularContainer
+ new Container
{
Masking = true,
- Origin = Anchor.Centre,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 60,
Colour = Color4.White.Opacity(0.5f),
},
- Child = new Box()
},
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 010bf072e8..f60b7e67b2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{
PathBindable.Value = value;
endPositionCache.Invalidate();
+
+ updateNestedPositions();
}
}
@@ -48,14 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
base.Position = value;
-
endPositionCache.Invalidate();
- if (HeadCircle != null)
- HeadCircle.Position = value;
-
- if (TailCircle != null)
- TailCircle.Position = EndPosition;
+ updateNestedPositions();
}
}
@@ -197,6 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
+ private void updateNestedPositions()
+ {
+ if (HeadCircle != null)
+ HeadCircle.Position = Position;
+
+ if (TailCircle != null)
+ TailCircle.Position = EndPosition;
+ }
+
private List getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 69e53d6eea..6d1ea4bbfc 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI;
-using System.Linq;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
- private readonly ConnectionRenderer connectionLayer;
+ private readonly FollowPointRenderer followPoints;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
- connectionLayer = new FollowPointRenderer
+ followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
@@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
};
base.Add(h);
+
+ followPoints.AddFollowPoints((DrawableOsuHitObject)h);
}
- public override void PostProcess()
+ public override bool Remove(DrawableHitObject h)
{
- connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType();
+ bool result = base.Remove(h);
+
+ if (result)
+ followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
+
+ return result;
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index 8347d255fa..3b18e41f30 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -38,9 +38,10 @@ namespace osu.Game.Rulesets.Osu.UI
});
}
- public override void Show()
+ protected override void PopIn()
{
- base.Show();
+ base.PopIn();
+
GameplayCursor.ActiveCursor.Hide();
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
clickToResumeCursor.Appear();
@@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
- public override void Hide()
+ protected override void PopOut()
{
+ base.PopOut();
+
localCursorContainer?.Expire();
localCursorContainer = null;
- GameplayCursor.ActiveCursor.Show();
-
- base.Hide();
+ GameplayCursor?.ActiveCursor?.Show();
}
protected override bool OnHover(HoverEvent e) => true;
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
index eaa8ca7ebb..8522a42739 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Reset height", () => changePlayfieldSize(6));
var controlPointInfo = new ControlPointInfo();
- controlPointInfo.TimingPoints.Add(new TimingControlPoint());
+ controlPointInfo.Add(0, new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
@@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
var cpi = new ControlPointInfo();
- cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
+ cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
var cpi = new ControlPointInfo();
- cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai });
+ cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
index ad2596931d..aaf113f216 100644
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
@@ -19,12 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
{
this.controlPoints = controlPoints;
- IEnumerable samplePoints;
- if (controlPoints.SamplePoints.Count == 0)
- // Get the default sample point
- samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) };
- else
- samplePoints = controlPoints.SamplePoints;
+ IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index de516d3142..2ecc516919 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -167,9 +167,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var controlPoints = beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
- Assert.AreEqual(42, controlPoints.DifficultyPoints.Count);
- Assert.AreEqual(42, controlPoints.SamplePoints.Count);
- Assert.AreEqual(42, controlPoints.EffectPoints.Count);
+ Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
+ Assert.AreEqual(34, controlPoints.SamplePoints.Count);
+ Assert.AreEqual(8, controlPoints.EffectPoints.Count);
var timingPoint = controlPoints.TimingPointAt(0);
Assert.AreEqual(956, timingPoint.Time);
@@ -191,7 +191,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
- Assert.AreEqual(48428, difficultyPoint.Time);
+ Assert.AreEqual(0, difficultyPoint.Time);
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(119637);
- Assert.AreEqual(119637, effectPoint.Time);
+ Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
}
@@ -262,6 +262,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestTimingPointResetsSpeedMultiplier()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPoints = decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
+ }
+ }
+
[Test]
public void TestDecodeBeatmapColours()
{
@@ -362,6 +377,23 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeControlPointDifficultyChange()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
+ Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
+ Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
+ Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
+ }
+ }
+
[Test]
public void TestDecodeControlPointCustomSampleBank()
{
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 9b4a90e9a9..fbb0416c45 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -273,6 +273,96 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(21, result.Links[0].Length);
}
+ [Test]
+ public void TestMarkdownFormatLinkWithInlineTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"osu!\") before..." });
+
+ Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(16, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithInlineTitleAndEscapedQuotes()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [this link format](https://osu.ppy.sh \"inner quote \\\" just to confuse \") before..." });
+
+ Assert.AreEqual("I haven't seen this link format before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(16, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithUrlInTextAndInlineTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://osu.ppy.sh](https://osu.ppy.sh \"https://osu.ppy.sh\") before..." });
+
+ Assert.AreEqual("I haven't seen https://osu.ppy.sh before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(18, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithUrlAndTextInTitle()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [oh no, text here! https://osu.ppy.sh](https://osu.ppy.sh) before..." });
+
+ Assert.AreEqual("I haven't seen oh no, text here! https://osu.ppy.sh before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(36, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkWithMisleadingUrlInText()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "I haven't seen [https://google.com](https://osu.ppy.sh) before..." });
+
+ Assert.AreEqual("I haven't seen https://google.com before...", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
+ Assert.AreEqual(15, result.Links[0].Index);
+ Assert.AreEqual(18, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkThatContractsIntoLargerLink()
+ {
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "super broken https://[osu.ppy](https://reddit.com).sh/" });
+
+ Assert.AreEqual("super broken https://osu.ppy.sh/", result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual("https://reddit.com", result.Links[0].Url);
+ Assert.AreEqual(21, result.Links[0].Index);
+ Assert.AreEqual(7, result.Links[0].Length);
+ }
+
+ [Test]
+ public void TestMarkdownFormatLinkDirectlyNextToRawLink()
+ {
+ // the raw link has a port at the end of it, so that the raw link regex terminates at the port and doesn't consume display text from the formatted one
+ Message result = MessageFormatter.FormatMessage(new Message { Content = "https://localhost:8080[https://osu.ppy.sh](https://osu.ppy.sh) should be two links" });
+
+ Assert.AreEqual("https://localhost:8080https://osu.ppy.sh should be two links", result.DisplayContent);
+ Assert.AreEqual(2, result.Links.Count);
+
+ Assert.AreEqual("https://localhost:8080", result.Links[0].Url);
+ Assert.AreEqual(0, result.Links[0].Index);
+ Assert.AreEqual(22, result.Links[0].Length);
+
+ Assert.AreEqual("https://osu.ppy.sh", result.Links[1].Url);
+ Assert.AreEqual(22, result.Links[1].Index);
+ Assert.AreEqual(18, result.Links[1].Length);
+ }
+
[Test]
public void TestChannelLink()
{
diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
new file mode 100644
index 0000000000..fe3cc375ea
--- /dev/null
+++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -0,0 +1,194 @@
+// 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.Clear();
+
+ composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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.Clear();
+ composer.EditorBeatmap.ControlPointInfo.Add(0, 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/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
new file mode 100644
index 0000000000..a51b90851c
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -0,0 +1,227 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Tests.NonVisual
+{
+ [TestFixture]
+ public class ControlPointInfoTest
+ {
+ [Test]
+ public void TestAdd()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestAddRedundantTiming()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
+ cpi.Add(1000, new TimingControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantDifficulty()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new DifficultyControlPoint()); // is redundant
+ cpi.Add(1000, new DifficultyControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantSample()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new SampleControlPoint()); // is redundant
+ cpi.Add(1000, new SampleControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddRedundantEffect()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new EffectControlPoint()); // is redundant
+ cpi.Add(1000, new EffectControlPoint()); // is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
+
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ var group2 = cpi.GroupAt(1000, true);
+
+ Assert.That(group, Is.EqualTo(group2));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestGroupAtLookupOnly()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(5000, true);
+ Assert.That(group, Is.Not.Null);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.GroupAt(1000), Is.Null);
+ Assert.That(cpi.GroupAt(5000), Is.Not.Null);
+ }
+
+ [Test]
+ public void TestAddRemoveGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ cpi.RemoveGroup(group);
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TestAddControlPointToGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ // usually redundant, but adding to group forces it to be added
+ group.Add(new DifficultyControlPoint());
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAddDuplicateControlPointToGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ group.Add(new DifficultyControlPoint());
+ group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void TestRemoveControlPointFromGroup()
+ {
+ var cpi = new ControlPointInfo();
+
+ var group = cpi.GroupAt(1000, true);
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+
+ var difficultyPoint = new DifficultyControlPoint();
+
+ group.Add(difficultyPoint);
+ group.Remove(difficultyPoint);
+
+ Assert.That(group.ControlPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TestOrdering()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+ cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
+ cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
+ cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
+ cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
+
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(8));
+
+ Assert.That(cpi.Groups, Is.Ordered.Ascending.By(nameof(ControlPointGroup.Time)));
+
+ Assert.That(cpi.AllControlPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
+ Assert.That(cpi.TimingPoints, Is.Ordered.Ascending.By(nameof(ControlPoint.Time)));
+ }
+
+ [Test]
+ public void TestClear()
+ {
+ var cpi = new ControlPointInfo();
+
+ cpi.Add(0, new TimingControlPoint());
+ cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
+ cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
+ cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
+ cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
+ cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
+
+ cpi.Clear();
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(0));
+ Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
+ Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu
new file mode 100644
index 0000000000..5f06fc33c8
--- /dev/null
+++ b/osu.Game.Tests/Resources/controlpoint-difficulty-multiplier.osu
@@ -0,0 +1,8 @@
+osu file format v7
+
+[TimingPoints]
+0,100,4,2,0,100,1,0
+12,500,4,2,0,100,1,0
+1000,-10,4,2,0,100,0,0
+2000,-54,4,2,0,100,0,0
+3000,-200,4,2,0,100,0,0
diff --git a/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu
new file mode 100644
index 0000000000..4512903c68
--- /dev/null
+++ b/osu.Game.Tests/Resources/timingpoint-speedmultiplier-reset.osu
@@ -0,0 +1,5 @@
+osu file format v14
+
+[TimingPoints]
+0,-200,4,1,0,100,0,0
+2000,100,1,1,0,100,1,0
diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
index df6740421b..f3f6444149 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
@@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddAssert("started", () => track.IsRunning);
AddStep("stop", () => track.Stop());
@@ -52,6 +53,8 @@ namespace osu.Game.Tests.Visual.Components
track2 = getOwnedTrack();
});
+ AddUntilStep("wait loaded", () => track1.IsLoaded && track2.IsLoaded);
+
AddStep("start track 1", () => track1.Start());
AddStep("start track 2", () => track2.Start());
AddAssert("track 1 stopped", () => !track1.IsRunning);
@@ -64,6 +67,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => track = getOwnedTrack());
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddStep("stop by owner", () => trackManager.StopAnyPlaying(this));
AddAssert("stopped", () => !track.IsRunning);
@@ -76,6 +80,7 @@ namespace osu.Game.Tests.Visual.Components
PreviewTrack track = null;
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
+ AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning);
@@ -89,16 +94,24 @@ namespace osu.Game.Tests.Visual.Components
{
var track = getTrack();
- Add(track);
+ LoadComponentAsync(track, Add);
return track;
}
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{
+ private readonly PreviewTrack track;
+
public TestTrackOwner(PreviewTrack track)
{
- AddInternal(track);
+ this.track = track;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LoadComponentAsync(track, AddInternal);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
index a9e5930478..e4c987923c 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,25 +25,27 @@ 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();
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}
[SetUp]
public void Setup() => Schedule(() =>
{
- Clear();
-
- editorBeatmap.ControlPointInfo.TimingPoints.Clear();
- editorBeatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { BeatLength = beat_length });
-
- BeatDivisor.Value = 1;
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ new TestDistanceSnapGrid(new HitObject(), grid_position)
+ };
});
[TestCase(1)]
@@ -56,65 +56,15 @@ 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()
+ public void TestLimitedDistance()
{
- 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}", () =>
+ AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
@@ -123,21 +73,17 @@ namespace osu.Game.Tests.Visual.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- grid = new TestDistanceSnapGrid(new HitObject(), grid_position)
+ new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
};
-
- func?.Invoke(grid);
});
}
private class TestDistanceSnapGrid : DistanceSnapGrid
{
- public new float Velocity = 1;
-
public new float DistanceSpacing => base.DistanceSpacing;
- public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
- : base(hitObject, centrePosition)
+ public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
+ : base(hitObject, nextHitObject, centrePosition)
{
}
@@ -152,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
int beatIndex = 0;
- for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -165,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -178,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -191,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0;
- for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
+ for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
@@ -203,11 +149,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) => (float)duration;
+
+ public double DistanceToDuration(double referenceTime, float distance) => distance;
+
+ public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+
+ public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
}
}
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index a8c2362910..6e5b3b93e9 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -10,9 +10,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
}
}
- private class StartStopButton : Button
+ private class StartStopButton : OsuButton
{
private IAdjustableClock adjustableClock;
private bool started;
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
index b997d6aaeb..3118e0cabe 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
@@ -28,18 +28,7 @@ namespace osu.Game.Tests.Visual.Editor
{
var testBeatmap = new Beatmap
{
- ControlPointInfo = new ControlPointInfo
- {
- TimingPoints =
- {
- new TimingControlPoint { Time = 0, BeatLength = 200 },
- new TimingControlPoint { Time = 100, BeatLength = 400 },
- new TimingControlPoint { Time = 175, BeatLength = 800 },
- new TimingControlPoint { Time = 350, BeatLength = 200 },
- new TimingControlPoint { Time = 450, BeatLength = 100 },
- new TimingControlPoint { Time = 500, BeatLength = 307.69230769230802 }
- }
- },
+ ControlPointInfo = new ControlPointInfo(),
HitObjects =
{
new HitCircle { StartTime = 0 },
@@ -47,6 +36,13 @@ namespace osu.Game.Tests.Visual.Editor
}
};
+ testBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
+ testBeatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 400 });
+ testBeatmap.ControlPointInfo.Add(175, new TimingControlPoint { BeatLength = 800 });
+ testBeatmap.ControlPointInfo.Add(350, new TimingControlPoint { BeatLength = 200 });
+ testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 });
+ testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 });
+
Beatmap.Value = CreateWorkingBeatmap(testBeatmap);
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
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/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
new file mode 100644
index 0000000000..121853d8d0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Timing;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+ [TestFixture]
+ public class TestSceneTimingScreen : EditorClockTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(ControlPointTable),
+ typeof(ControlPointSettings),
+ typeof(Section<>),
+ typeof(TimingSection),
+ typeof(EffectSection),
+ typeof(SampleSection),
+ typeof(DifficultySection),
+ typeof(RowAttribute)
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ Child = new TimingScreen();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index dcab964d6d..684e79b3f5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -47,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleSingleTimingPoint()
{
- var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -61,10 +62,10 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range / 2 },
- new TimingControlPoint { Time = 12000, BeatLength = time_range },
- new TimingControlPoint { Time = 100000, BeatLength = time_range });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
+ beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -75,9 +76,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range },
- new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -97,9 +98,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestNonRelativeScale()
{
- var beatmap = createBeatmap(
- new TimingControlPoint { BeatLength = time_range },
- new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
createTest(beatmap);
@@ -119,7 +120,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
{
- var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
@@ -132,7 +134,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSliderMultiplierAffectsNonRelativeBeatLength()
{
- var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range });
+ var beatmap = createBeatmap();
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2;
createTest(beatmap);
@@ -154,14 +157,11 @@ namespace osu.Game.Tests.Visual.Gameplay
/// Creates an , containing 10 hitobjects and user-provided timing points.
/// The hitobjects are spaced milliseconds apart.
///
- /// The timing points to add to the beatmap.
/// The .
- private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints)
+ private IBeatmap createBeatmap()
{
var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
- beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints);
-
for (int i = 0; i < 10; i++)
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 2df22df659..6e8975f11b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -69,6 +69,24 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(true);
}
+ [Test]
+ public void TestPauseWithResumeOverlay()
+ {
+ AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
+ AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1);
+
+ pauseAndConfirm();
+
+ resume();
+ confirmClockRunning(false);
+ confirmPauseOverlayShown(false);
+
+ pauseAndConfirm();
+
+ AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
+ confirmPaused();
+ }
+
[Test]
public void TestResumeWithResumeOverlaySkipped()
{
@@ -219,6 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player not exited", () => Player.IsCurrentScreen());
AddStep("exit", () => Player.Exit());
confirmExited();
+ confirmNoTrackAdjustments();
}
private void confirmPaused()
@@ -240,6 +259,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player exited", () => !Player.IsCurrentScreen());
}
+ private void confirmNoTrackAdjustments()
+ {
+ AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
+ }
+
private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause());
private void resume() => AddStep("resume", () => Player.Resume());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 0dfcda122f..7b22fedbd5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -7,11 +7,10 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
-using osu.Game.Screens.Play;
using osu.Game.Users;
-using osuTK;
using System;
using System.Collections.Generic;
+using osu.Game.Screens.Ranking.Pages;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -42,7 +41,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(80, 40),
};
});
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
index 944480243d..cdfb3beb19 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
@@ -20,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
+ State = { Value = Visibility.Visible }
});
Add(container = new ExampleContainer());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
index f3c8f89db7..7790126db5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs
@@ -3,11 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Pages;
@@ -22,11 +27,13 @@ namespace osu.Game.Tests.Visual.Gameplay
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(ScoreInfo),
typeof(Results),
typeof(ResultsPage),
typeof(ScoreResultsPage),
- typeof(LocalLeaderboardPage)
+ typeof(RetryButton),
+ typeof(ReplayDownloadButton),
+ typeof(LocalLeaderboardPage),
+ typeof(TestPlayer)
};
[BackgroundDependencyLoader]
@@ -42,26 +49,82 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0);
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
+ }
- LoadScreen(new SoloResults(new ScoreInfo
+ private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
+ {
+ TotalScore = 2845370,
+ Accuracy = 0.98,
+ MaxCombo = 123,
+ Rank = ScoreRank.A,
+ Date = DateTimeOffset.Now,
+ Statistics = new Dictionary
{
- TotalScore = 2845370,
- Accuracy = 0.98,
- MaxCombo = 123,
- Rank = ScoreRank.A,
- Date = DateTimeOffset.Now,
- Statistics = new Dictionary
+ { HitResult.Great, 50 },
+ { HitResult.Good, 20 },
+ { HitResult.Meh, 50 },
+ { HitResult.Miss, 1 }
+ },
+ User = new User
+ {
+ Username = "peppy",
+ }
+ });
+
+ [Test]
+ public void ResultsWithoutPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new OsuScreenStack(screen = createResultsScreen())
+ {
+ RelativeSizeAxes = Axes.Both
+ });
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
+ }
+
+ [Test]
+ public void ResultsWithPlayer()
+ {
+ TestSoloResults screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay present", () => screen.RetryOverlay != null);
+ }
+
+ private class TestResultsContainer : Container
+ {
+ [Cached(typeof(Player))]
+ private readonly Player player = new TestPlayer();
+
+ public TestResultsContainer(IScreen screen)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new OsuScreenStack(screen)
{
- { HitResult.Great, 50 },
- { HitResult.Good, 20 },
- { HitResult.Meh, 50 },
- { HitResult.Miss, 1 }
- },
- User = new User
- {
- Username = "peppy",
- }
- }));
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+ }
+
+ private class TestSoloResults : SoloResults
+ {
+ public HotkeyRetryOverlay RetryOverlay;
+
+ public TestSoloResults(ScoreInfo score)
+ : base(score)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RetryOverlay = InternalChildren.OfType().SingleOrDefault();
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
index 000832b784..61fed3013e 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs
@@ -33,23 +33,15 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestInstantLoad()
{
- bool logoVisible = false;
+ // visual only, very impossible to test this using asserts.
- AddStep("begin loading", () =>
+ AddStep("load immediately", () =>
{
loader = new TestLoader();
loader.AllowLoad.Set();
LoadScreen(loader);
});
-
- AddUntilStep("loaded", () =>
- {
- logoVisible = loader.Logo?.Alpha > 0;
- return loader.Logo != null && loader.ScreenLoaded;
- });
-
- AddAssert("logo was not visible", () => !logoVisible);
}
[Test]
@@ -58,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
AddStep("finish loading", () => loader.AllowLoad.Set());
- AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded);
+ AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded);
AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
new file mode 100644
index 0000000000..1f8df438fb
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs
@@ -0,0 +1,102 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.BeatmapSet;
+using osu.Game.Rulesets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneBeatmapRulesetSelector : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(BeatmapRulesetSelector),
+ typeof(BeatmapRulesetTabItem),
+ };
+
+ private readonly TestRulesetSelector selector;
+
+ public TestSceneBeatmapRulesetSelector()
+ {
+ Add(selector = new TestRulesetSelector());
+ }
+
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
+
+ [Test]
+ public void TestMultipleRulesetsBeatmapSet()
+ {
+ var enabledRulesets = rulesets.AvailableRulesets.Skip(1).Take(2);
+
+ AddStep("load multiple rulesets beatmapset", () =>
+ {
+ selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = enabledRulesets.Select(r => new BeatmapInfo { Ruleset = r }).ToList()
+ };
+ });
+
+ var tabItems = selector.TabContainer.TabItems;
+ AddAssert("other rulesets disabled", () => tabItems.Except(tabItems.Where(t => enabledRulesets.Any(r => r.Equals(t.Value)))).All(t => !t.Enabled.Value));
+ AddAssert("left-most ruleset selected", () => tabItems.First(t => t.Enabled.Value).Active.Value);
+ }
+
+ [Test]
+ public void TestSingleRulesetBeatmapSet()
+ {
+ var enabledRuleset = rulesets.AvailableRulesets.Last();
+
+ AddStep("load single ruleset beatmapset", () =>
+ {
+ selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = new List
+ {
+ new BeatmapInfo
+ {
+ Ruleset = enabledRuleset
+ }
+ }
+ };
+ });
+
+ AddAssert("single ruleset selected", () => selector.SelectedTab.Value.Equals(enabledRuleset));
+ }
+
+ [Test]
+ public void TestEmptyBeatmapSet()
+ {
+ AddStep("load empty beatmapset", () => selector.BeatmapSet = new BeatmapSetInfo
+ {
+ Beatmaps = new List()
+ });
+
+ AddAssert("no ruleset selected", () => selector.SelectedTab == null);
+ AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
+ }
+
+ [Test]
+ public void TestNullBeatmapSet()
+ {
+ AddStep("load null beatmapset", () => selector.BeatmapSet = null);
+
+ AddAssert("no ruleset selected", () => selector.SelectedTab == null);
+ AddAssert("all rulesets disabled", () => selector.TabContainer.TabItems.All(t => !t.Enabled.Value));
+ }
+
+ private class TestRulesetSelector : BeatmapRulesetSelector
+ {
+ public new TabItem SelectedTab => base.SelectedTab;
+
+ public new TabFillFlowContainer TabContainer => base.TabContainer;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 9f03d947b9..286971bc90 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -40,24 +40,19 @@ namespace osu.Game.Tests.Visual.Online
typeof(PreviewButton),
typeof(SuccessRate),
typeof(BeatmapAvailability),
+ typeof(BeatmapRulesetSelector),
+ typeof(BeatmapRulesetTabItem),
};
protected override bool UseOnlineAPI => true;
- private RulesetInfo taikoRuleset;
- private RulesetInfo maniaRuleset;
-
public TestSceneBeatmapSetOverlay()
{
Add(overlay = new TestBeatmapSetOverlay());
}
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
- {
- taikoRuleset = rulesets.GetRuleset(1);
- maniaRuleset = rulesets.GetRuleset(3);
- }
+ [Resolved]
+ private RulesetStore rulesets { get; set; }
[Test]
public void TestLoading()
@@ -111,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 9.99,
Version = @"TEST",
Length = 456000,
- Ruleset = maniaRuleset,
+ Ruleset = rulesets.GetRuleset(3),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 1,
@@ -189,7 +184,7 @@ namespace osu.Game.Tests.Visual.Online
StarDifficulty = 5.67,
Version = @"ANOTHER TEST",
Length = 123000,
- Ruleset = taikoRuleset,
+ Ruleset = rulesets.GetRuleset(1),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 9,
@@ -217,6 +212,54 @@ namespace osu.Game.Tests.Visual.Online
downloadAssert(false);
}
+ [Test]
+ public void TestMultipleRulesets()
+ {
+ AddStep("show multiple rulesets beatmap", () =>
+ {
+ var beatmaps = new List();
+
+ foreach (var ruleset in rulesets.AvailableRulesets.Skip(1))
+ {
+ beatmaps.Add(new BeatmapInfo
+ {
+ Version = ruleset.Name,
+ Ruleset = ruleset,
+ BaseDifficulty = new BeatmapDifficulty(),
+ OnlineInfo = new BeatmapOnlineInfo(),
+ Metrics = new BeatmapMetrics
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ });
+ }
+
+ overlay.ShowBeatmapSet(new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = @"multiple rulesets beatmap",
+ Artist = @"none",
+ Author = new User
+ {
+ Username = "BanchoBot",
+ Id = 3,
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers(),
+ },
+ Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
+ Beatmaps = beatmaps
+ });
+ });
+
+ AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value)));
+ AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
+ }
+
[Test]
public void TestHide()
{
@@ -281,12 +324,12 @@ namespace osu.Game.Tests.Visual.Online
private void downloadAssert(bool shown)
{
- AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown);
+ AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown);
}
private class TestBeatmapSetOverlay : BeatmapSetOverlay
{
- public bool DownloadButtonsVisible => Header.DownloadButtonsVisible;
+ public new Header Header => base.Header;
}
}
}
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.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 39c2fbfcc9..01400bf1d9 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -7,6 +7,10 @@ using osu.Game.Online.Chat;
using osu.Game.Users;
using osuTK;
using System;
+using System.Linq;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays.Chat;
namespace osu.Game.Tests.Visual.Online
{
@@ -42,14 +46,14 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private ChannelManager channelManager = new ChannelManager();
- private readonly StandAloneChatDisplay chatDisplay;
- private readonly StandAloneChatDisplay chatDisplay2;
+ private readonly TestStandAloneChatDisplay chatDisplay;
+ private readonly TestStandAloneChatDisplay chatDisplay2;
public TestSceneStandAloneChatDisplay()
{
Add(channelManager);
- Add(chatDisplay = new StandAloneChatDisplay
+ Add(chatDisplay = new TestStandAloneChatDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -57,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80)
});
- Add(chatDisplay2 = new StandAloneChatDisplay(true)
+ Add(chatDisplay2 = new TestStandAloneChatDisplay(true)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -119,6 +123,49 @@ namespace osu.Game.Tests.Visual.Online
Content = "Message from the future!",
Timestamp = DateTimeOffset.Now
}));
+
+ AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
+
+ const int messages_per_call = 10;
+ AddRepeatStep("add many messages", () =>
+ {
+ for (int i = 0; i < messages_per_call; i++)
+ testChannel.AddNewMessages(new Message(sequence++)
+ {
+ Sender = longUsernameUser,
+ Content = "Many messages! " + Guid.NewGuid(),
+ Timestamp = DateTimeOffset.Now
+ });
+ }, Channel.MAX_HISTORY / messages_per_call + 5);
+
+ AddAssert("Ensure no adjacent day separators", () =>
+ {
+ var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
+
+ foreach (var i in indices)
+ if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
+ return false;
+
+ return true;
+ });
+
+ AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
+ }
+
+ private class TestStandAloneChatDisplay : StandAloneChatDisplay
+ {
+ public TestStandAloneChatDisplay(bool textbox = false)
+ : base(textbox)
+ {
+ }
+
+ protected DrawableChannel DrawableChannel => InternalChildren.OfType().First();
+
+ protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child;
+
+ public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
+
+ public bool ScrolledToBottom => ScrollContainer.IsScrolledToEnd(1);
}
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index f87d6ebebb..8b82567a8d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -245,6 +245,28 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
}
+ [Test]
+ public void TestSortingStability()
+ {
+ var sets = new List();
+
+ for (int i = 0; i < 20; i++)
+ {
+ var set = createTestBeatmapSet(i);
+ set.Metadata.Artist = "same artist";
+ set.Metadata.Title = "same title";
+ sets.Add(set);
+ }
+
+ loadBeatmaps(sets);
+
+ AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
+ AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
+
+ AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
+ AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
+ }
+
[Test]
public void TestSortingWithFiltered()
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
index d84ffa0d93..ed44d82bce 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -10,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Lists;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
@@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
- private SortedList timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints;
+ private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
similarity index 82%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
index 700adad9cb..8179f92ffc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs
@@ -11,7 +11,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneLabelledComponent : OsuTestScene
+ public class TestSceneLabelledDrawable : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledComponent component;
+ LabelledDrawable component;
Child = new Container
{
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
+ Child = component = padded ? (LabelledDrawable)new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(),
};
component.Label = "a sample component";
@@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
- private class PaddedLabelledComponent : LabelledComponent
+ private class PaddedLabelledDrawable : LabelledDrawable
{
- public PaddedLabelledComponent()
+ public PaddedLabelledDrawable()
: base(true)
{
}
@@ -57,9 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
- private class NonPaddedLabelledComponent : LabelledComponent
+ private class NonPaddedLabelledDrawable : LabelledDrawable
{
- public NonPaddedLabelledComponent()
+ public NonPaddedLabelledDrawable()
: base(false)
{
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
index 53a2bfabbc..8208b55952 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
@@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
@@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledComponent component;
+ LabelledTextBox component;
Child = new Container
{
diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs
index 091a837745..a67daa2756 100644
--- a/osu.Game.Tournament/Screens/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/SetupScreen.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Tournament.Screens
};
}
- private class ActionableInfo : LabelledComponent
+ private class ActionableInfo : LabelledDrawable
{
private OsuButton button;
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index dbfa70704b..21552882ef 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -215,7 +215,7 @@ namespace osu.Game.Tournament
foreach (var r in ladder.Rounds)
foreach (var b in r.Beatmaps)
- if (b.BeatmapInfo == null)
+ if (b.BeatmapInfo == null && b.ID > 0)
{
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
req.Perform(API);
diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs
index 22ce7d4711..1234554e79 100644
--- a/osu.Game/Audio/PreviewTrack.cs
+++ b/osu.Game/Audio/PreviewTrack.cs
@@ -9,15 +9,18 @@ using osu.Framework.Threading;
namespace osu.Game.Audio
{
+ [LongRunningLoad]
public abstract class PreviewTrack : Component
{
///
/// Invoked when this has stopped playing.
+ /// Not invoked in a thread-safe context.
///
public event Action Stopped;
///
/// Invoked when this has started playing.
+ /// Not invoked in a thread-safe context.
///
public event Action Started;
@@ -29,7 +32,7 @@ namespace osu.Game.Audio
{
track = GetTrack();
if (track != null)
- track.Completed += () => Schedule(Stop);
+ track.Completed += Stop;
}
///
@@ -93,6 +96,7 @@ namespace osu.Game.Audio
hasStarted = false;
track.Stop();
+
Stopped?.Invoke();
}
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index e12c46ef16..fad2b5a5e8 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -46,18 +46,18 @@ namespace osu.Game.Audio
{
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
- track.Started += () =>
+ track.Started += () => Schedule(() =>
{
current?.Stop();
current = track;
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
- };
+ });
- track.Stopped += () =>
+ track.Stopped += () => Schedule(() =>
{
current = null;
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
- };
+ });
return track;
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index dd2044b4bc..6e485f642a 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -392,8 +392,15 @@ namespace osu.Game.Beatmaps
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); };
- // intentionally blocking to limit web request concurrency
- req.Perform(api);
+ try
+ {
+ // intentionally blocking to limit web request concurrency
+ req.Perform(api);
+ }
+ catch (Exception e)
+ {
+ LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})");
+ }
}
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index abe7e5e803..0861e00d8d 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -1,25 +1,30 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Beatmaps.ControlPoints
{
- public class ControlPoint : IComparable, IEquatable
+ public abstract class ControlPoint : IComparable, IEquatable
{
///
/// The time at which the control point takes effect.
///
- public double Time;
+ public double Time => controlPointGroup?.Time ?? 0;
- ///
- /// Whether this timing point was generated internally, as opposed to parsed from the underlying beatmap.
- ///
- internal bool AutoGenerated;
+ private ControlPointGroup controlPointGroup;
+
+ public void AttachGroup(ControlPointGroup pointGroup) => this.controlPointGroup = pointGroup;
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
- public bool Equals(ControlPoint other)
- => Time.Equals(other?.Time);
+ ///
+ /// Whether this control point is equivalent to another, ignoring time.
+ ///
+ /// Another control point to compare with.
+ /// Whether equivalent.
+ public abstract bool EquivalentTo(ControlPoint other);
+
+ public bool Equals(ControlPoint other) => Time.Equals(other?.Time) && EquivalentTo(other);
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
new file mode 100644
index 0000000000..cb73ce884e
--- /dev/null
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Beatmaps.ControlPoints
+{
+ public class ControlPointGroup : IComparable
+ {
+ public event Action ItemAdded;
+ public event Action ItemRemoved;
+
+ ///
+ /// The time at which the control point takes effect.
+ ///
+ public double Time { get; }
+
+ public IBindableList ControlPoints => controlPoints;
+
+ private readonly BindableList controlPoints = new BindableList();
+
+ public ControlPointGroup(double time)
+ {
+ Time = time;
+ }
+
+ public int CompareTo(ControlPointGroup other) => Time.CompareTo(other.Time);
+
+ public void Add(ControlPoint point)
+ {
+ var existing = controlPoints.FirstOrDefault(p => p.GetType() == point.GetType());
+
+ if (existing != null)
+ Remove(existing);
+
+ point.AttachGroup(this);
+
+ controlPoints.Add(point);
+ ItemAdded?.Invoke(point);
+ }
+
+ public void Remove(ControlPoint point)
+ {
+ controlPoints.Remove(point);
+ ItemRemoved?.Invoke(point);
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 855084ad02..c3e2b469ae 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
+using osu.Framework.Bindables;
using osu.Framework.Lists;
namespace osu.Game.Beatmaps.ControlPoints
@@ -12,57 +13,78 @@ namespace osu.Game.Beatmaps.ControlPoints
[Serializable]
public class ControlPointInfo
{
+ ///
+ /// All control points grouped by time.
+ ///
+ [JsonProperty]
+ public IBindableList Groups => groups;
+
+ private readonly BindableList groups = new BindableList();
+
///
/// All timing points.
///
[JsonProperty]
- public SortedList TimingPoints { get; private set; } = new SortedList(Comparer.Default);
+ public IReadOnlyList TimingPoints => timingPoints;
+
+ private readonly SortedList timingPoints = new SortedList(Comparer.Default);
///
/// All difficulty points.
///
[JsonProperty]
- public SortedList DifficultyPoints { get; private set; } = new SortedList(Comparer.Default);
+ public IReadOnlyList DifficultyPoints => difficultyPoints;
+
+ private readonly SortedList difficultyPoints = new SortedList(Comparer.Default);
///
/// All sound points.
///
[JsonProperty]
- public SortedList SamplePoints { get; private set; } = new SortedList(Comparer.Default);
+ public IReadOnlyList SamplePoints => samplePoints;
+
+ private readonly SortedList samplePoints = new SortedList(Comparer.Default);
///
/// All effect points.
///
[JsonProperty]
- public SortedList EffectPoints { get; private set; } = new SortedList(Comparer.Default);
+ public IReadOnlyList EffectPoints => effectPoints;
+
+ private readonly SortedList effectPoints = new SortedList(Comparer.Default);
+
+ ///
+ /// All control points, of all types.
+ ///
+ public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
///
/// Finds the difficulty control point that is active at .
///
/// The time to find the difficulty control point at.
/// The difficulty control point.
- public DifficultyControlPoint DifficultyPointAt(double time) => binarySearch(DifficultyPoints, time);
+ public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
///
/// Finds the effect control point that is active at .
///
/// The time to find the effect control point at.
/// The effect control point.
- public EffectControlPoint EffectPointAt(double time) => binarySearch(EffectPoints, time);
+ public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
///
/// Finds the sound control point that is active at .
///
/// The time to find the sound control point at.
/// The sound control point.
- public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
+ public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
///
/// Finds the timing control point that is active at .
///
/// The time to find the timing control point at.
/// The timing control point.
- public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
+ public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
///
/// Finds the maximum BPM represented by any timing control point.
@@ -85,24 +107,93 @@ namespace osu.Game.Beatmaps.ControlPoints
public double BPMMode =>
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ ///
+ /// Remove all s and return to a pristine state.
+ ///
+ public void Clear()
+ {
+ groups.Clear();
+ timingPoints.Clear();
+ difficultyPoints.Clear();
+ samplePoints.Clear();
+ effectPoints.Clear();
+ }
+
+ ///
+ /// Add a new . Note that the provided control point may not be added if the correct state is already present at the provided time.
+ ///
+ /// The time at which the control point should be added.
+ /// The control point to add.
+ /// Whether the control point was added.
+ public bool Add(double time, ControlPoint controlPoint)
+ {
+ if (checkAlreadyExisting(time, controlPoint))
+ return false;
+
+ GroupAt(time, true).Add(controlPoint);
+ return true;
+ }
+
+ public ControlPointGroup GroupAt(double time, bool addIfNotExisting = false)
+ {
+ var newGroup = new ControlPointGroup(time);
+
+ int i = groups.BinarySearch(newGroup);
+
+ if (i >= 0)
+ return groups[i];
+
+ if (addIfNotExisting)
+ {
+ newGroup.ItemAdded += groupItemAdded;
+ newGroup.ItemRemoved += groupItemRemoved;
+
+ groups.Insert(~i, newGroup);
+ return newGroup;
+ }
+
+ return null;
+ }
+
+ public void RemoveGroup(ControlPointGroup group)
+ {
+ group.ItemAdded -= groupItemAdded;
+ group.ItemRemoved -= groupItemRemoved;
+
+ groups.Remove(group);
+ }
+
///
/// Binary searches one of the control point lists to find the active control point at .
+ /// Includes logic for returning a specific point when no matching point is found.
///
/// The list to search.
/// The time to find the control point at.
/// The control point to use when is before any control points. If null, a new control point will be constructed.
- /// The active control point at .
- private T binarySearch(SortedList list, double time, T prePoint = null)
+ /// The active control point at , or a fallback if none found.
+ private T binarySearchWithFallback(IReadOnlyList list, double time, T prePoint = null)
where T : ControlPoint, new()
+ {
+ return binarySearch(list, time) ?? prePoint ?? new T();
+ }
+
+ ///
+ /// Binary searches one of the control point lists to find the active control point at .
+ ///
+ /// The list to search.
+ /// The time to find the control point at.
+ /// The active control point at .
+ private T binarySearch(IReadOnlyList list, double time)
+ where T : ControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
if (list.Count == 0)
- return new T();
+ return null;
if (time < list[0].Time)
- return prePoint ?? new T();
+ return null;
if (time >= list[list.Count - 1].Time)
return list[list.Count - 1];
@@ -125,5 +216,82 @@ namespace osu.Game.Beatmaps.ControlPoints
// l will be the first control point with Time > time, but we want the one before it
return list[l - 1];
}
+
+ ///
+ /// Check whether should be added.
+ ///
+ /// The time to find the timing control point at.
+ /// A point to be added.
+ /// Whether the new point should be added.
+ private bool checkAlreadyExisting(double time, ControlPoint newPoint)
+ {
+ ControlPoint existing = null;
+
+ switch (newPoint)
+ {
+ case TimingControlPoint _:
+ // Timing points are a special case and need to be added regardless of fallback availability.
+ existing = binarySearch(TimingPoints, time);
+ break;
+
+ case EffectControlPoint _:
+ existing = EffectPointAt(time);
+ break;
+
+ case SampleControlPoint _:
+ existing = SamplePointAt(time);
+ break;
+
+ case DifficultyControlPoint _:
+ existing = DifficultyPointAt(time);
+ break;
+ }
+
+ return existing?.EquivalentTo(newPoint) == true;
+ }
+
+ private void groupItemAdded(ControlPoint controlPoint)
+ {
+ switch (controlPoint)
+ {
+ case TimingControlPoint typed:
+ timingPoints.Add(typed);
+ break;
+
+ case EffectControlPoint typed:
+ effectPoints.Add(typed);
+ break;
+
+ case SampleControlPoint typed:
+ samplePoints.Add(typed);
+ break;
+
+ case DifficultyControlPoint typed:
+ difficultyPoints.Add(typed);
+ break;
+ }
+ }
+
+ private void groupItemRemoved(ControlPoint controlPoint)
+ {
+ switch (controlPoint)
+ {
+ case TimingControlPoint typed:
+ timingPoints.Remove(typed);
+ break;
+
+ case EffectControlPoint typed:
+ effectPoints.Remove(typed);
+ break;
+
+ case SampleControlPoint typed:
+ samplePoints.Remove(typed);
+ break;
+
+ case DifficultyControlPoint typed:
+ difficultyPoints.Remove(typed);
+ break;
+ }
+ }
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index a3e3121575..8b21098a51 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -1,26 +1,33 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using osuTK;
+using osu.Framework.Bindables;
namespace osu.Game.Beatmaps.ControlPoints
{
- public class DifficultyControlPoint : ControlPoint, IEquatable
+ public class DifficultyControlPoint : ControlPoint
{
+ ///
+ /// The speed multiplier at this control point.
+ ///
+ public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
+ {
+ Precision = 0.1,
+ Default = 1,
+ MinValue = 0.1,
+ MaxValue = 10
+ };
+
///
/// The speed multiplier at this control point.
///
public double SpeedMultiplier
{
- get => speedMultiplier;
- set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10);
+ get => SpeedMultiplierBindable.Value;
+ set => SpeedMultiplierBindable.Value = value;
}
- private double speedMultiplier = 1;
-
- public bool Equals(DifficultyControlPoint other)
- => base.Equals(other)
- && SpeedMultiplier.Equals(other?.SpeedMultiplier);
+ public override bool EquivalentTo(ControlPoint other) =>
+ other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 354d86dc13..369b93ff3d 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -1,24 +1,42 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
+using osu.Framework.Bindables;
namespace osu.Game.Beatmaps.ControlPoints
{
- public class EffectControlPoint : ControlPoint, IEquatable
+ public class EffectControlPoint : ControlPoint
{
///
- /// Whether this control point enables Kiai mode.
+ /// Whether the first bar line of this control point is ignored.
///
- public bool KiaiMode;
+ public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
///
/// Whether the first bar line of this control point is ignored.
///
- public bool OmitFirstBarLine;
+ public bool OmitFirstBarLine
+ {
+ get => OmitFirstBarLineBindable.Value;
+ set => OmitFirstBarLineBindable.Value = value;
+ }
- public bool Equals(EffectControlPoint other)
- => base.Equals(other)
- && KiaiMode == other?.KiaiMode && OmitFirstBarLine == other.OmitFirstBarLine;
+ ///
+ /// Whether this control point enables Kiai mode.
+ ///
+ public readonly BindableBool KiaiModeBindable = new BindableBool();
+
+ ///
+ /// Whether this control point enables Kiai mode.
+ ///
+ public bool KiaiMode
+ {
+ get => KiaiModeBindable.Value;
+ set => KiaiModeBindable.Value = value;
+ }
+
+ public override bool EquivalentTo(ControlPoint other) =>
+ other is EffectControlPoint otherTyped &&
+ KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 7bc7a9056d..42865c686c 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -1,24 +1,47 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
+using osu.Framework.Bindables;
using osu.Game.Audio;
namespace osu.Game.Beatmaps.ControlPoints
{
- public class SampleControlPoint : ControlPoint, IEquatable
+ public class SampleControlPoint : ControlPoint
{
public const string DEFAULT_BANK = "normal";
///
/// The default sample bank at this control point.
///
- public string SampleBank = DEFAULT_BANK;
+ public readonly Bindable SampleBankBindable = new Bindable(DEFAULT_BANK) { Default = DEFAULT_BANK };
+
+ ///
+ /// The speed multiplier at this control point.
+ ///
+ public string SampleBank
+ {
+ get => SampleBankBindable.Value;
+ set => SampleBankBindable.Value = value;
+ }
+
+ ///
+ /// The default sample bank at this control point.
+ ///
+ public readonly BindableInt SampleVolumeBindable = new BindableInt(100)
+ {
+ MinValue = 0,
+ MaxValue = 100,
+ Default = 100
+ };
///
/// The default sample volume at this control point.
///
- public int SampleVolume = 100;
+ public int SampleVolume
+ {
+ get => SampleVolumeBindable.Value;
+ set => SampleVolumeBindable.Value = value;
+ }
///
/// Create a SampleInfo based on the sample settings in this control point.
@@ -45,8 +68,8 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
- public bool Equals(SampleControlPoint other)
- => base.Equals(other)
- && string.Equals(SampleBank, other?.SampleBank) && SampleVolume == other?.SampleVolume;
+ public override bool EquivalentTo(ControlPoint other) =>
+ other is SampleControlPoint otherTyped &&
+ string.Equals(SampleBank, otherTyped.SampleBank) && SampleVolume == otherTyped.SampleVolume;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index ccb8a92b3a..51b3377394 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -1,34 +1,55 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using osuTK;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing;
namespace osu.Game.Beatmaps.ControlPoints
{
- public class TimingControlPoint : ControlPoint, IEquatable
+ public class TimingControlPoint : ControlPoint
{
///
/// The time signature at this control point.
///
- public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple;
+ public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
+
+ ///
+ /// The time signature at this control point.
+ ///
+ public TimeSignatures TimeSignature
+ {
+ get => TimeSignatureBindable.Value;
+ set => TimeSignatureBindable.Value = value;
+ }
public const double DEFAULT_BEAT_LENGTH = 1000;
///
/// The beat length at this control point.
///
- public virtual double BeatLength
+ public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
{
- get => beatLength;
- set => beatLength = MathHelper.Clamp(value, 6, 60000);
+ Default = DEFAULT_BEAT_LENGTH,
+ MinValue = 6,
+ MaxValue = 60000
+ };
+
+ ///
+ /// The beat length at this control point.
+ ///
+ public double BeatLength
+ {
+ get => BeatLengthBindable.Value;
+ set => BeatLengthBindable.Value = value;
}
- private double beatLength = DEFAULT_BEAT_LENGTH;
+ ///
+ /// The BPM at this control point.
+ ///
+ public double BPM => 60000 / BeatLength;
- public bool Equals(TimingControlPoint other)
- => base.Equals(other)
- && TimeSignature == other?.TimeSignature && beatLength.Equals(other.beatLength);
+ public override bool EquivalentTo(ControlPoint other) =>
+ other is TimingControlPoint otherTyped
+ && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
}
}
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs
index d0db7765c2..5245bc319d 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures;
namespace osu.Game.Beatmaps.Drawables
{
+ [LongRunningLoad]
public class BeatmapSetCover : Sprite
{
private readonly BeatmapSetInfo set;
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 786b7611b5..aeb5df46f8 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
@@ -50,6 +51,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap);
+ flushPendingPoints();
+
// Objects may be out of order *only* if a user has manually edited an .osu file.
// Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828).
// OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted)
@@ -369,104 +372,64 @@ namespace osu.Game.Beatmaps.Formats
if (timingChange)
{
var controlPoint = CreateTimingControlPoint();
- controlPoint.Time = time;
+
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
- handleTimingControlPoint(controlPoint);
+ addControlPoint(time, controlPoint, true);
}
- handleDifficultyControlPoint(new DifficultyControlPoint
+ addControlPoint(time, new LegacyDifficultyControlPoint
{
- Time = time,
SpeedMultiplier = speedMultiplier,
- AutoGenerated = timingChange
- });
+ }, timingChange);
- handleEffectControlPoint(new EffectControlPoint
+ addControlPoint(time, new EffectControlPoint
{
- Time = time,
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
- AutoGenerated = timingChange
- });
+ }, timingChange);
- handleSampleControlPoint(new LegacySampleControlPoint
+ addControlPoint(time, new LegacySampleControlPoint
{
- Time = time,
SampleBank = stringSampleSet,
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
- AutoGenerated = timingChange
- });
+ }, timingChange);
+
+ // To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but
+ // appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line
+ // with the same time value (allowing them to overwrite as necessary).
+ //
+ // The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal.
+ if (timingChange)
+ flushPendingPoints();
}
- private void handleTimingControlPoint(TimingControlPoint newPoint)
+ private readonly List pendingControlPoints = new List();
+ private double pendingControlPointsTime;
+
+ private void addControlPoint(double time, ControlPoint point, bool timingChange)
{
- var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time);
+ if (time != pendingControlPointsTime)
+ flushPendingPoints();
- if (existing.Time == newPoint.Time)
+ if (timingChange)
{
- // autogenerated points should not replace non-autogenerated.
- // this allows for incorrectly ordered timing points to still be correctly handled.
- if (newPoint.AutoGenerated && !existing.AutoGenerated)
- return;
-
- beatmap.ControlPointInfo.TimingPoints.Remove(existing);
+ beatmap.ControlPointInfo.Add(time, point);
+ return;
}
- beatmap.ControlPointInfo.TimingPoints.Add(newPoint);
+ pendingControlPoints.Add(point);
+ pendingControlPointsTime = time;
}
- private void handleDifficultyControlPoint(DifficultyControlPoint newPoint)
+ private void flushPendingPoints()
{
- var existing = beatmap.ControlPointInfo.DifficultyPointAt(newPoint.Time);
+ foreach (var p in pendingControlPoints)
+ beatmap.ControlPointInfo.Add(pendingControlPointsTime, p);
- if (existing.Time == newPoint.Time)
- {
- // autogenerated points should not replace non-autogenerated.
- // this allows for incorrectly ordered timing points to still be correctly handled.
- if (newPoint.AutoGenerated && !existing.AutoGenerated)
- return;
-
- beatmap.ControlPointInfo.DifficultyPoints.Remove(existing);
- }
-
- beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint);
- }
-
- private void handleEffectControlPoint(EffectControlPoint newPoint)
- {
- var existing = beatmap.ControlPointInfo.EffectPointAt(newPoint.Time);
-
- if (existing.Time == newPoint.Time)
- {
- // autogenerated points should not replace non-autogenerated.
- // this allows for incorrectly ordered timing points to still be correctly handled.
- if (newPoint.AutoGenerated && !existing.AutoGenerated)
- return;
-
- beatmap.ControlPointInfo.EffectPoints.Remove(existing);
- }
-
- beatmap.ControlPointInfo.EffectPoints.Add(newPoint);
- }
-
- private void handleSampleControlPoint(SampleControlPoint newPoint)
- {
- var existing = beatmap.ControlPointInfo.SamplePointAt(newPoint.Time);
-
- if (existing.Time == newPoint.Time)
- {
- // autogenerated points should not replace non-autogenerated.
- // this allows for incorrectly ordered timing points to still be correctly handled.
- if (newPoint.AutoGenerated && !existing.AutoGenerated)
- return;
-
- beatmap.ControlPointInfo.SamplePoints.Remove(existing);
- }
-
- beatmap.ControlPointInfo.SamplePoints.Add(newPoint);
+ pendingControlPoints.Clear();
}
private void handleHitObject(string line)
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 83d20da458..2b914669cb 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -189,7 +189,15 @@ namespace osu.Game.Beatmaps.Formats
Foreground = 3
}
- internal class LegacySampleControlPoint : SampleControlPoint, IEquatable
+ internal class LegacyDifficultyControlPoint : DifficultyControlPoint
+ {
+ public LegacyDifficultyControlPoint()
+ {
+ SpeedMultiplierBindable.Precision = double.Epsilon;
+ }
+ }
+
+ internal class LegacySampleControlPoint : SampleControlPoint
{
public int CustomSampleBank;
@@ -203,9 +211,9 @@ namespace osu.Game.Beatmaps.Formats
return baseInfo;
}
- public bool Equals(LegacySampleControlPoint other)
- => base.Equals(other)
- && CustomSampleBank == other?.CustomSampleBank;
+ public override bool EquivalentTo(ControlPoint other) =>
+ base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
+ CustomSampleBank == otherTyped.CustomSampleBank;
}
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
index 238187bf8f..527f520172 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
@@ -28,11 +28,15 @@ namespace osu.Game.Beatmaps.Formats
}
protected override TimingControlPoint CreateTimingControlPoint()
- => new LegacyDifficultyCalculatorControlPoint();
+ => new LegacyDifficultyCalculatorTimingControlPoint();
- private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
+ private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint
{
- public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH;
+ public LegacyDifficultyCalculatorTimingControlPoint()
+ {
+ BeatLengthBindable.MinValue = double.MinValue;
+ BeatLengthBindable.MaxValue = double.MaxValue;
+ }
}
}
}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index b567f0c0e3..9fed8e03ac 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Database
return Import(notification, paths);
}
- protected async Task Import(ProgressNotification notification, params string[] paths)
+ protected async Task> Import(ProgressNotification notification, params string[] paths)
{
notification.Progress = 0;
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
@@ -168,6 +168,8 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Completed;
}
+
+ return imported;
}
///
diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs
index 78c0837ce9..a81dff3475 100644
--- a/osu.Game/Database/DownloadableArchiveModelManager.cs
+++ b/osu.Game/Database/DownloadableArchiveModelManager.cs
@@ -76,21 +76,17 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
- await Import(notification, filename);
+ var imported = await Import(notification, filename);
+
+ // for now a failed import will be marked as a failed download for simplicity.
+ if (!imported.Any())
+ DownloadFailed?.Invoke(request);
+
currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};
- request.Failure += error =>
- {
- DownloadFailed?.Invoke(request);
-
- if (error is OperationCanceledException) return;
-
- notification.State = ProgressNotificationState.Cancelled;
- Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
- currentDownloads.Remove(request);
- };
+ request.Failure += triggerFailure;
notification.CancelRequested += () =>
{
@@ -103,11 +99,31 @@ namespace osu.Game.Database
currentDownloads.Add(request);
PostNotification?.Invoke(notification);
- Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning);
+ Task.Factory.StartNew(() =>
+ {
+ try
+ {
+ request.Perform(api);
+ }
+ catch (Exception error)
+ {
+ triggerFailure(error);
+ }
+ }, TaskCreationOptions.LongRunning);
DownloadBegan?.Invoke(request);
-
return true;
+
+ void triggerFailure(Exception error)
+ {
+ DownloadFailed?.Invoke(request);
+
+ if (error is OperationCanceledException) return;
+
+ notification.State = ProgressNotificationState.Cancelled;
+ Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!");
+ currentDownloads.Remove(request);
+ }
}
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index ea3318598f..2ae07b3cf8 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -166,19 +166,6 @@ namespace osu.Game.Database
// no-op. called by tooling.
}
- private class OsuDbLoggerProvider : ILoggerProvider
- {
- #region Disposal
-
- public void Dispose()
- {
- }
-
- #endregion
-
- public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
- }
-
private class OsuDbLogger : ILogger
{
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index 370d044ba4..2e76ab964f 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -104,14 +104,10 @@ namespace osu.Game.Graphics.Containers
defaultTiming = new TimingControlPoint
{
BeatLength = default_beat_length,
- AutoGenerated = true,
- Time = 0
};
defaultEffect = new EffectControlPoint
{
- Time = 0,
- AutoGenerated = true,
KiaiMode = false,
OmitFirstBarLine = false
};
diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index 15068d81c0..61391b7102 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -8,9 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Graphics;
-using osu.Framework.Logging;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Notifications;
using osu.Game.Users;
namespace osu.Game.Graphics.Containers
@@ -23,21 +20,12 @@ namespace osu.Game.Graphics.Containers
}
private OsuGame game;
- private ChannelManager channelManager;
- private Action showNotImplementedError;
[BackgroundDependencyLoader(true)]
- private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
+ private void load(OsuGame game)
{
// will be null in tests
this.game = game;
- this.channelManager = channelManager;
-
- showNotImplementedError = () => notifications?.Post(new SimpleNotification
- {
- Text = @"This link type is not yet supported!",
- Icon = FontAwesome.Solid.LifeRing,
- });
}
public void AddLinks(string text, List links)
@@ -56,85 +44,47 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
- AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
+ AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
previousLinkEnd = link.Index + link.Length;
}
AddText(text.Substring(previousLinkEnd));
}
- public IEnumerable AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action creationParameters = null)
- => createLink(AddText(text, creationParameters), text, url, linkType, linkArgument, tooltipText);
+ public void AddLink(string text, string url, Action creationParameters = null) =>
+ createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.External, url), url);
- public IEnumerable AddLink(string text, Action action, string tooltipText = null, Action creationParameters = null)
- => createLink(AddText(text, creationParameters), text, tooltipText: tooltipText, action: action);
+ public void AddLink(string text, Action action, string tooltipText = null, Action creationParameters = null)
+ => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
- public IEnumerable AddLink(IEnumerable text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
+ public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action creationParameters = null)
+ => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
+
+ public void AddLink(IEnumerable text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{
foreach (var t in text)
AddArbitraryDrawable(t);
- return createLink(text, null, url, linkType, linkArgument, tooltipText);
+ createLink(text, new LinkDetails(action, linkArgument), tooltipText);
}
- public IEnumerable AddUserLink(User user, Action creationParameters = null)
- => createLink(AddText(user.Username, creationParameters), user.Username, null, LinkAction.OpenUserProfile, user.Id.ToString(), "View profile");
+ public void AddUserLink(User user, Action creationParameters = null)
+ => createLink(AddText(user.Username, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.Id.ToString()), "View Profile");
- private IEnumerable createLink(IEnumerable drawables, string text, string url = null, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null, Action action = null)
+ private void createLink(IEnumerable drawables, LinkDetails link, string tooltipText, Action action = null)
{
AddInternal(new DrawableLinkCompiler(drawables.OfType().ToList())
{
RelativeSizeAxes = Axes.Both,
- TooltipText = tooltipText ?? (url != text ? url : string.Empty),
- Action = action ?? (() =>
+ TooltipText = tooltipText,
+ Action = () =>
{
- switch (linkType)
- {
- case LinkAction.OpenBeatmap:
- // TODO: proper query params handling
- if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId))
- game?.ShowBeatmap(beatmapId);
- break;
-
- case LinkAction.OpenBeatmapSet:
- if (int.TryParse(linkArgument, out int setId))
- game?.ShowBeatmapSet(setId);
- break;
-
- case LinkAction.OpenChannel:
- try
- {
- channelManager?.OpenChannel(linkArgument);
- }
- catch (ChannelNotFoundException)
- {
- Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
- }
-
- break;
-
- case LinkAction.OpenEditorTimestamp:
- case LinkAction.JoinMultiplayerMatch:
- case LinkAction.Spectate:
- showNotImplementedError?.Invoke();
- break;
-
- case LinkAction.External:
- game?.OpenUrlExternally(url);
- break;
-
- case LinkAction.OpenUserProfile:
- if (long.TryParse(linkArgument, out long userId))
- game?.ShowUser(userId);
- break;
-
- default:
- throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
- }
- }),
+ if (action != null)
+ action();
+ else
+ game.HandleLink(link);
+ },
});
-
- return drawables;
}
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
diff --git a/osu.Game/Graphics/UserInterface/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/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index c1810800a0..2750e61f0d 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
@@ -19,53 +21,104 @@ namespace osu.Game.Graphics.UserInterface
///
public class OsuButton : Button
{
- private Box hover;
+ public string Text
+ {
+ get => SpriteText?.Text;
+ set
+ {
+ if (SpriteText != null)
+ SpriteText.Text = value;
+ }
+ }
+
+ private Color4? backgroundColour;
+
+ public Color4 BackgroundColour
+ {
+ set
+ {
+ backgroundColour = value;
+ Background.FadeColour(value);
+ }
+ }
+
+ protected override Container Content { get; }
+
+ protected Box Hover;
+ protected Box Background;
+ protected SpriteText SpriteText;
public OsuButton()
{
Height = 40;
- Content.Masking = true;
- Content.CornerRadius = 5;
+ AddInternal(Content = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ CornerRadius = 5,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Background = new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Hover = new Box
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White.Opacity(.1f),
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue
+ },
+ SpriteText = CreateText(),
+ new HoverClickSounds(HoverSampleSet.Loud),
+ }
+ });
+
+ Enabled.BindValueChanged(enabledChanged, true);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- BackgroundColour = colours.BlueDark;
-
- AddRange(new Drawable[]
- {
- hover = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Blending = BlendingParameters.Additive,
- Colour = Color4.White.Opacity(0.1f),
- Alpha = 0,
- Depth = -1
- },
- new HoverClickSounds(HoverSampleSet.Loud),
- });
+ if (backgroundColour == null)
+ BackgroundColour = colours.BlueDark;
Enabled.ValueChanged += enabledChanged;
Enabled.TriggerChange();
}
- private void enabledChanged(ValueChangedEvent e)
+ protected override bool OnClick(ClickEvent e)
{
- this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
+ if (Enabled.Value)
+ {
+ Debug.Assert(backgroundColour != null);
+ Background.FlashColour(backgroundColour.Value, 200);
+ }
+
+ return base.OnClick(e);
}
protected override bool OnHover(HoverEvent e)
{
- hover.FadeIn(200);
+ if (Enabled.Value)
+ Hover.FadeIn(200, Easing.OutQuint);
+
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- hover.FadeOut(200);
base.OnHoverLost(e);
+
+ Hover.FadeOut(300);
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -80,12 +133,17 @@ namespace osu.Game.Graphics.UserInterface
return base.OnMouseUp(e);
}
- protected override SpriteText CreateText() => new OsuSpriteText
+ protected virtual SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.Bold)
};
+
+ private void enabledChanged(ValueChangedEvent e)
+ {
+ this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
+ }
}
}
diff --git a/osu.Game/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/Graphics/UserInterfaceV2/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
index 2e659825b7..1819b36667 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
@@ -1,132 +1,24 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.Containers;
-using osuTK;
+using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
- public abstract class LabelledComponent : CompositeDrawable
- where T : Drawable
+ public abstract class LabelledComponent : LabelledDrawable, IHasCurrentValue
+ where T : Drawable, IHasCurrentValue
{
- protected const float CONTENT_PADDING_VERTICAL = 10;
- protected const float CONTENT_PADDING_HORIZONTAL = 15;
- protected const float CORNER_RADIUS = 15;
-
- ///
- /// The component that is being displayed.
- ///
- protected readonly T Component;
-
- private readonly OsuTextFlowContainer labelText;
- private readonly OsuTextFlowContainer descriptionText;
-
- ///
- /// Creates a new .
- ///
- /// Whether the component should be padded or should be expanded to the bounds of this .
protected LabelledComponent(bool padded)
+ : base(padded)
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- CornerRadius = CORNER_RADIUS;
- Masking = true;
-
- InternalChildren = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.FromHex("1c2125"),
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Padding = padded
- ? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
- : new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
- Spacing = new Vector2(0, 12),
- Children = new Drawable[]
- {
- new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Content = new[]
- {
- new Drawable[]
- {
- labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- AutoSizeAxes = Axes.Both,
- Padding = new MarginPadding { Right = 20 }
- },
- new Container
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = Component = CreateComponent().With(d =>
- {
- d.Anchor = Anchor.CentreRight;
- d.Origin = Anchor.CentreRight;
- })
- }
- },
- },
- RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
- ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
- },
- descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
- Alpha = 0,
- }
- }
- }
- };
}
- [BackgroundDependencyLoader]
- private void load(OsuColour osuColour)
+ public Bindable Current
{
- descriptionText.Colour = osuColour.Yellow;
+ get => Component.Current;
+ set => Component.Current = value;
}
-
- public string Label
- {
- set => labelText.Text = value;
- }
-
- public string Description
- {
- set
- {
- descriptionText.Text = value;
-
- if (!string.IsNullOrEmpty(value))
- descriptionText.Show();
- else
- descriptionText.Hide();
- }
- }
-
- ///
- /// Creates the component that should be displayed.
- ///
- /// The component.
- protected abstract T CreateComponent();
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
new file mode 100644
index 0000000000..f44bd72aee
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+using osuTK;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public abstract class LabelledDrawable : CompositeDrawable
+ where T : Drawable
+ {
+ protected const float CONTENT_PADDING_VERTICAL = 10;
+ protected const float CONTENT_PADDING_HORIZONTAL = 15;
+ protected const float CORNER_RADIUS = 15;
+
+ ///
+ /// The component that is being displayed.
+ ///
+ protected readonly T Component;
+
+ private readonly OsuTextFlowContainer labelText;
+ private readonly OsuTextFlowContainer descriptionText;
+
+ ///
+ /// Creates a new .
+ ///
+ /// Whether the component should be padded or should be expanded to the bounds of this .
+ protected LabelledDrawable(bool padded)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ CornerRadius = CORNER_RADIUS;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.FromHex("1c2125"),
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = padded
+ ? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
+ : new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
+ Spacing = new Vector2(0, 12),
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Right = 20 }
+ },
+ new Container
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = Component = CreateComponent().With(d =>
+ {
+ d.Anchor = Anchor.CentreRight;
+ d.Origin = Anchor.CentreRight;
+ })
+ }
+ },
+ },
+ RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
+ ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
+ },
+ descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
+ Alpha = 0,
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ descriptionText.Colour = osuColour.Yellow;
+ }
+
+ public string Label
+ {
+ set => labelText.Text = value;
+ }
+
+ public string Description
+ {
+ set
+ {
+ descriptionText.Text = value;
+
+ if (!string.IsNullOrEmpty(value))
+ descriptionText.Show();
+ else
+ descriptionText.Hide();
+ }
+ }
+
+ ///
+ /// Creates the component that should be displayed.
+ ///
+ /// The component.
+ protected abstract T CreateComponent();
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs
index c973f1d13e..c374d80830 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSwitchButton.cs
@@ -3,7 +3,7 @@
namespace osu.Game.Graphics.UserInterfaceV2
{
- public class LabelledSwitchButton : LabelledComponent
+ public class LabelledSwitchButton : LabelledComponent
{
public LabelledSwitchButton()
: base(true)
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
index 50d2a14482..2cbe095d0b 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -8,7 +8,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
- public class LabelledTextBox : LabelledComponent
+ public class LabelledTextBox : LabelledComponent
{
public event TextBox.OnCommitHandler OnCommit;
diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs
index 9033e7529d..35f38ea7e8 100644
--- a/osu.Game/IO/Archives/ZipArchiveReader.cs
+++ b/osu.Game/IO/Archives/ZipArchiveReader.cs
@@ -1,30 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
-using SharpCompress.Common;
namespace osu.Game.IO.Archives
{
public sealed class ZipArchiveReader : ArchiveReader
{
- ///
- /// List of substrings that indicate a file should be ignored during the import process
- /// (usually due to representing no useful data and being autogenerated by the OS).
- ///
- private static readonly string[] filename_ignore_list =
- {
- // Mac-specific
- "__MACOSX",
- ".DS_Store",
- // Windows-specific
- "Thumbs.db"
- };
-
private readonly Stream archiveStream;
private readonly ZipArchive archive;
@@ -58,9 +44,7 @@ namespace osu.Game.IO.Archives
archiveStream.Dispose();
}
- private static bool canBeIgnored(IEntry entry) => filename_ignore_list.Any(ignoredName => entry.Key.IndexOf(ignoredName, StringComparison.OrdinalIgnoreCase) >= 0);
-
- public override IEnumerable Filenames => archive.Entries.Where(e => !canBeIgnored(e)).Select(e => e.Key).ToArray();
+ public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
public override Stream GetUnderlyingStream() => archiveStream;
}
diff --git a/osu.Game/Online/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/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index 9ec39c5cb1..451174a73c 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Online.Chat
{
public class Channel
{
- public readonly int MaxHistory = 300;
+ public const int MAX_HISTORY = 300;
///
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
@@ -80,8 +80,6 @@ namespace osu.Game.Online.Chat
///
public Bindable Joined = new Bindable();
- public const int MAX_HISTORY = 300;
-
[JsonConstructor]
public Channel()
{
@@ -162,8 +160,8 @@ namespace osu.Game.Online.Chat
{
// never purge local echos
int messageCount = Messages.Count - pendingMessages.Count;
- if (messageCount > MaxHistory)
- Messages.RemoveRange(0, messageCount - MaxHistory);
+ if (messageCount > MAX_HISTORY)
+ Messages.RemoveRange(0, messageCount - MAX_HISTORY);
}
}
}
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 24d17612ee..717de18c14 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Online.Chat
private static readonly Regex new_link_regex = new Regex(@"\[(?[a-z]+://[^ ]+) (?(((?<=\\)[\[\]])|[^\[\]])*(((?\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]");
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
- private static readonly Regex markdown_link_regex = new Regex(@"\[(?(((?<=\\)[\[\]])|[^\[\]])*(((?\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]\((?[a-z]+://[^ ]+)\)");
+ private static readonly Regex markdown_link_regex = new Regex(@"\[(?(((?<=\\)[\[\]])|[^\[\]])*(((?\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]\((?[a-z]+://[^ ]+)(\s+(?""([^""]|(?<=\\)"")*""))?\)");
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
// This is in the format (, [optional]):
@@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
- var details = getLinkDetails(linkText);
+ var details = GetLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
@@ -95,15 +95,21 @@ namespace osu.Game.Online.Chat
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
- var link = m.Groups["link"].Value;
- var indexLength = link.Length;
+ var linkText = m.Groups["link"].Value;
+ var indexLength = linkText.Length;
- var details = getLinkDetails(link);
- result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
+ var details = GetLinkDetails(linkText);
+ var link = new Link(linkText, index, indexLength, details.Action, details.Argument);
+
+ // sometimes an already-processed formatted link can reduce to a simple URL, too
+ // (example: [mean example - https://osu.ppy.sh](https://osu.ppy.sh))
+ // therefore we need to check if any of the pre-existing links contains the raw one we found
+ if (result.Links.All(existingLink => !existingLink.Overlaps(link)))
+ result.Links.Add(link);
}
}
- private static LinkDetails getLinkDetails(string url)
+ public static LinkDetails GetLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
@@ -249,17 +255,17 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text;
}
}
+ }
- public class LinkDetails
+ public class LinkDetails
+ {
+ public LinkAction Action;
+ public string Argument;
+
+ public LinkDetails(LinkAction action, string argument)
{
- public LinkAction Action;
- public string Argument;
-
- public LinkDetails(LinkAction action, string argument)
- {
- Action = action;
- Argument = argument;
- }
+ Action = action;
+ Argument = argument;
}
}
@@ -273,6 +279,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch,
Spectate,
OpenUserProfile,
+ Custom
}
public class Link : IComparable
@@ -292,6 +299,8 @@ namespace osu.Game.Online.Chat
Argument = argument;
}
+ public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length;
+
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
}
}
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 9387482f14..623db07938 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -21,6 +21,7 @@ using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
using Humanizer;
+using osu.Game.Online.API;
namespace osu.Game.Online.Leaderboards
{
@@ -37,6 +38,7 @@ namespace osu.Game.Online.Leaderboards
private readonly ScoreInfo score;
private readonly int rank;
+ private readonly bool allowHighlight;
private Box background;
private Container content;
@@ -49,17 +51,18 @@ namespace osu.Game.Online.Leaderboards
private List statisticsLabels;
- public LeaderboardScore(ScoreInfo score, int rank)
+ public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)
{
this.score = score;
this.rank = rank;
+ this.allowHighlight = allowHighlight;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(IAPIProvider api, OsuColour colour)
{
var user = score.User;
@@ -100,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
+ Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha,
},
},
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 4dcc181bea..1f823e6eba 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -215,31 +215,102 @@ namespace osu.Game
private ExternalLinkOpener externalLinkOpener;
- public void OpenUrlExternally(string url)
+ ///
+ /// Handle an arbitrary URL. Displays via in-game overlays where possible.
+ /// This can be called from a non-thread-safe non-game-loaded state.
+ ///
+ /// The URL to load.
+ public void HandleLink(string url) => HandleLink(MessageFormatter.GetLinkDetails(url));
+
+ ///
+ /// Handle a specific .
+ /// This can be called from a non-thread-safe non-game-loaded state.
+ ///
+ /// The link to load.
+ public void HandleLink(LinkDetails link) => Schedule(() =>
+ {
+ switch (link.Action)
+ {
+ case LinkAction.OpenBeatmap:
+ // TODO: proper query params handling
+ if (link.Argument != null && int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
+ ShowBeatmap(beatmapId);
+ break;
+
+ case LinkAction.OpenBeatmapSet:
+ if (int.TryParse(link.Argument, out int setId))
+ ShowBeatmapSet(setId);
+ break;
+
+ case LinkAction.OpenChannel:
+ ShowChannel(link.Argument);
+ break;
+
+ case LinkAction.OpenEditorTimestamp:
+ case LinkAction.JoinMultiplayerMatch:
+ case LinkAction.Spectate:
+ waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
+ {
+ Text = @"This link type is not yet supported!",
+ Icon = FontAwesome.Solid.LifeRing,
+ }));
+ break;
+
+ case LinkAction.External:
+ OpenUrlExternally(link.Argument);
+ break;
+
+ case LinkAction.OpenUserProfile:
+ if (long.TryParse(link.Argument, out long userId))
+ ShowUser(userId);
+ break;
+
+ default:
+ throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action.");
+ }
+ });
+
+ public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{
if (url.StartsWith("/"))
url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url);
- }
+ });
+
+ ///
+ /// Open a specific channel in chat.
+ ///
+ /// The channel to display.
+ public void ShowChannel(string channel) => waitForReady(() => channelManager, _ =>
+ {
+ try
+ {
+ channelManager.OpenChannel(channel);
+ }
+ catch (ChannelNotFoundException)
+ {
+ Logger.Log($"The requested channel \"{channel}\" does not exist");
+ }
+ });
///
/// Show a beatmap set as an overlay.
///
/// The set to display.
- public void ShowBeatmapSet(int setId) => beatmapSetOverlay.FetchAndShowBeatmapSet(setId);
+ public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId));
///
/// Show a user's profile as an overlay.
///
/// The user to display.
- public void ShowUser(long userId) => userProfile.ShowUser(userId);
+ public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
///
/// Show a beatmap's set as an overlay, displaying the given beatmap.
///
/// The beatmap to show.
- public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
+ public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
///
/// Present a beatmap at song select immediately.
@@ -397,6 +468,23 @@ namespace osu.Game
performFromMainMenuTask = Schedule(() => performFromMainMenu(action, taskName));
}
+ ///
+ /// Wait for the game (and target component) to become loaded and then run an action.
+ ///
+ /// A function to retrieve a (potentially not-yet-constructed) target instance.
+ /// The action to perform on the instance when load is confirmed.
+ /// The type of the target instance.
+ private void waitForReady(Func retrieveInstance, Action action)
+ where T : Drawable
+ {
+ var instance = retrieveInstance();
+
+ if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true)
+ Schedule(() => waitForReady(retrieveInstance, action));
+ else
+ action(instance);
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
index 28947b6f22..bf2a92cd4f 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -27,10 +28,11 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7;
private const float tile_spacing = 2;
- private readonly DifficultiesContainer difficulties;
private readonly OsuSpriteText version, starRating;
private readonly Statistic plays, favourites;
+ public readonly DifficultiesContainer Difficulties;
+
public readonly Bindable Beatmap = new Bindable();
private BeatmapSetInfo beatmapSet;
@@ -43,38 +45,10 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmapSet) return;
beatmapSet = value;
-
updateDisplay();
}
}
- private void updateDisplay()
- {
- difficulties.Clear();
-
- if (BeatmapSet != null)
- {
- difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty).Select(b => new DifficultySelectorButton(b)
- {
- State = DifficultySelectorState.NotSelected,
- OnHovered = beatmap =>
- {
- showBeatmap(beatmap);
- starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
- starRating.FadeIn(100);
- },
- OnClicked = beatmap => { Beatmap.Value = beatmap; },
- });
- }
-
- starRating.FadeOut(100);
- Beatmap.Value = BeatmapSet?.Beatmaps.FirstOrDefault();
- plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
- favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
-
- updateDifficultyButtons();
- }
-
public BeatmapPicker()
{
RelativeSizeAxes = Axes.X;
@@ -89,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- difficulties = new DifficultiesContainer
+ Difficulties = new DifficultiesContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -147,6 +121,9 @@ namespace osu.Game.Overlays.BeatmapSet
};
}
+ [Resolved]
+ private IBindable ruleset { get; set; }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -158,10 +135,39 @@ namespace osu.Game.Overlays.BeatmapSet
{
base.LoadComplete();
+ ruleset.ValueChanged += r => updateDisplay();
+
// done here so everything can bind in intialization and get the first trigger
Beatmap.TriggerChange();
}
+ private void updateDisplay()
+ {
+ Difficulties.Clear();
+
+ if (BeatmapSet != null)
+ {
+ Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Where(b => b.Ruleset.Equals(ruleset.Value)).OrderBy(b => b.StarDifficulty).Select(b => new DifficultySelectorButton(b)
+ {
+ State = DifficultySelectorState.NotSelected,
+ OnHovered = beatmap =>
+ {
+ showBeatmap(beatmap);
+ starRating.Text = beatmap.StarDifficulty.ToString("Star Difficulty 0.##");
+ starRating.FadeIn(100);
+ },
+ OnClicked = beatmap => { Beatmap.Value = beatmap; },
+ });
+ }
+
+ starRating.FadeOut(100);
+ Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap;
+ plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0;
+ favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0;
+
+ updateDifficultyButtons();
+ }
+
private void showBeatmap(BeatmapInfo beatmap)
{
version.Text = beatmap?.Version;
@@ -169,10 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDifficultyButtons()
{
- difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
+ Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
}
- private class DifficultiesContainer : FillFlowContainer
+ public class DifficultiesContainer : FillFlowContainer
{
public Action OnLostHover;
@@ -183,7 +189,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
- private class DifficultySelectorButton : OsuClickableContainer, IStateful
+ public class DifficultySelectorButton : OsuClickableContainer, IStateful
{
private const float transition_duration = 100;
private const float size = 52;
@@ -320,7 +326,7 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
- private enum DifficultySelectorState
+ public enum DifficultySelectorState
{
Selected,
NotSelected,
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
new file mode 100644
index 0000000000..a0bedc848e
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osuTK;
+using System.Linq;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public class BeatmapRulesetSelector : RulesetSelector
+ {
+ private readonly Bindable beatmapSet = new Bindable();
+
+ public BeatmapSetInfo BeatmapSet
+ {
+ get => beatmapSet.Value;
+ set
+ {
+ // propagate value to tab items first to enable only available rulesets.
+ beatmapSet.Value = value;
+
+ SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value));
+ }
+ }
+
+ public BeatmapRulesetSelector()
+ {
+ AutoSizeAxes = Axes.Both;
+ }
+
+ protected override TabItem CreateTabItem(RulesetInfo value) => new BeatmapRulesetTabItem(value)
+ {
+ BeatmapSet = { BindTarget = beatmapSet }
+ };
+
+ protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ };
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
new file mode 100644
index 0000000000..cdea49afe7
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs
@@ -0,0 +1,145 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets;
+using osuTK;
+using osuTK.Graphics;
+using System.Linq;
+
+namespace osu.Game.Overlays.BeatmapSet
+{
+ public class BeatmapRulesetTabItem : TabItem
+ {
+ private readonly OsuSpriteText name, count;
+ private readonly Box bar;
+
+ public readonly Bindable BeatmapSet = new Bindable();
+
+ public override bool PropagatePositionalInputSubTree => Enabled.Value && !Active.Value && base.PropagatePositionalInputSubTree;
+
+ public BeatmapRulesetTabItem(RulesetInfo value)
+ : base(value)
+ {
+ AutoSizeAxes = Axes.Both;
+
+ FillFlowContainer nameContainer;
+
+ Children = new Drawable[]
+ {
+ nameContainer = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding { Bottom = 7.5f },
+ Spacing = new Vector2(2.5f),
+ Children = new Drawable[]
+ {
+ name = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = value.Name,
+ Font = OsuFont.Default.With(size: 18),
+ },
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 4f,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black.Opacity(0.5f),
+ },
+ count = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Horizontal = 5f },
+ Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
+ }
+ }
+ }
+ }
+ },
+ bar = new Box
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ },
+ new HoverClickSounds(),
+ };
+
+ BeatmapSet.BindValueChanged(setInfo =>
+ {
+ var beatmapsCount = setInfo.NewValue?.Beatmaps.Count(b => b.Ruleset.Equals(Value)) ?? 0;
+
+ count.Text = beatmapsCount.ToString();
+ count.Alpha = beatmapsCount > 0 ? 1f : 0f;
+
+ Enabled.Value = beatmapsCount > 0;
+ }, true);
+
+ Enabled.BindValueChanged(v => nameContainer.Alpha = v.NewValue ? 1f : 0.5f, true);
+ }
+
+ [Resolved]
+ private OsuColour colour { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ count.Colour = colour.Gray9;
+ bar.Colour = colour.Blue;
+
+ updateState();
+ }
+
+ private void updateState()
+ {
+ var isHoveredOrActive = IsHovered || Active.Value;
+
+ bar.ResizeHeightTo(isHoveredOrActive ? 4 : 0, 200, Easing.OutQuint);
+
+ name.Colour = isHoveredOrActive ? colour.GrayE : colour.GrayC;
+ name.Font = name.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
+ }
+
+ #region Hovering and activation logic
+
+ protected override void OnActivated() => updateState();
+
+ protected override void OnDeactivated() => updateState();
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateState();
+ return false;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e) => updateState();
+
+ #endregion
+ }
+}
diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs
index 260a989628..7b42e7e459 100644
--- a/osu.Game/Overlays/BeatmapSet/Header.cs
+++ b/osu.Game/Overlays/BeatmapSet/Header.cs
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -16,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct;
+using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -39,6 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet
public bool DownloadButtonsVisible => downloadButtonsContainer.Any();
+ public readonly BeatmapRulesetSelector RulesetSelector;
public readonly BeatmapPicker Picker;
private readonly FavouriteButton favouriteButton;
@@ -47,6 +50,9 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly LoadingAnimation loading;
+ [Cached(typeof(IBindable))]
+ private readonly Bindable ruleset = new Bindable();
+
public Header()
{
ExternalLinkButton externalLink;
@@ -69,12 +75,18 @@ namespace osu.Game.Overlays.BeatmapSet
{
RelativeSizeAxes = Axes.X,
Height = tabs_height,
- Children = new[]
+ Children = new Drawable[]
{
tabsBg = new Box
{
RelativeSizeAxes = Axes.Both,
},
+ RulesetSelector = new BeatmapRulesetSelector
+ {
+ Current = ruleset,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ }
},
},
new Container
@@ -223,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.BindValueChanged(setInfo =>
{
- Picker.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
+ Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue;
cover.BeatmapSet = setInfo.NewValue;
if (setInfo.NewValue == null)
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index bce1be5941..d8488b21ab 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Changelog
t.Font = fontLarge;
t.Colour = entryColour;
});
- title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External,
+ title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl,
creationParameters: t =>
{
t.Font = fontLarge;
@@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour;
});
else if (entry.GithubUser.GithubUrl != null)
- title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t =>
+ title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t =>
{
t.Font = fontMedium;
t.Colour = entryColour;
diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
index 9c3504f477..adcd33fb48 100644
--- a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs
@@ -44,7 +44,17 @@ namespace osu.Game.Overlays.Changelog
req.Failure += _ => complete = true;
// This is done on a separate thread to support cancellation below
- Task.Run(() => req.Perform(api));
+ Task.Run(() =>
+ {
+ try
+ {
+ req.Perform(api);
+ }
+ catch
+ {
+ complete = true;
+ }
+ });
while (!complete)
{
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index dfe3669813..559989af5c 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -170,6 +170,7 @@ namespace osu.Game.Overlays
var tcs = new TaskCompletionSource();
var req = new GetChangelogRequest();
+
req.Success += res => Schedule(() =>
{
// remap streams to builds to ensure model equality
@@ -183,8 +184,22 @@ namespace osu.Game.Overlays
tcs.SetResult(true);
});
- req.Failure += _ => initialFetchTask = null;
- req.Perform(API);
+
+ req.Failure += _ =>
+ {
+ initialFetchTask = null;
+ tcs.SetResult(false);
+ };
+
+ try
+ {
+ req.Perform(API);
+ }
+ catch
+ {
+ initialFetchTask = null;
+ tcs.SetResult(false);
+ }
await tcs.Task;
});
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index 6cdbfabe0f..427bd8dcde 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
@@ -89,8 +89,10 @@ namespace osu.Game.Overlays.Chat
private void newMessagesArrived(IEnumerable newMessages)
{
+ bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage);
+
// Add up to last Channel.MAX_HISTORY messages
- var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory));
+ var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
Message lastMessage = chatLines.LastOrDefault()?.Message;
@@ -103,19 +105,32 @@ namespace osu.Game.Overlays.Chat
lastMessage = message;
}
- if (scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage))
- scrollToEnd();
-
var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
- int count = staleMessages.Length - Channel.MaxHistory;
+ int count = staleMessages.Length - Channel.MAX_HISTORY;
- for (int i = 0; i < count; i++)
+ if (count > 0)
{
- var d = staleMessages[i];
- if (!scroll.IsScrolledToEnd(10))
+ void expireAndAdjustScroll(Drawable d)
+ {
scroll.OffsetScrollPosition(-d.DrawHeight);
- d.Expire();
+ d.Expire();
+ }
+
+ for (int i = 0; i < count; i++)
+ expireAndAdjustScroll(staleMessages[i]);
+
+ // remove all adjacent day separators after stale message removal
+ for (int i = 0; i < ChatLineFlow.Count - 1; i++)
+ {
+ if (!(ChatLineFlow[i] is DaySeparator)) break;
+ if (!(ChatLineFlow[i + 1] is DaySeparator)) break;
+
+ expireAndAdjustScroll(ChatLineFlow[i]);
+ }
}
+
+ if (shouldScrollToEnd)
+ scrollToEnd();
}
private void pendingMessageResolved(Message existing, Message updated)
@@ -141,7 +156,7 @@ namespace osu.Game.Overlays.Chat
private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd());
- protected class DaySeparator : Container
+ public class DaySeparator : Container
{
public float TextSize
{
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/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index c1c871aade..6c89e1f549 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -168,12 +168,13 @@ namespace osu.Game.Overlays
},
}
},
- progressBar = new ProgressBar
+ progressBar = new HoverableProgressBar
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
- Height = progress_height,
+ Height = progress_height / 2,
FillColour = colours.Yellow,
+ BackgroundColour = colours.YellowDarker.Opacity(0.5f),
OnSeek = musicController.SeekTo
}
},
@@ -401,5 +402,20 @@ namespace osu.Game.Overlays
return base.OnDragEnd(e);
}
}
+
+ private class HoverableProgressBar : ProgressBar
+ {
+ protected override bool OnHover(HoverEvent e)
+ {
+ this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ this.ResizeHeightTo(progress_height / 2, 500, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index b02b1a5489..a8bbccb168 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -200,6 +200,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
private TextBox username;
private TextBox password;
+ private ShakeContainer shakeSignIn;
private IAPIProvider api;
public Action RequestHide;
@@ -208,6 +209,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api.Login(username.Text, password.Text);
+ else
+ shakeSignIn.Shake();
}
[BackgroundDependencyLoader(permitNulls: true)]
@@ -244,10 +247,23 @@ namespace osu.Game.Overlays.Settings.Sections.General
LabelText = "Stay signed in",
Bindable = config.GetBindable(OsuSetting.SavePassword),
},
- new SettingsButton
+ new Container
{
- Text = "Sign in",
- Action = performLogin
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ shakeSignIn = new ShakeContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new SettingsButton
+ {
+ Text = "Sign in",
+ Action = performLogin
+ },
+ }
+ }
},
new SettingsButton
{
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index a94f76e7af..68836bc6b3 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@@ -9,21 +8,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
- public class SidebarButton : Button
+ public class SidebarButton : OsuButton
{
private readonly SpriteIcon drawableIcon;
private readonly SpriteText headerText;
private readonly Box selectionIndicator;
private readonly Container text;
- public new Action Action;
private SettingsSection section;
@@ -62,12 +58,11 @@ namespace osu.Game.Overlays.Settings
public SidebarButton()
{
- BackgroundColour = OsuColour.Gray(60);
- Background.Alpha = 0;
-
Height = Sidebar.DEFAULT_WIDTH;
RelativeSizeAxes = Axes.X;
+ BackgroundColour = Color4.Black;
+
AddRange(new Drawable[]
{
text = new Container
@@ -99,7 +94,6 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
- new HoverClickSounds(HoverSampleSet.Loud),
});
}
@@ -108,23 +102,5 @@ namespace osu.Game.Overlays.Settings
{
selectionIndicator.Colour = colours.Yellow;
}
-
- protected override bool OnClick(ClickEvent e)
- {
- Action?.Invoke(section);
- return base.OnClick(e);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- Background.FadeTo(0.4f, 200);
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- Background.FadeTo(0, 200);
- base.OnHoverLost(e);
- }
}
}
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index d028664fe0..2948231c4b 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -123,9 +123,9 @@ namespace osu.Game.Overlays
var button = new SidebarButton
{
Section = section,
- Action = s =>
+ Action = () =>
{
- SectionsContainer.ScrollTo(s);
+ SectionsContainer.ScrollTo(section);
Sidebar.State = ExpandedState.Contracted;
},
};
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 6396301add..1c942e52ce 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;
@@ -144,7 +148,7 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap = new EditorBeatmap(playableBeatmap);
EditorBeatmap.HitObjectAdded += addHitObject;
EditorBeatmap.HitObjectRemoved += removeHitObject;
- EditorBeatmap.StartTimeChanged += updateHitObject;
+ EditorBeatmap.StartTimeChanged += UpdateHitObject;
var dependencies = new DependencyContainer(parent);
dependencies.CacheAs(EditorBeatmap);
@@ -221,11 +225,7 @@ namespace osu.Game.Rulesets.Edit
private ScheduledDelegate scheduledUpdate;
- private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
-
- private void removeHitObject(HitObject hitObject) => updateHitObject(null);
-
- private void updateHitObject([CanBeNull] HitObject hitObject)
+ public override void UpdateHitObject(HitObject hitObject)
{
scheduledUpdate?.Cancel();
scheduledUpdate = Schedule(() =>
@@ -236,6 +236,10 @@ namespace osu.Game.Rulesets.Edit
});
}
+ private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject);
+
+ private void removeHitObject(HitObject hitObject) => UpdateHitObject(null);
+
public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
@@ -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,22 @@ namespace osu.Game.Rulesets.Edit
[CanBeNull]
protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null;
- public abstract Vector2 GetSnappedPosition(Vector2 position);
+ ///
+ /// Updates a , invoking and re-processing the beatmap.
+ ///
+ /// The to update.
+ public abstract void UpdateHitObject([CanBeNull] HitObject hitObject);
- 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/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 3076ad081a..2923411ce1 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -3,12 +3,12 @@
using System;
using osu.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
-using osu.Framework.Input.Events;
-using osu.Framework.Input.States;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
@@ -29,31 +29,18 @@ namespace osu.Game.Rulesets.Edit
///
public event Action Deselected;
- ///
- /// Invoked when this has requested selection.
- /// Will fire even if already selected. Does not actually perform selection.
- ///
- public event Action SelectionRequested;
-
- ///
- /// Invoked when this has requested drag.
- ///
- public event Action DragRequested;
-
///
/// The which this applies to.
///
public readonly DrawableHitObject DrawableObject;
- ///
- /// The screen-space position of prior to handling a movement event.
- ///
- internal Vector2 ScreenSpaceMovementStartPosition { get; private set; }
-
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;
+ [Resolved(CanBeNull = true)]
+ private HitObjectComposer composer { get; set; }
+
protected SelectionBlueprint(DrawableHitObject drawableObject)
{
DrawableObject = drawableObject;
@@ -95,6 +82,9 @@ namespace osu.Game.Rulesets.Edit
}
}
+ // When not selected, input is only required for the blueprint itself to receive IsHovering
+ protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
+
///
/// Selects this , causing it to become visible.
///
@@ -107,47 +97,13 @@ namespace osu.Game.Rulesets.Edit
public bool IsSelected => State == SelectionState.Selected;
+ ///
+ /// Updates the , invoking and re-processing the beatmap.
+ ///
+ protected void UpdateHitObject() => composer?.UpdateHitObject(DrawableObject.HitObject);
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
- private bool selectionRequested;
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- selectionRequested = false;
-
- if (State == SelectionState.NotSelected)
- {
- SelectionRequested?.Invoke(this, e.CurrentState);
- selectionRequested = true;
- }
-
- return IsSelected;
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- if (State == SelectionState.Selected && !selectionRequested)
- {
- selectionRequested = true;
- SelectionRequested?.Invoke(this, e.CurrentState);
- return true;
- }
-
- return base.OnClick(e);
- }
-
- protected override bool OnDragStart(DragStartEvent e)
- {
- ScreenSpaceMovementStartPosition = DrawableObject.ToScreenSpace(DrawableObject.OriginPosition);
- return true;
- }
-
- protected override bool OnDrag(DragEvent e)
- {
- DragRequested?.Invoke(this, e);
- return true;
- }
-
///
/// The screen-space point that causes this to be selected.
///
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index d5b3df27df..e005eea831 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -239,6 +239,12 @@ namespace osu.Game.Rulesets.UI
continueResume();
}
+ public override void CancelResume()
+ {
+ // called if the user pauses while the resume overlay is open
+ ResumeOverlay?.Hide();
+ }
+
///
/// Creates and adds the visual representation of a to this .
///
@@ -453,6 +459,11 @@ namespace osu.Game.Rulesets.UI
/// The action to run when resuming is to be completed.
public abstract void RequestResume(Action continueResume);
+ ///
+ /// Invoked when the user requests to pause while the resume overlay is active.
+ ///
+ public abstract void CancelResume();
+
///
/// Create a for the associated ruleset and link with this
/// .
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index e00597dd56..857929ff9e 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -13,12 +13,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
public class ScrollingHitObjectContainer : HitObjectContainer
{
- ///
- /// A multiplier applied to the length of the scrolling area to determine a safe default lifetime end for hitobjects.
- /// This is only used to limit the lifetime end within reason, as proper lifetime management should be implemented on hitobjects themselves.
- ///
- private const float safe_lifetime_end_multiplier = 2;
-
private readonly IBindable timeRange = new BindableDouble();
private readonly IBindable direction = new Bindable();
@@ -123,28 +117,22 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (cached.IsValid)
return;
- double endTime = hitObject.HitObject.StartTime;
-
if (hitObject.HitObject is IHasEndTime e)
{
- endTime = e.EndTime;
-
switch (direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
- hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength);
+ hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break;
case ScrollingDirection.Left:
case ScrollingDirection.Right:
- hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength);
+ hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
break;
}
}
- hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
-
foreach (var obj in hitObject.NestedHitObjects)
{
computeInitialStateRecursive(obj);
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 4001a0f33a..8fa022f129 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -10,7 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Events;
-using osu.Framework.Input.States;
+using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
@@ -23,12 +24,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public event Action> SelectionChanged;
+ private DragBox dragBox;
private SelectionBlueprintContainer selectionBlueprints;
private Container placementBlueprintContainer;
private PlacementBlueprint currentPlacement;
private SelectionHandler selectionHandler;
private InputManager inputManager;
+ [Resolved]
+ private IAdjustableClock adjustableClock { get; set; }
+
[Resolved]
private HitObjectComposer composer { get; set; }
@@ -46,12 +51,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectionHandler = composer.CreateSelectionHandler();
selectionHandler.DeselectAll = deselectAll;
- var dragBox = new DragBox(select);
- dragBox.DragEnd += () => selectionHandler.UpdateVisibility();
-
InternalChildren = new[]
{
- dragBox,
+ dragBox = new DragBox(select),
selectionHandler,
selectionBlueprints = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both },
placementBlueprintContainer = new Container { RelativeSizeAxes = Axes.Both },
@@ -91,6 +93,97 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ beginClickSelection(e);
+ return true;
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ // Deselection should only occur if no selected blueprints are hovered
+ // A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
+ if (endClickSelection() || selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
+ return true;
+
+ deselectAll();
+ return true;
+ }
+
+ protected override bool OnDoubleClick(DoubleClickEvent e)
+ {
+ SelectionBlueprint clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
+
+ if (clickedBlueprint == null)
+ return false;
+
+ adjustableClock?.Seek(clickedBlueprint.DrawableObject.HitObject.StartTime);
+ return true;
+ }
+
+ protected override bool OnMouseUp(MouseUpEvent e)
+ {
+ // Special case for when a drag happened instead of a click
+ Schedule(() => endClickSelection());
+ return true;
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ if (currentPlacement != null)
+ {
+ updatePlacementPosition(e.ScreenSpaceMousePosition);
+ return true;
+ }
+
+ return base.OnMouseMove(e);
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (!beginSelectionMovement())
+ {
+ dragBox.UpdateDrag(e);
+ dragBox.FadeIn(250, Easing.OutQuint);
+ }
+
+ return true;
+ }
+
+ protected override bool OnDrag(DragEvent e)
+ {
+ if (!moveCurrentSelection(e))
+ dragBox.UpdateDrag(e);
+
+ return true;
+ }
+
+ protected override bool OnDragEnd(DragEndEvent e)
+ {
+ if (!finishSelectionMovement())
+ {
+ dragBox.FadeOut(250, Easing.OutQuint);
+ selectionHandler.UpdateVisibility();
+ }
+
+ return true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (currentPlacement != null)
+ {
+ if (composer.CursorInPlacementArea)
+ currentPlacement.State = PlacementState.Shown;
+ else if (currentPlacement?.PlacementBegun == false)
+ currentPlacement.State = PlacementState.Hidden;
+ }
+ }
+
+ #region Blueprint Addition/Removal
+
private void addBlueprintFor(HitObject hitObject)
{
var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject);
@@ -110,8 +203,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected -= onBlueprintSelected;
blueprint.Deselected -= onBlueprintDeselected;
- blueprint.SelectionRequested -= onSelectionRequested;
- blueprint.DragRequested -= onDragRequested;
selectionBlueprints.Remove(blueprint);
}
@@ -126,43 +217,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected;
- blueprint.SelectionRequested += onSelectionRequested;
- blueprint.DragRequested += onDragRequested;
selectionBlueprints.Add(blueprint);
}
- private void removeBlueprintFor(DrawableHitObject hitObject) => removeBlueprintFor(hitObject.HitObject);
+ #endregion
- protected override bool OnClick(ClickEvent e)
- {
- deselectAll();
- return true;
- }
-
- protected override bool OnMouseMove(MouseMoveEvent e)
- {
- if (currentPlacement != null)
- {
- updatePlacementPosition(e.ScreenSpaceMousePosition);
- return true;
- }
-
- return base.OnMouseMove(e);
- }
-
- protected override void Update()
- {
- base.Update();
-
- if (currentPlacement != null)
- {
- if (composer.CursorInPlacementArea)
- currentPlacement.State = PlacementState.Shown;
- else if (currentPlacement?.PlacementBegun == false)
- currentPlacement.State = PlacementState.Hidden;
- }
- }
+ #region Placement
///
/// Refreshes the current placement tool.
@@ -185,12 +246,59 @@ 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);
}
+ #endregion
+
+ #region Selection
+
+ ///
+ /// Whether a blueprint was selected by a previous click event.
+ ///
+ private bool clickSelectionBegan;
+
+ ///
+ /// Attempts to select any hovered blueprints.
+ ///
+ /// The input event that triggered this selection.
+ private void beginClickSelection(UIEvent e)
+ {
+ Debug.Assert(!clickSelectionBegan);
+
+ // If a select blueprint is already hovered, disallow changes in selection.
+ // Exception is made when holding control, as deselection should still be allowed.
+ if (!e.CurrentState.Keyboard.ControlPressed &&
+ selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
+ return;
+
+ foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints)
+ {
+ if (blueprint.IsHovered)
+ {
+ selectionHandler.HandleSelectionRequested(blueprint, e.CurrentState);
+ clickSelectionBegan = true;
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Finishes the current blueprint selection.
+ ///
+ /// Whether a click selection was active.
+ private bool endClickSelection()
+ {
+ if (!clickSelectionBegan)
+ return false;
+
+ clickSelectionBegan = false;
+ return true;
+ }
+
///
/// Select all masks in a given rectangle selection area.
///
@@ -227,24 +335,81 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
}
- private void onSelectionRequested(SelectionBlueprint blueprint, InputState state) => selectionHandler.HandleSelectionRequested(blueprint, state);
+ #endregion
- private void onDragRequested(SelectionBlueprint blueprint, DragEvent dragEvent)
+ #region Selection Movement
+
+ private Vector2? screenSpaceMovementStartPosition;
+ private SelectionBlueprint movementBlueprint;
+
+ ///
+ /// Attempts to begin the movement of any selected blueprints.
+ ///
+ /// Whether movement began.
+ private bool beginSelectionMovement()
{
- HitObject draggedObject = blueprint.DrawableObject.HitObject;
+ Debug.Assert(movementBlueprint == null);
- Vector2 movePosition = blueprint.ScreenSpaceMovementStartPosition + dragEvent.ScreenSpaceMousePosition - dragEvent.ScreenSpaceMouseDownPosition;
- Vector2 snappedPosition = composer.GetSnappedPosition(ToLocalSpace(movePosition));
+ // Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement
+ // A special case is added for when a click selection occurred before the drag
+ if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
+ return false;
+
+ // 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.DrawableObject.HitObject.StartTime).First();
+ screenSpaceMovementStartPosition = movementBlueprint.DrawableObject.ToScreenSpace(movementBlueprint.DrawableObject.OriginPosition);
+
+ return true;
+ }
+
+ ///
+ /// Moves the current selected blueprints.
+ ///
+ /// The defining the movement event.
+ /// Whether a movement was active.
+ private bool moveCurrentSelection(DragEvent e)
+ {
+ if (movementBlueprint == null)
+ return false;
+
+ Debug.Assert(screenSpaceMovementStartPosition != null);
+
+ Vector2 startPosition = screenSpaceMovementStartPosition.Value;
+ HitObject draggedObject = movementBlueprint.DrawableObject.HitObject;
+
+ // The final movement position, relative to screenSpaceMovementStartPosition
+ Vector2 movePosition = startPosition + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
+ (Vector2 snappedPosition, double snappedTime) = composer.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects
- selectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, blueprint.ScreenSpaceMovementStartPosition, ToScreenSpace(snappedPosition)));
+ if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition))))
+ return true;
// Apply the start time at the newly snapped-to position
- double offset = composer.GetSnappedTime(draggedObject.StartTime, snappedPosition) - draggedObject.StartTime;
+ double offset = snappedTime - draggedObject.StartTime;
foreach (HitObject obj in selectionHandler.SelectedHitObjects)
obj.StartTime += offset;
+
+ return true;
}
+ ///
+ /// Finishes the current movement of selected blueprints.
+ ///
+ /// Whether a movement was active.
+ private bool finishSelectionMovement()
+ {
+ if (movementBlueprint == null)
+ return false;
+
+ screenSpaceMovementStartPosition = null;
+ movementBlueprint = null;
+
+ return true;
+ }
+
+ #endregion
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -258,6 +423,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private class SelectionBlueprintContainer : Container
{
+ public IEnumerable AliveBlueprints => AliveInternalChildren.Cast();
+
protected override int Compare(Drawable x, Drawable y)
{
if (!(x is SelectionBlueprint xBlueprint) || !(y is SelectionBlueprint yBlueprint))
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index a644e51c13..0f2bae6305 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{
- protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
- : base(hitObject, centrePosition)
+ protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition)
+ : base(hitObject, nextHitObject, centrePosition)
{
}
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
float maxDistance = new Vector2(dx, dy).Length;
- int requiredCircles = (int)(maxDistance / DistanceSpacing);
+ int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing));
for (int i = 0; i < requiredCircles; i++)
{
@@ -63,20 +63,24 @@ 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;
+ if (MaxIntervals == 0)
+ return (CentrePosition, StartTime);
+ Vector2 direction = position - CentrePosition;
if (direction == Vector2.Zero)
direction = new Vector2(0.001f, 0.001f);
float distance = direction.Length;
float radius = DistanceSpacing;
- int radialCount = Math.Max(1, (int)Math.Round(distance / radius));
+ int radialCount = MathHelper.Clamp((int)Math.Round(distance / radius), 1, MaxIntervals);
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
- 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..475b6e7274 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Allocation;
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 +20,21 @@ 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 maximum number of distance snapping intervals allowed.
+ ///
+ protected int MaxIntervals { get; private set; }
+
///
/// The position which the grid is centred on.
/// The first beat snapping tick is located at + in the desired direction.
@@ -39,6 +44,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; }
@@ -47,14 +55,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly Cached gridCache = new Cached();
private readonly HitObject hitObject;
+ private readonly HitObject nextHitObject;
- private double startTime;
- private double beatLength;
-
- protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
+ protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition)
{
this.hitObject = hitObject;
- this.CentrePosition = centrePosition;
+ this.nextHitObject = nextHitObject;
+
+ CentrePosition = centrePosition;
RelativeSizeAxes = Axes.Both;
}
@@ -62,8 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[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 +82,17 @@ 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);
+
+ if (nextHitObject == null)
+ MaxIntervals = int.MaxValue;
+ else
+ {
+ // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
+ double maxDuration = nextHitObject.StartTime - StartTime + 1;
+ MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
+ }
+
gridCache.Invalidate();
}
@@ -105,28 +121,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.
diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
index 143615148a..2a510e74fd 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
@@ -19,11 +19,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
private readonly Action performSelection;
- ///
- /// Invoked when the drag selection has finished.
- ///
- public event Action DragEnd;
-
private Drawable box;
///
@@ -55,13 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
};
}
- protected override bool OnDragStart(DragStartEvent e)
- {
- this.FadeIn(250, Easing.OutQuint);
- return true;
- }
-
- protected override bool OnDrag(DragEvent e)
+ public void UpdateDrag(MouseButtonEvent e)
{
var dragPosition = e.ScreenSpaceMousePosition;
var dragStartPosition = e.ScreenSpaceMouseDownPosition;
@@ -78,14 +67,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
box.Size = bottomRight - topLeft;
performSelection?.Invoke(dragRectangle);
- return true;
- }
-
- protected override bool OnDragEnd(DragEndEvent e)
- {
- this.FadeOut(250, Easing.OutQuint);
- DragEnd?.Invoke();
- return true;
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index d7821eff07..44bf22cfe1 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -8,21 +8,21 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.States;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
-using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
///
/// A component which outlines s and handles movement of selections.
///
- public class SelectionHandler : CompositeDrawable
+ public class SelectionHandler : CompositeDrawable, IKeyBindingHandler
{
public const float BORDER_RADIUS = 2;
@@ -68,26 +68,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handles the selected s being moved.
///
/// The move event.
- public virtual void HandleMovement(MoveSelectionEvent moveEvent)
- {
- }
+ /// Whether any s were moved.
+ public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
- protected override bool OnKeyDown(KeyDownEvent e)
+ public bool OnPressed(PlatformAction action)
{
- if (e.Repeat)
- return base.OnKeyDown(e);
-
- switch (e.Key)
+ switch (action.ActionMethod)
{
- case Key.Delete:
+ case PlatformActionMethod.Delete:
foreach (var h in selectedBlueprints.ToList())
placementHandler.Delete(h.DrawableObject.HitObject);
return true;
}
- return base.OnKeyDown(e);
+ return false;
}
+ public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
+
#endregion
#region Selection Handling
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 24fb561f04..bd2db4ae2b 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit
// Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
// the next timing point's start time
- var nextTimingPoint = ControlPointInfo.TimingPoints.Find(t => t.Time > timingPoint.Time);
+ var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (position > nextTimingPoint?.Time)
position = nextTimingPoint.Time;
@@ -85,12 +85,8 @@ namespace osu.Game.Screens.Edit
var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime);
if (direction < 0 && timingPoint.Time == CurrentTime)
- {
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
- int activeIndex = ControlPointInfo.TimingPoints.IndexOf(timingPoint);
- while (activeIndex > 0 && CurrentTime == timingPoint.Time)
- timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
- }
+ timingPoint = ControlPointInfo.TimingPointAt(CurrentTime - 1);
double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount;
double seekTime = CurrentTime + seekAmount * direction;
@@ -124,7 +120,7 @@ namespace osu.Game.Screens.Edit
if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First())
seekTime = timingPoint.Time;
- var nextTimingPoint = ControlPointInfo.TimingPoints.Find(t => t.Time > timingPoint.Time);
+ var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
new file mode 100644
index 0000000000..e1182d9fa4
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class ControlPointSettings : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray3,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = createSections()
+ },
+ }
+ };
+ }
+
+ private IReadOnlyList createSections() => new Drawable[]
+ {
+ new TimingSection(),
+ new DifficultySection(),
+ new SampleSection(),
+ new EffectSection(),
+ };
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
new file mode 100644
index 0000000000..96e3ab48f2
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
@@ -0,0 +1,247 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class ControlPointTable : TableContainer
+ {
+ private const float horizontal_inset = 20;
+ private const float row_height = 25;
+ private const int text_size = 14;
+
+ private readonly FillFlowContainer backgroundFlow;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ public ControlPointTable()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Padding = new MarginPadding { Horizontal = horizontal_inset };
+ RowSize = new Dimension(GridSizeMode.Absolute, row_height);
+
+ AddInternal(backgroundFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 1f,
+ Padding = new MarginPadding { Horizontal = -horizontal_inset },
+ Margin = new MarginPadding { Top = row_height }
+ });
+ }
+
+ public IEnumerable ControlGroups
+ {
+ set
+ {
+ Content = null;
+ backgroundFlow.Clear();
+
+ if (value?.Any() != true)
+ return;
+
+ foreach (var group in value)
+ {
+ backgroundFlow.Add(new RowBackground(group));
+ }
+
+ Columns = createHeaders();
+ Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
+ }
+ }
+
+ private TableColumn[] createHeaders()
+ {
+ var columns = new List
+ {
+ new TableColumn(string.Empty, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
+ new TableColumn("Time", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
+ new TableColumn("Attributes", Anchor.Centre),
+ };
+
+ return columns.ToArray();
+ }
+
+ private Drawable[] createContent(int index, ControlPointGroup group) => new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = $"#{index + 1}",
+ Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
+ Margin = new MarginPadding(10)
+ },
+ new OsuSpriteText
+ {
+ Text = $"{group.Time:n0}ms",
+ Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
+ },
+ new ControlGroupAttributes(group),
+ };
+
+ private class ControlGroupAttributes : CompositeDrawable
+ {
+ private readonly IBindableList controlPoints;
+
+ private readonly FillFlowContainer fill;
+
+ public ControlGroupAttributes(ControlPointGroup group)
+ {
+ InternalChild = fill = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(2)
+ };
+
+ controlPoints = group.ControlPoints.GetBoundCopy();
+ controlPoints.ItemsAdded += _ => createChildren();
+ controlPoints.ItemsRemoved += _ => createChildren();
+
+ createChildren();
+ }
+
+ private void createChildren()
+ {
+ fill.ChildrenEnumerable = controlPoints.Select(createAttribute).Where(c => c != null);
+ }
+
+ private Drawable createAttribute(ControlPoint controlPoint)
+ {
+ switch (controlPoint)
+ {
+ case TimingControlPoint timing:
+ return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
+
+ case DifficultyControlPoint difficulty:
+
+ return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
+
+ case EffectControlPoint effect:
+ return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
+
+ case SampleControlPoint sample:
+ return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
+ }
+
+ return null;
+ }
+ }
+
+ protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
+
+ private class HeaderText : OsuSpriteText
+ {
+ public HeaderText(string text)
+ {
+ Text = text.ToUpper();
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black);
+ }
+ }
+
+ public class RowBackground : OsuClickableContainer
+ {
+ private readonly ControlPointGroup controlGroup;
+ private const int fade_duration = 100;
+
+ private readonly Box hoveredBackground;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ public RowBackground(ControlPointGroup controlGroup)
+ {
+ this.controlGroup = controlGroup;
+ RelativeSizeAxes = Axes.X;
+ Height = 25;
+
+ AlwaysPresent = true;
+
+ CornerRadius = 3;
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ hoveredBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ };
+
+ Action = () => selectedGroup.Value = controlGroup;
+ }
+
+ private Color4 colourHover;
+ private Color4 colourSelected;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ hoveredBackground.Colour = colourHover = colours.BlueDarker;
+ colourSelected = colours.YellowDarker;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
+ }
+
+ private bool selected;
+
+ protected bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (value == selected)
+ return;
+
+ selected = value;
+ updateState();
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
+
+ if (selected || IsHovered)
+ hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
+ else
+ hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
new file mode 100644
index 0000000000..58a7f97e5f
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class DifficultySection : Section
+ {
+ private SettingsSlider multiplier;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new[]
+ {
+ multiplier = new SettingsSlider
+ {
+ LabelText = "Speed Multiplier",
+ Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
+ RelativeSizeAxes = Axes.X,
+ }
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
+ }
+ }
+
+ protected override DifficultyControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
+
+ return new DifficultyControlPoint
+ {
+ SpeedMultiplier = reference.SpeedMultiplier,
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
new file mode 100644
index 0000000000..71e7f42713
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class EffectSection : Section
+ {
+ private LabelledSwitchButton kiai;
+ private LabelledSwitchButton omitBarLine;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new[]
+ {
+ kiai = new LabelledSwitchButton { Label = "Kiai Time" },
+ omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ kiai.Current = point.NewValue.KiaiModeBindable;
+ omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
+ }
+ }
+
+ protected override EffectControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
+
+ return new EffectControlPoint
+ {
+ KiaiMode = reference.KiaiMode,
+ OmitFirstBarLine = reference.OmitFirstBarLine
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs
new file mode 100644
index 0000000000..be8f693683
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs
@@ -0,0 +1,60 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public class RowAttribute : CompositeDrawable, IHasTooltip
+ {
+ private readonly string header;
+ private readonly Func content;
+
+ public RowAttribute(string header, Func content)
+ {
+ this.header = header;
+ this.content = content;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AutoSizeAxes = Axes.X;
+
+ Height = 20;
+
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+
+ Masking = true;
+ CornerRadius = 5;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Yellow,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Padding = new MarginPadding(2),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
+ Text = header,
+ Colour = colours.Gray3
+ },
+ };
+ }
+
+ public string TooltipText => content();
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs
new file mode 100644
index 0000000000..4665c77991
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class SampleSection : Section
+ {
+ private LabelledTextBox bank;
+ private SettingsSlider volume;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new Drawable[]
+ {
+ bank = new LabelledTextBox
+ {
+ Label = "Bank Name",
+ },
+ volume = new SettingsSlider
+ {
+ Bindable = new SampleControlPoint().SampleVolumeBindable,
+ LabelText = "Volume",
+ }
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ bank.Current = point.NewValue.SampleBankBindable;
+ volume.Bindable = point.NewValue.SampleVolumeBindable;
+ }
+ }
+
+ protected override SampleControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
+
+ return new SampleControlPoint
+ {
+ SampleBank = reference.SampleBank,
+ SampleVolume = reference.SampleVolume,
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs
new file mode 100644
index 0000000000..ccf1582486
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/Section.cs
@@ -0,0 +1,130 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal abstract class Section : CompositeDrawable
+ where T : ControlPoint
+ {
+ private OsuCheckbox checkbox;
+ private Container content;
+
+ protected FillFlowContainer Flow { get; private set; }
+
+ protected Bindable ControlPoint { get; } = new Bindable();
+
+ private const float header_height = 20;
+
+ [Resolved]
+ protected IBindable Beatmap { get; private set; }
+
+ [Resolved]
+ protected Bindable SelectedGroup { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeDuration = 200;
+ AutoSizeEasing = Easing.OutQuint;
+ AutoSizeAxes = Axes.Y;
+
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray1,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = header_height,
+ Children = new Drawable[]
+ {
+ checkbox = new OsuCheckbox
+ {
+ LabelText = typeof(T).Name.Replace(typeof(ControlPoint).Name, string.Empty)
+ }
+ }
+ },
+ content = new Container
+ {
+ Y = header_height,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray2,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Flow = new FillFlowContainer
+ {
+ Padding = new MarginPadding(10),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ },
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ checkbox.Current.BindValueChanged(selected =>
+ {
+ if (selected.NewValue)
+ {
+ if (SelectedGroup.Value == null)
+ {
+ checkbox.Current.Value = false;
+ return;
+ }
+
+ if (ControlPoint.Value == null)
+ SelectedGroup.Value.Add(ControlPoint.Value = CreatePoint());
+ }
+ else
+ {
+ if (ControlPoint.Value != null)
+ {
+ SelectedGroup.Value.Remove(ControlPoint.Value);
+ ControlPoint.Value = null;
+ }
+ }
+
+ content.BypassAutoSizeAxes = selected.NewValue ? Axes.None : Axes.Y;
+ }, true);
+
+ SelectedGroup.BindValueChanged(points =>
+ {
+ ControlPoint.Value = points.NewValue?.ControlPoints.OfType().FirstOrDefault();
+ checkbox.Current.Value = ControlPoint.Value != null;
+ }, true);
+
+ ControlPoint.BindValueChanged(OnControlPointChanged, true);
+ }
+
+ protected abstract void OnControlPointChanged(ValueChangedEvent point);
+
+ protected abstract T CreatePoint();
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index 9ded4207e5..d9da3ff92d 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -1,13 +1,151 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
namespace osu.Game.Screens.Edit.Timing
{
- public class TimingScreen : EditorScreen
+ public class TimingScreen : EditorScreenWithTimeline
{
- public TimingScreen()
+ [Cached]
+ private Bindable selectedGroup = new Bindable();
+
+ [Resolved]
+ private IAdjustableClock clock { get; set; }
+
+ protected override Drawable CreateMainContent() => new GridContainer
{
- Child = new ScreenWhiteBox.UnderConstructionMessage("Timing mode");
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 200),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new ControlPointList(),
+ new ControlPointSettings(),
+ },
+ }
+ };
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected =>
+ {
+ if (selected.NewValue != null)
+ clock.Seek(selected.NewValue.Time);
+ });
+ }
+
+ public class ControlPointList : CompositeDrawable
+ {
+ private OsuButton deleteButton;
+ private ControlPointTable table;
+
+ private IBindableList controlGroups;
+
+ [Resolved]
+ private IFrameBasedClock clock { get; set; }
+
+ [Resolved]
+ protected IBindable Beatmap { get; private set; }
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = table = new ControlPointTable(),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding(10),
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ deleteButton = new OsuButton
+ {
+ Text = "-",
+ Size = new Vector2(30, 30),
+ Action = delete,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ new OsuButton
+ {
+ Text = "+",
+ Action = addNew,
+ Size = new Vector2(30, 30),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
+
+ controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
+ controlGroups.ItemsAdded += _ => createContent();
+ controlGroups.ItemsRemoved += _ => createContent();
+ createContent();
+ }
+
+ private void createContent() => table.ControlGroups = controlGroups;
+
+ private void delete()
+ {
+ if (selectedGroup.Value == null)
+ return;
+
+ Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
+
+ selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
+ }
+
+ private void addNew()
+ {
+ selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs
new file mode 100644
index 0000000000..906644ce14
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class TimingSection : Section
+ {
+ private SettingsSlider bpm;
+ private SettingsEnumDropdown timeSignature;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Flow.AddRange(new Drawable[]
+ {
+ bpm = new BPMSlider
+ {
+ Bindable = new TimingControlPoint().BeatLengthBindable,
+ LabelText = "BPM",
+ },
+ timeSignature = new SettingsEnumDropdown
+ {
+ LabelText = "Time Signature"
+ },
+ });
+ }
+
+ protected override void OnControlPointChanged(ValueChangedEvent point)
+ {
+ if (point.NewValue != null)
+ {
+ bpm.Bindable = point.NewValue.BeatLengthBindable;
+ timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
+ }
+ }
+
+ protected override TimingControlPoint CreatePoint()
+ {
+ var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time);
+
+ return new TimingControlPoint
+ {
+ BeatLength = reference.BeatLength,
+ TimeSignature = reference.TimeSignature
+ };
+ }
+
+ private class BPMSlider : SettingsSlider
+ {
+ private readonly BindableDouble beatLengthBindable = new BindableDouble();
+
+ private BindableDouble bpmBindable;
+
+ public override Bindable Bindable
+ {
+ get => base.Bindable;
+ set
+ {
+ // incoming will be beatlength
+
+ beatLengthBindable.UnbindBindings();
+ beatLengthBindable.BindTo(value);
+
+ base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
+ {
+ MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
+ MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
+ Default = beatLengthToBpm(beatLengthBindable.Default),
+ };
+
+ bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
+ }
+ }
+
+ private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs
index 093d01f12d..e2dd14b18c 100644
--- a/osu.Game/Screens/Menu/IntroSequence.cs
+++ b/osu.Game/Screens/Menu/IntroSequence.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Screens.Menu
public IntroSequence()
{
RelativeSizeAxes = Axes.Both;
+ Alpha = 0;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
index e096fb33da..f79cac7649 100644
--- a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
+++ b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.Components
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
Font = OsuFont.GetFont(size: TextSize),
}
- }, null, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
+ }, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
}
}
}
diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index 90806bab6e..86d52ff791 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -167,14 +167,17 @@ namespace osu.Game.Screens.Multi
public void APIStateChanged(IAPIProvider api, APIState state)
{
if (state != APIState.Online)
- forcefullyExit();
+ Schedule(forcefullyExit);
}
private void forcefullyExit()
{
// This is temporary since we don't currently have a way to force screens to be exited
if (this.IsCurrentScreen())
- this.Exit();
+ {
+ while (this.IsCurrentScreen())
+ this.Exit();
+ }
else
{
this.MakeCurrent();
@@ -212,6 +215,8 @@ namespace osu.Game.Screens.Multi
public override bool OnExiting(IScreen next)
{
+ roomManager.PartRoom();
+
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
{
screenStack.Exit();
diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs
index 6f473aaafa..cdaba85b9e 100644
--- a/osu.Game/Screens/Multi/RoomManager.cs
+++ b/osu.Game/Screens/Multi/RoomManager.cs
@@ -87,9 +87,8 @@ namespace osu.Game.Screens.Multi
public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
{
currentJoinRoomRequest?.Cancel();
- currentJoinRoomRequest = null;
-
currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
+
currentJoinRoomRequest.Success += () =>
{
joinedRoom = room;
@@ -98,7 +97,8 @@ namespace osu.Game.Screens.Multi
currentJoinRoomRequest.Failure += exception =>
{
- Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
+ if (!(exception is OperationCanceledException))
+ Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important);
onError?.Invoke(exception.ToString());
};
@@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi
public void PartRoom()
{
+ currentJoinRoomRequest?.Cancel();
+
if (joinedRoom == null)
return;
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 6a03271b86..2f2028ff53 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
///
/// The original source (usually a 's track).
///
- private readonly IAdjustableClock sourceClock;
+ private IAdjustableClock sourceClock;
public readonly BindableBool IsPaused = new BindableBool();
@@ -153,10 +154,18 @@ namespace osu.Game.Screens.Play
IsPaused.Value = true;
}
- public void ResetLocalAdjustments()
+ ///
+ /// Changes the backing clock to avoid using the originally provided beatmap's track.
+ ///
+ public void StopUsingBeatmapClock()
{
- // In the case of replays, we may have changed the playback rate.
- UserPlaybackRate.Value = 1;
+ if (sourceClock != beatmap.Track)
+ return;
+
+ removeSourceClockAdjustments();
+
+ sourceClock = new TrackVirtual(beatmap.Track.Length);
+ adjustableClock.ChangeSource(sourceClock);
}
protected override void Update()
@@ -185,6 +194,14 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+
+ removeSourceClockAdjustments();
+ sourceClock = null;
+ }
+
+ private void removeSourceClockAdjustments()
+ {
+ sourceClock.ResetSpeedAdjustments();
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
}
}
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index b2c3952f38..d201b5d30e 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -45,8 +45,6 @@ namespace osu.Game.Screens.Play.HUD
VisualSettings = new VisualSettings { Expanded = false }
}
};
-
- Show();
}
protected override void PopIn() => this.FadeIn(fade_duration);
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 0b363eac4d..a9b0649fab 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -30,6 +30,7 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play
{
+ [Cached]
public class Player : ScreenWithBeatmapBackground
{
public override bool AllowBackButton => false; // handled by HoldForMenuButton
@@ -311,14 +312,19 @@ namespace osu.Game.Screens.Play
this.Exit();
}
+ ///
+ /// Restart gameplay via a parent .
+ /// This can be called from a child screen in order to trigger the restart process.
+ ///
public void Restart()
{
- if (!this.IsCurrentScreen()) return;
-
sampleRestart?.Play();
-
RestartRequested?.Invoke();
- performImmediateExit();
+
+ if (this.IsCurrentScreen())
+ performImmediateExit();
+ else
+ this.MakeCurrent();
}
private ScheduledDelegate completionProgressDelegate;
@@ -443,7 +449,12 @@ namespace osu.Game.Screens.Play
{
if (!canPause) return;
- IsResuming = false;
+ if (IsResuming)
+ {
+ DrawableRuleset.CancelResume();
+ IsResuming = false;
+ }
+
GameplayClockContainer.Stop();
PauseOverlay.Show();
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
@@ -525,7 +536,9 @@ namespace osu.Game.Screens.Play
return true;
}
- GameplayClockContainer.ResetLocalAdjustments();
+ // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
+ // as we are no longer the current screen, we cannot guarantee the track is still usable.
+ GameplayClockContainer.StopUsingBeatmapClock();
fadeOut();
return base.OnExiting(next);
diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs
index 6642efdf8b..3df06ebfa8 100644
--- a/osu.Game/Screens/Play/SongProgress.cs
+++ b/osu.Game/Screens/Play/SongProgress.cs
@@ -106,6 +106,8 @@ namespace osu.Game.Screens.Play
protected override void LoadComplete()
{
+ base.LoadComplete();
+
Show();
replayLoaded.ValueChanged += loaded => AllowSeeking = loaded.NewValue;
diff --git a/osu.Game/Screens/Play/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs
similarity index 96%
rename from osu.Game/Screens/Play/ReplayDownloadButton.cs
rename to osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs
index 290e00f287..73c647d6fa 100644
--- a/osu.Game/Screens/Play/ReplayDownloadButton.cs
+++ b/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs
@@ -4,12 +4,13 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Online;
-using osu.Game.Scoring;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Scoring;
+using osuTK;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Ranking.Pages
{
public class ReplayDownloadButton : DownloadTrackingComposite
{
@@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
public ReplayDownloadButton(ScoreInfo score)
: base(score)
{
+ Size = new Vector2(50, 30);
}
[BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Screens/Ranking/Pages/RetryButton.cs b/osu.Game/Screens/Ranking/Pages/RetryButton.cs
new file mode 100644
index 0000000000..2a281224c1
--- /dev/null
+++ b/osu.Game/Screens/Ranking/Pages/RetryButton.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Play;
+using osuTK;
+
+namespace osu.Game.Screens.Ranking.Pages
+{
+ public class RetryButton : OsuAnimatedButton
+ {
+ private readonly Box background;
+
+ [Resolved(canBeNull: true)]
+ private Player player { get; set; }
+
+ public RetryButton()
+ {
+ Size = new Vector2(50, 30);
+
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue
+ },
+ new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(13),
+ Icon = FontAwesome.Solid.ArrowCircleLeft,
+ },
+ };
+
+ TooltipText = "Retry";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ background.Colour = colours.Green;
+
+ if (player != null)
+ Action = () => player.Restart();
+ }
+ }
+}
diff --git a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs b/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs
index 7c35742ff6..27cea99f1c 100644
--- a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs
+++ b/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs
@@ -169,12 +169,19 @@ namespace osu.Game.Screens.Ranking.Pages
},
},
},
- new ReplayDownloadButton(score)
+ new FillFlowContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 10 },
- Size = new Vector2(50, 30),
+ Spacing = new Vector2(5),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new ReplayDownloadButton(score),
+ new RetryButton()
+ }
},
};
@@ -253,9 +260,7 @@ namespace osu.Game.Screens.Ranking.Pages
{
this.date = date;
- AutoSizeAxes = Axes.Y;
-
- Width = 140;
+ AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
@@ -271,22 +276,26 @@ namespace osu.Game.Screens.Ranking.Pages
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray6,
},
- new OsuSpriteText
+ new FillFlowContainer
{
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- Text = date.ToShortDateString(),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Horizontal = 10, Vertical = 5 },
- Colour = Color4.White,
+ Spacing = new Vector2(10),
+ Children = new[]
+ {
+ new OsuSpriteText
+ {
+ Text = date.ToShortDateString(),
+ Colour = Color4.White,
+ },
+ new OsuSpriteText
+ {
+ Text = date.ToShortTimeString(),
+ Colour = Color4.White,
+ }
+ }
},
- new OsuSpriteText
- {
- Origin = Anchor.CentreRight,
- Anchor = Anchor.CentreRight,
- Text = date.ToShortTimeString(),
- Padding = new MarginPadding { Horizontal = 10, Vertical = 5 },
- Colour = Color4.White,
- }
};
}
}
diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs
index cac26b3dbf..d063988b3f 100644
--- a/osu.Game/Screens/Ranking/Results.cs
+++ b/osu.Game/Screens/Ranking/Results.cs
@@ -19,6 +19,7 @@ using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
namespace osu.Game.Screens.Ranking
{
@@ -34,6 +35,9 @@ namespace osu.Game.Screens.Ranking
private ResultModeTabControl modeChangeButtons;
+ [Resolved(canBeNull: true)]
+ private Player player { get; set; }
+
public override bool DisallowExternalBeatmapRulesetChanges => true;
protected readonly ScoreInfo Score;
@@ -100,10 +104,7 @@ namespace osu.Game.Screens.Ranking
public override bool OnExiting(IScreen next)
{
- allCircles.ForEach(c =>
- {
- c.ScaleTo(0, transition_time, Easing.OutSine);
- });
+ allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine));
Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint);
@@ -115,147 +116,157 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- InternalChildren = new Drawable[]
+ InternalChild = new AspectContainer
{
- new AspectContainer
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Height = overscan,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Y,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Height = overscan,
- Children = new Drawable[]
+ circleOuterBackground = new CircularContainer
{
- circleOuterBackground = new CircularContainer
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
+ new Box
{
- new Box
- {
- Alpha = 0.2f,
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- }
+ Alpha = 0.2f,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
}
- },
- circleOuter = new CircularContainer
+ }
+ },
+ circleOuter = new CircularContainer
+ {
+ Size = new Vector2(circle_outer_scale),
+ EdgeEffect = new EdgeEffectParameters
{
- Size = new Vector2(circle_outer_scale),
- EdgeEffect = new EdgeEffectParameters
+ Colour = Color4.Black.Opacity(0.4f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 15,
+ },
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
{
- Colour = Color4.Black.Opacity(0.4f),
- Type = EdgeEffectType.Shadow,
- Radius = 15,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
},
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
+ backgroundParallax = new ParallaxContainer
{
- new Box
+ RelativeSizeAxes = Axes.Both,
+ ParallaxAmount = 0.01f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
- backgroundParallax = new ParallaxContainer
- {
- RelativeSizeAxes = Axes.Both,
- ParallaxAmount = 0.01f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
+ new Sprite
{
- new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.2f,
- Texture = Beatmap.Value.Background,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- FillMode = FillMode.Fill
- }
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.2f,
+ Texture = Beatmap.Value.Background,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ FillMode = FillMode.Fill
}
- },
- modeChangeButtons = new ResultModeTabControl
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- RelativeSizeAxes = Axes.X,
- Height = 50,
- Margin = new MarginPadding { Bottom = 110 },
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.BottomCentre,
- Text = $"{Score.MaxCombo}x",
- RelativePositionAxes = Axes.X,
- Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
- X = 0.1f,
- Colour = colours.BlueDarker,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopCentre,
- Text = "max combo",
- Font = OsuFont.GetFont(size: 20),
- RelativePositionAxes = Axes.X,
- X = 0.1f,
- Colour = colours.Gray6,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.BottomCentre,
- Text = $"{Score.Accuracy:P2}",
- Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
- RelativePositionAxes = Axes.X,
- X = 0.9f,
- Colour = colours.BlueDarker,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopCentre,
- Text = "accuracy",
- Font = OsuFont.GetFont(size: 20),
- RelativePositionAxes = Axes.X,
- X = 0.9f,
- Colour = colours.Gray6,
- },
- }
- },
- circleInner = new CircularContainer
- {
- Size = new Vector2(0.6f),
- EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.Black.Opacity(0.4f),
- Type = EdgeEffectType.Shadow,
- Radius = 15,
+ }
},
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Children = new Drawable[]
+ modeChangeButtons = new ResultModeTabControl
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
- }
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ Margin = new MarginPadding { Bottom = 110 },
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.BottomCentre,
+ Text = $"{Score.MaxCombo}x",
+ RelativePositionAxes = Axes.X,
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
+ X = 0.1f,
+ Colour = colours.BlueDarker,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.TopCentre,
+ Text = "max combo",
+ Font = OsuFont.GetFont(size: 20),
+ RelativePositionAxes = Axes.X,
+ X = 0.1f,
+ Colour = colours.Gray6,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.BottomCentre,
+ Text = $"{Score.Accuracy:P2}",
+ Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40),
+ RelativePositionAxes = Axes.X,
+ X = 0.9f,
+ Colour = colours.BlueDarker,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.TopCentre,
+ Text = "accuracy",
+ Font = OsuFont.GetFont(size: 20),
+ RelativePositionAxes = Axes.X,
+ X = 0.9f,
+ Colour = colours.Gray6,
+ },
+ }
+ },
+ circleInner = new CircularContainer
+ {
+ Size = new Vector2(0.6f),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.4f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 15,
+ },
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ },
}
}
}
};
+ if (player != null)
+ {
+ AddInternal(new HotkeyRetryOverlay
+ {
+ Action = () =>
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ player?.Restart();
+ },
+ });
+ }
+
var pages = CreateResultPages();
foreach (var p in pages)
diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs
index 09b728abeb..aa48d1a04e 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
namespace osu.Game.Screens.Select.Carousel
{
@@ -81,12 +82,10 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Filter(criteria);
- var children = new List(InternalChildren);
-
- children.ForEach(c => c.Filter(criteria));
- children.Sort((x, y) => x.CompareTo(criteria, y));
-
- InternalChildren = children;
+ InternalChildren.ForEach(c => c.Filter(criteria));
+ // IEnumerable.OrderBy() is used instead of List.Sort() to ensure sorting stability
+ var criteriaComparer = Comparer.Create((x, y) => x.CompareTo(criteria, y));
+ InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList();
}
protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value)
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 337d46ecdd..3ef1fe5bc5 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return req;
}
- protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index)
+ protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope)
{
Action = () => ScoreSelected?.Invoke(model)
};
diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
index da8f676cd0..a787eb5629 100644
--- a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
+++ b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (newScore == null)
return;
- LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position)
+ LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position, false)
{
Action = () => ScoreSelected?.Invoke(newScore.Score)
}, drawableScore =>
diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs
index f53c12b047..3233ee160d 100644
--- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs
+++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit;
namespace osu.Game.Tests.Visual
{
- public abstract class SelectionBlueprintTestScene : OsuTestScene
+ public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene
{
protected override Container Content => content ?? base.Content;
private readonly Container content;
diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs
index ee3cf6331b..ee9af15863 100644
--- a/osu.Game/Users/Drawables/DrawableAvatar.cs
+++ b/osu.Game/Users/Drawables/DrawableAvatar.cs
@@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
namespace osu.Game.Users.Drawables
{
+ [LongRunningLoad]
public class DrawableAvatar : Container
{
///
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index e898a001de..dbe6559c40 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 656c60543e..4010ef1d74 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -118,8 +118,8 @@
-
-
+
+
diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs
index 9ef21e014c..164a182ebe 100644
--- a/osu.iOS/AppDelegate.cs
+++ b/osu.iOS/AppDelegate.cs
@@ -4,7 +4,7 @@
using System.Threading.Tasks;
using Foundation;
using osu.Framework.iOS;
-using osu.Game;
+using osu.Framework.Threading;
using UIKit;
namespace osu.iOS
@@ -16,9 +16,12 @@ namespace osu.iOS
protected override Framework.Game CreateGame() => game = new OsuGameIOS();
- public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
+ public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
- Task.Run(() => game.Import(url.Path));
+ if (url.IsFileUrl)
+ Task.Run(() => game.Import(url.Path));
+ else
+ Task.Run(() => game.HandleLink(url.AbsoluteString));
return true;
}
}
diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist
index a118b329aa..5ceccdf99f 100644
--- a/osu.iOS/Info.plist
+++ b/osu.iOS/Info.plist
@@ -14,6 +14,8 @@
0.1.0
LSRequiresIPhoneOS
+ LSSupportsOpeningDocumentsInPlace
+
MinimumOSVersion
10.0
UIDeviceFamily
@@ -32,9 +34,9 @@
UIStatusBarHidden
NSCameraUsageDescription
- We don't really use the camera.
- NSMicrophoneUsageDescription
- We don't really use the microphone.
+ We don't really use the camera.
+ NSMicrophoneUsageDescription
+ We don't really use the microphone.
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
@@ -109,5 +111,17 @@
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ osu
+ osump
+
+ CFBundleTypeRole
+ Editor
+
+