From 4e0155fa4bac0dd8a8316f4cf1e80b610a465e17 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 18:32:01 +0900 Subject: [PATCH 01/60] Make `JuiceStreamPath` time based instead of distance based. And remove the "slope limit" feature. TODO: for a juice stream with a large slope, the slider velocity of the hit object should be changed. --- .../TestSceneJuiceStreamSelectionBlueprint.cs | 8 +- .../JuiceStreamPathTest.cs | 95 +++++------ .../Blueprints/Components/EditablePath.cs | 30 ++-- .../Components/PlacementEditablePath.cs | 10 +- .../Components/SelectionEditablePath.cs | 12 +- .../JuiceStreamPlacementBlueprint.cs | 6 +- .../JuiceStreamSelectionBlueprint.cs | 6 +- .../Objects/JuiceStreamPath.cs | 158 ++++++++---------- .../Objects/JuiceStreamPathVertex.cs | 10 +- 9 files changed, 157 insertions(+), 178 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index fb77fb1efd..123316f461 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -234,10 +234,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { var path = new JuiceStreamPath(); for (int i = 1; i < times.Length; i++) - path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]); + path.Add(times[i] - times[0], positions[i] - positions[0]); var sliderPath = new SliderPath(); - path.ConvertToSliderPath(sliderPath, 0); + path.ConvertToSliderPath(sliderPath, 0, velocity); addBlueprintStep(times[0], positions[0], sliderPath, velocity); } @@ -245,11 +245,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () => { - double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity; + double expectedTime = time - hitObject.StartTime; float expectedX = x - hitObject.OriginalX; var vertices = getVertices(); return vertices.Count == count && - Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) && + Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) && Precision.AlmostEquals(vertices[index].X, expectedX); }); diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 8fa96fb8c9..5248d5a96a 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests { case 0: { - double distance = rng.NextDouble() * scale * 2 - scale; + double time = rng.NextDouble() * scale * 2 - scale; if (integralValues) - distance = Math.Round(distance); + time = Math.Round(time); - float oldX = path.PositionAtDistance(distance); - int index = path.InsertVertex(distance); + float oldX = path.PositionAtTime(time); + int index = path.InsertVertex(time); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); - Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].Time, Is.EqualTo(time)); Assert.That(path.Vertices[index].X, Is.EqualTo(oldX)); break; } @@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests case 1: { int index = rng.Next(path.Vertices.Count); - double distance = path.Vertices[index].Distance; + double time = path.Vertices[index].Time; float newX = (float)(rng.NextDouble() * scale * 2 - scale); if (integralValues) newX = MathF.Round(newX); path.SetVertexPosition(index, newX); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); - Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); + Assert.That(path.Vertices[index].Time, Is.EqualTo(time)); Assert.That(path.Vertices[index].X, Is.EqualTo(newX)); break; } } - assertInvariants(path.Vertices, checkSlope); + assertInvariants(path.Vertices); } } @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Add(10, 5); path.Add(20, -5); - int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1); + int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1); Assert.That(removeCount, Is.EqualTo(1)); Assert.That(path.Vertices, Is.EqualTo(new[] { @@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests })); } - [Test] - public void TestRandomConvertFromSliderPath() + [TestCase(10)] + [TestCase(0.1)] + public void TestRandomConvertFromSliderPath(double velocity) { var rng = new Random(1); var path = new JuiceStreamPath(); @@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests else sliderPath.ExpectedDistance.Value = null; - path.ConvertFromSliderPath(sliderPath); - Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); - Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); - assertInvariants(path.Vertices, true); + path.ConvertFromSliderPath(sliderPath, velocity); + Assert.That(path.Vertices[0].Time, Is.EqualTo(0)); + Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3)); + assertInvariants(path.Vertices); - double[] sampleDistances = Enumerable.Range(0, 10) - .Select(_ => rng.NextDouble() * sliderPath.Distance) - .ToArray(); + double[] sampleTimes = Enumerable.Range(0, 10) + .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity) + .ToArray(); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; - Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X; + Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3)); } - path.ResampleVertices(sampleDistances); - assertInvariants(path.Vertices, true); + path.ResampleVertices(sampleTimes); + assertInvariants(path.Vertices); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; - Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); + float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X; + Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3)); } } } @@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests do { - double distance = rng.NextDouble() * 1e3; + double time = rng.NextDouble() * 1e3; float x = (float)(rng.NextDouble() * 1e3); - path.Add(distance, x); + path.Add(time, x); } while (rng.Next(5) != 0); float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); - path.ConvertToSliderPath(sliderPath, sliderStartY); - Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); - Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X)); - assertInvariants(path.Vertices, true); + double requiredVelocity = path.ComputeRequiredVelocity(); + double velocity = Math.Clamp(requiredVelocity, 1, 100); + + path.ConvertToSliderPath(sliderPath, sliderStartY, velocity); foreach (var point in sliderPath.ControlPoints) { @@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); } + Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X)); + + // The path is preserved only if required velocity is used. + if (velocity < requiredVelocity) continue; + + Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3)); + for (int i = 0; i < 10; i++) { - double distance = rng.NextDouble() * path.Distance; - float expected = path.PositionAtDistance(distance); - Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); + double time = rng.NextDouble() * path.Duration; + float expected = path.PositionAtTime(time); + Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3)); } } } @@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Add(20, 0); checkNewId(); - path.RemoveVertices((v, _) => v.Distance == 20); + path.RemoveVertices((v, _) => v.Time == 20); checkNewId(); path.ResampleVertices(new double[] { 5, 10, 15 }); @@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests path.Clear(); checkNewId(); - path.ConvertFromSliderPath(new SliderPath()); + path.ConvertFromSliderPath(new SliderPath(), 1); checkNewId(); void checkNewId() @@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private void assertInvariants(IReadOnlyList vertices, bool checkSlope) + private void assertInvariants(IReadOnlyList vertices) { Assert.That(vertices, Is.Not.Empty); for (int i = 0; i < vertices.Count; i++) { - Assert.That(double.IsFinite(vertices[i].Distance)); + Assert.That(double.IsFinite(vertices[i].Time)); Assert.That(float.IsFinite(vertices[i].X)); } for (int i = 1; i < vertices.Count; i++) { - Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); - - if (!checkSlope) continue; - - float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X); - double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance; - Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON)); + Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time)); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 1a43a10c81..3004d3644c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public int VertexCount => path.Vertices.Count; - protected readonly Func PositionToDistance; + protected readonly Func PositionToTime; protected IReadOnlyList VertexStates => vertexStates; @@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components [CanBeNull] private IBeatSnapProvider beatSnapProvider { get; set; } - protected EditablePath(Func positionToDistance) + protected EditablePath(Func positionToTime) { - PositionToDistance = positionToDistance; + PositionToTime = positionToTime; Anchor = Anchor.BottomLeft; } @@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components while (InternalChildren.Count < path.Vertices.Count) AddInternal(new VertexPiece()); - double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); + double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1); for (int i = 0; i < VertexCount; i++) { var piece = (VertexPiece)InternalChildren[i]; var vertex = path.Vertices[i]; - piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor)); + piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor)); piece.UpdateFrom(vertexStates[i]); } } @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void InitializeFromHitObject(JuiceStream hitObject) { var sliderPath = hitObject.Path; - path.ConvertFromSliderPath(sliderPath); + path.ConvertFromSliderPath(sliderPath, hitObject.Velocity); // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) @@ -92,11 +92,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY); + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); if (beatSnapProvider == null) return; - double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity; + double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; } @@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; - protected int AddVertex(double distance, float x) + protected int AddVertex(double time, float x) { - int index = path.InsertVertex(distance); + int index = path.InsertVertex(time); path.SetVertexPosition(index, x); vertexStates.Insert(index, new VertexState()); @@ -138,9 +138,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components return true; } - protected void MoveSelectedVertices(double distanceDelta, float xDelta) + protected void MoveSelectedVertices(double timeDelta, float xDelta) { - // Because the vertex list may be reordered due to distance change, the state list must be reordered as well. + // Because the vertex list may be reordered due to time change, the state list must be reordered as well. previousVertexStates.Clear(); previousVertexStates.AddRange(vertexStates); @@ -152,11 +152,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components for (int i = 1; i < vertexCount; i++) { var state = previousVertexStates[i]; - double distance = state.VertexBeforeChange.Distance; + double time = state.VertexBeforeChange.Time; if (state.IsSelected) - distance += distanceDelta; + time += timeDelta; - int newIndex = path.InsertVertex(Math.Max(0, distance)); + int newIndex = path.InsertVertex(Math.Max(0, time)); vertexStates.Insert(newIndex, state); } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs index 158872fbab..511aec5e5d 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/PlacementEditablePath.cs @@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components /// private JuiceStreamPathVertex lastVertex; - public PlacementEditablePath(Func positionToDistance) - : base(positionToDistance) + public PlacementEditablePath(Func positionToTime) + : base(positionToTime) { } public void AddNewVertex() { var endVertex = Vertices[^1]; - int index = AddVertex(endVertex.Distance, endVertex.X); + int index = AddVertex(endVertex.Time, endVertex.X); for (int i = 0; i < VertexCount; i++) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void MoveLastVertex(Vector2 screenSpacePosition) { Vector2 position = ToRelativePosition(screenSpacePosition); - double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance; + double timeDelta = PositionToTime(position.Y) - lastVertex.Time; float xDelta = position.X - lastVertex.X; - MoveSelectedVertices(distanceDelta, xDelta); + MoveSelectedVertices(timeDelta, xDelta); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 8c7314d0b6..b4c353313c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components [CanBeNull] private IEditorChangeHandler changeHandler { get; set; } - public SelectionEditablePath(Func positionToDistance) - : base(positionToDistance) + public SelectionEditablePath(Func positionToTime) + : base(positionToTime) { } public void AddVertex(Vector2 relativePosition) { - double distance = Math.Max(0, PositionToDistance(relativePosition.Y)); - int index = AddVertex(distance, relativePosition.X); + double time = Math.Max(0, PositionToTime(relativePosition.Y)); + int index = AddVertex(time, relativePosition.X); selectOnly(index); } @@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components protected override void OnDrag(DragEvent e) { Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition); - double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y); + double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y); float xDelta = mousePosition.X - dragStartPosition.X; - MoveSelectedVertices(distanceDelta, xDelta); + MoveSelectedVertices(timeDelta, xDelta); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index cff5bc2417..4a5a1d8160 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { scrollingPath = new ScrollingPath(), nestedOutlineContainer = new NestedOutlineContainer(), - editablePath = new PlacementEditablePath(positionToDistance) + editablePath = new PlacementEditablePath(positionToTime) }; } @@ -119,10 +119,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints lastEditablePathId = editablePath.PathId; } - private double positionToDistance(float relativeYPosition) + private double positionToTime(float relativeYPosition) { double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); - return (time - HitObject.StartTime) * HitObject.Velocity; + return time - HitObject.StartTime; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 890d059d19..12054a1d16 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { scrollingPath = new ScrollingPath(), nestedOutlineContainer = new NestedOutlineContainer(), - editablePath = new SelectionEditablePath(positionToDistance) + editablePath = new SelectionEditablePath(positionToTime) }; } @@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); } - private double positionToDistance(float relativeYPosition) + private double positionToTime(float relativeYPosition) { double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); - return (time - HitObject.StartTime) * HitObject.Velocity; + return time - HitObject.StartTime; } private void initializeJuiceStreamPath() diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index 7207833fe6..d8cea3945c 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects /// However, the representation is difficult to work with. /// This represents the path in a more convenient way, a polyline connecting list of s. /// - /// - /// The path can be regarded as a function from the closed interval [Vertices[0].Distance, Vertices[^1].Distance] to the x position, given by . - /// To ensure the path is convertible to a , the slope of the function must not be more than 1 everywhere, - /// and this slope condition is always maintained as an invariant. - /// /// public class JuiceStreamPath { @@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects public int InvalidationID { get; private set; } = 1; /// - /// The difference between first vertex's and last vertex's . + /// The difference between first vertex's and last vertex's . /// - public double Distance => vertices[^1].Distance - vertices[0].Distance; + public double Duration => vertices[^1].Time - vertices[0].Time; /// /// This list should always be non-empty. @@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects }; /// - /// Compute the x-position of the path at the given . + /// Compute the x-position of the path at the given . /// /// - /// When the given distance is outside of the path, the x position at the corresponding endpoint is returned, + /// When the given time is outside of the path, the x position at the corresponding endpoint is returned, /// - public float PositionAtDistance(double distance) + public float PositionAtTime(double time) { - int index = vertexIndexAtDistance(distance); - return positionAtDistance(distance, index); + int index = vertexIndexAtTime(time); + return positionAtTime(time, index); } /// @@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects } /// - /// Insert a vertex at given . - /// The is used as the position of the new vertex. + /// Insert a vertex at given . + /// The is used as the position of the new vertex. /// Thus, the set of points of the path is not changed (up to floating-point precision). /// /// The index of the new vertex. - public int InsertVertex(double distance) + public int InsertVertex(double time) { - if (!double.IsFinite(distance)) - throw new ArgumentOutOfRangeException(nameof(distance)); + if (!double.IsFinite(time)) + throw new ArgumentOutOfRangeException(nameof(time)); - int index = vertexIndexAtDistance(distance); - float x = positionAtDistance(distance, index); - vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); + int index = vertexIndexAtTime(time); + float x = positionAtTime(time, index); + vertices.Insert(index, new JuiceStreamPathVertex(time, x)); invalidate(); return index; @@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// Move the vertex of given to the given position . - /// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards . /// public void SetVertexPosition(int index, float newX) { @@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects if (!float.IsFinite(newX)) throw new ArgumentOutOfRangeException(nameof(newX)); - var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); - - for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--) - { - float clampedX = clampToConnectablePosition(newVertex, vertices[i]); - vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); - } - - for (int i = index + 1; i < vertices.Count; i++) - { - float clampedX = clampToConnectablePosition(newVertex, vertices[i]); - vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX); - } - - vertices[index] = newVertex; + vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX); invalidate(); } /// - /// Add a new vertex at given and position. - /// Adjacent vertices are moved when necessary in the same way as . + /// Add a new vertex at given and position. /// - public void Add(double distance, float x) + public void Add(double time, float x) { - int index = InsertVertex(distance); + int index = InsertVertex(time); SetVertexPosition(index, x); } @@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects } /// - /// Recreate this path by using difference set of vertices at given distances. - /// In addition to the given , the first vertex and the last vertex are always added to the new path. - /// New vertices use the positions on the original path. Thus, s at are preserved. + /// Recreate this path by using difference set of vertices at given time points. + /// In addition to the given , the first vertex and the last vertex are always added to the new path. + /// New vertices use the positions on the original path. Thus, s at are preserved. /// - public void ResampleVertices(IEnumerable sampleDistances) + public void ResampleVertices(IEnumerable sampleTimes) { var sampledVertices = new List(); - foreach (double distance in sampleDistances) + foreach (double time in sampleTimes) { - if (!double.IsFinite(distance)) - throw new ArgumentOutOfRangeException(nameof(sampleDistances)); + if (!double.IsFinite(time)) + throw new ArgumentOutOfRangeException(nameof(sampleTimes)); - double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); - float x = PositionAtDistance(clampedDistance); - sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); + double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time); + float x = PositionAtTime(clampedTime); + sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x)); } sampledVertices.Sort(); @@ -196,37 +175,57 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// Duplicated vertices are automatically removed. /// - public void ConvertFromSliderPath(SliderPath sliderPath) + public void ConvertFromSliderPath(SliderPath sliderPath, double velocity) { var sliderPathVertices = new List(); sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); - double distance = 0; + double time = 0; vertices.Clear(); vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); for (int i = 1; i < sliderPathVertices.Count; i++) { - distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]); + time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity; - if (!Precision.AlmostEquals(vertices[^1].Distance, distance)) - vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); + if (!Precision.AlmostEquals(vertices[^1].Time, time)) + Add(time, sliderPathVertices[i].X); } invalidate(); } + /// + /// Computes the minimum slider velocity required to convert this path to a . + /// + public double ComputeRequiredVelocity() + { + double maximumSlope = 0; + + for (int i = 1; i < vertices.Count; i++) + { + double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X); + double timeDifference = vertices[i].Time - vertices[i - 1].Time; + maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference); + } + + return maximumSlope; + } + /// /// Convert the path of this to a and write the result to . /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is . + /// + /// The velocity of the converted slider is assumed to be . + /// To preserve the path, should be at least the value returned by . /// - public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) + public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity) { const float margin = 1; // Note: these two variables and `sliderPath` are modified by the local functions. - double currentDistance = 0; + double currentTime = 0; Vector2 lastPosition = new Vector2(vertices[0].X, 0); sliderPath.ControlPoints.Clear(); @@ -237,10 +236,10 @@ namespace osu.Game.Rulesets.Catch.Objects sliderPath.ControlPoints[^1].Type = PathType.Linear; float deltaX = vertices[i].X - lastPosition.X; - double length = vertices[i].Distance - currentDistance; + double length = (vertices[i].Time - currentTime) * velocity; // Should satisfy `deltaX^2 + deltaY^2 = length^2`. - // By invariants, the expression inside the `sqrt` is (almost) non-negative. + // The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough. double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX)); // When `deltaY` is small, one segment is always enough. @@ -280,59 +279,38 @@ namespace osu.Game.Rulesets.Catch.Objects { Vector2 nextPosition = new Vector2(nextX, nextY); sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); - currentDistance += Vector2.Distance(lastPosition, nextPosition); + currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity; lastPosition = nextPosition; } } /// - /// Find the index at which a new vertex with can be inserted. + /// Find the index at which a new vertex with can be inserted. /// - private int vertexIndexAtDistance(double distance) + private int vertexIndexAtTime(double time) { - // The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. - int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); + // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed. + int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity)); return i < 0 ? ~i : i; } /// - /// Compute the position at the given , assuming is the vertex index returned by . + /// Compute the position at the given , assuming is the vertex index returned by . /// - private float positionAtDistance(double distance, int index) + private float positionAtTime(double time, int index) { if (index <= 0) return vertices[0].X; if (index >= vertices.Count) return vertices[^1].X; - double length = vertices[index].Distance - vertices[index - 1].Distance; - if (Precision.AlmostEquals(length, 0)) + double duration = vertices[index].Time - vertices[index - 1].Time; + if (Precision.AlmostEquals(duration, 0)) return vertices[index].X; float deltaX = vertices[index].X - vertices[index - 1].X; - return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); - } - - /// - /// Check the two vertices can connected directly while satisfying the slope condition. - /// - private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0) - { - double xDistance = Math.Abs((double)vertex2.X - vertex1.X); - float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance); - return xDistance <= length + allowance; - } - - /// - /// Move the position of towards the position of - /// until the vertex pair satisfies the condition . - /// - /// The resulting position of . - private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex) - { - float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance); - return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length); + return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration)); } private void invalidate() => InvalidationID++; diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs index 58c50603c4..afef2e637f 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs @@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects /// public readonly struct JuiceStreamPathVertex : IComparable { - public readonly double Distance; + public readonly double Time; public readonly float X; - public JuiceStreamPathVertex(double distance, float x) + public JuiceStreamPathVertex(double time, float x) { - Distance = distance; + Time = time; X = x; } public int CompareTo(JuiceStreamPathVertex other) { - int c = Distance.CompareTo(other.Distance); + int c = Time.CompareTo(other.Time); return c != 0 ? c : X.CompareTo(other.X); } - public override string ToString() => $"({Distance}, {X})"; + public override string ToString() => $"({Time}, {X})"; } } From 9ffa90602bc675ed0a37eac66845b34f36948f8f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:17:57 +0900 Subject: [PATCH 02/60] Automatically set slider velocity from juice stream path --- .../Blueprints/Components/EditablePath.cs | 22 +++++++++++++++++-- .../Objects/JuiceStreamPath.cs | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 3004d3644c..20e303dad1 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -92,13 +92,31 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateHitObjectFromPath(JuiceStream hitObject) { - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); + // The SV setting may need to be changed for the current path. + var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable; + double svToVelocityFactor = hitObject.Velocity / svBindable.Value; + double requiredVelocity = path.ComputeRequiredVelocity(); + + // The value is pre-rounded here because setting it to the bindable will rounded to the nearest value + // but it should be always rounded up to satisfy the required minimum velocity condition. + // + // This is rounded to integers instead of using the precision of the bindable + // because it results in a smaller number of non-redundant control points. + // + // The value is clamped here by the bindable min and max values. + // In case the required velocity is too large, the path is not preserved. + svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); + + // Calculate the velocity using the resulting SV because `hitObject.Velocity` is not recomputed yet. + double velocity = svBindable.Value * svToVelocityFactor; + + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, velocity); if (beatSnapProvider == null) return; double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); - hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; + hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * velocity; } public Vector2 ToRelativePosition(Vector2 screenSpacePosition) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index d8cea3945c..61f4c580ae 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -207,6 +207,11 @@ namespace osu.Game.Rulesets.Catch.Objects { double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X); double timeDifference = vertices[i].Time - vertices[i - 1].Time; + + // A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero. + if (Precision.AlmostEquals(timeDifference, 0)) + continue; + maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference); } From 0e98bb28bda2c7dcedba4d4637493e61c3c28d8e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:21:51 +0900 Subject: [PATCH 03/60] Fix wrong resampling times are used for juice stream path --- .../Edit/Blueprints/Components/EditablePath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 20e303dad1..652d083c4e 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { path.ResampleVertices(hitObject.NestedHitObjects .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. - .Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity)); + .Select(h => h.StartTime - hitObject.StartTime)); } vertexStates.Clear(); From 670922c8e563c202b6d7e396450f64ce77ecb78d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:32:05 +0900 Subject: [PATCH 04/60] Use latest slider velocity for juice stream velocity computation. This fixes one-frame glitch in editor when slider velocity is changed. --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 282afb6343..d34452cdbb 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } [JsonIgnore] - public double Velocity { get; private set; } + private double velocityFactor; [JsonIgnore] - public double TickDistance { get; private set; } + private double tickDistanceFactor; + + [JsonIgnore] + public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity; + + [JsonIgnore] + public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; /// /// The length of one span of this . @@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; - - Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = scoringDistance / difficulty.SliderTickRate; + velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; + tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From 37c9aac49f836bb49f57c0ea246998dcf6815144 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:35:06 +0900 Subject: [PATCH 05/60] Make `ScrollingPath` use time instead of distance. This is consistent as other components now use time instead of distance. --- .../Edit/Blueprints/Components/ScrollingPath.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs index 109bf61ea5..cfaca2f9a4 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { private readonly Path drawablePath; - private readonly List<(double Distance, float X)> vertices = new List<(double, float)>(); + private readonly List<(double Time, float X)> vertices = new List<(double, float)>(); public ScrollingPath() { @@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) { - double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); + double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1); - computeDistanceXs(hitObject); + computeTimeXs(hitObject); drawablePath.Vertices = vertices - .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor))) + .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor))) .ToArray(); drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero); } - private void computeDistanceXs(JuiceStream hitObject) + private void computeTimeXs(JuiceStream hitObject) { vertices.Clear(); @@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components if (sliderVertices.Count == 0) return; - double distance = 0; + double time = 0; Vector2 lastPosition = Vector2.Zero; for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++) { foreach (var position in sliderVertices) { - distance += Vector2.Distance(lastPosition, position); + time += Vector2.Distance(lastPosition, position) / hitObject.Velocity; lastPosition = position; - vertices.Add((distance, position.X)); + vertices.Add((time, position.X)); } sliderVertices.Reverse(); From 7daa3d8eb79b59756fad96810012c01e3a167dfe Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 21:54:14 +0900 Subject: [PATCH 06/60] Remove now-redundant velocity calculation Velocity is computed from the up-to-date SV now. --- .../Edit/Blueprints/Components/EditablePath.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 652d083c4e..e038562b4b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -107,16 +107,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components // In case the required velocity is too large, the path is not preserved. svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); - // Calculate the velocity using the resulting SV because `hitObject.Velocity` is not recomputed yet. - double velocity = svBindable.Value * svToVelocityFactor; - - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, velocity); + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); if (beatSnapProvider == null) return; double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); - hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * velocity; + hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; } public Vector2 ToRelativePosition(Vector2 screenSpacePosition) From d8a4f9d37ddb42a2e0a73f51e5112edab432964a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 8 May 2022 22:33:16 +0900 Subject: [PATCH 07/60] Update juice stream blueprint tests No "clipping" occur anymore for vertex positions. Instead, clipping may occur when the path is converted to a slider. Add tests for automatic slider velocity change. --- .../TestSceneJuiceStreamPlacementBlueprint.cs | 29 +++++------------ .../TestSceneJuiceStreamSelectionBlueprint.cs | 31 +++++-------------- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index 981efc9a13..b1adc4901c 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene { - private const double velocity = 0.5; + private const double velocity_factor = 0.5; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { var playable = base.GetPlayableBeatmap(); playable.Difficulty.SliderTickRate = 5; - playable.Difficulty.SliderMultiplier = velocity * 10; + playable.Difficulty.SliderMultiplier = velocity_factor * 10; return playable; } @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); + AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] @@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } [Test] - public void TestVelocityLimit() + public void TestSliderVelocityChange() { double[] times = { 100, 300 }; float[] positions = { 200, 500 }; addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 200, 300 }); - } + addPathCheckStep(times, positions); - [Test] - public void TestPreviousVerticesAreFixed() - { - double[] times = { 100, 300, 500, 700 }; - float[] positions = { 200, 400, 100, 500 }; - addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 200, 300, 200, 300 }); + AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] public void TestClampedPositionIsRestored() { double[] times = { 100, 300, 500 }; - float[] positions = { 200, 200, 0, 250 }; + float[] positions = { 200, 200, -3000, 250 }; addMoveAndClickSteps(times[0], positions[0]); addMoveAndClickSteps(times[1], positions[1]); @@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addPathCheckStep(times, new float[] { 200, 200, 250 }); } - [Test] - public void TestFirstVertexIsFixed() - { - double[] times = { 100, 200 }; - float[] positions = { 100, 300 }; - addPlacementSteps(times, positions); - addPathCheckStep(times, new float[] { 100, 150 }); - } - [Test] public void TestOutOfOrder() { diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 123316f461..22a839d847 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } [Test] - public void TestClampedPositionIsRestored() + public void TestSliderVelocityChange() { - const double velocity = 0.25; - double[] times = { 100, 500, 700 }; - float[] positions = { 100, 100, 100 }; - addBlueprintStep(times, positions, velocity); + double[] times = { 100, 300 }; + float[] positions = { 200, 300 }; + addBlueprintStep(times, positions); + AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); addDragStartStep(times[1], positions[1]); - - AddMouseMoveStep(times[1], 200); - addVertexCheckStep(3, 1, times[1], 200); - addVertexCheckStep(3, 2, times[2], 150); - - AddMouseMoveStep(times[1], 100); - addVertexCheckStep(3, 1, times[1], 100); - // Stored position is restored. - addVertexCheckStep(3, 2, times[2], positions[2]); - - AddMouseMoveStep(times[1], 300); - addDragEndStep(); - addDragStartStep(times[1], 300); - - AddMouseMoveStep(times[1], 100); - // Position is different because a changed position is committed when the previous drag is ended. - addVertexCheckStep(3, 2, times[2], 250); + AddMouseMoveStep(times[1], 400); + AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); } [Test] @@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addAddVertexSteps(500, 150); addVertexCheckStep(3, 1, 500, 150); - addAddVertexSteps(90, 220); + addAddVertexSteps(90, 200); addVertexCheckStep(4, 1, times[0], positions[0]); addAddVertexSteps(750, 180); From c0abce918fbacbd57f10a100d04cc1649d8bfb73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 May 2022 15:23:41 +0900 Subject: [PATCH 08/60] Add `enum` to snap method as alternative to mutliple nested invocations --- .../Edit/CatchHitObjectComposer.cs | 15 +++++--- .../Editor/TestSceneManiaBeatSnapGrid.cs | 7 +--- .../Edit/ManiaHitObjectComposer.cs | 4 +- .../Edit/OsuHitObjectComposer.cs | 37 ++++++++----------- .../Editing/TestSceneDistanceSnapGrid.cs | 5 +-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 23 ++++++------ .../Rulesets/Edit/IPositionSnapProvider.cs | 14 +------ osu.Game/Rulesets/Edit/SnapType.cs | 15 ++++++++ .../Compose/Components/BlueprintContainer.cs | 4 +- .../Compose/Components/Timeline/Timeline.cs | 5 +-- 10 files changed, 62 insertions(+), 67 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/SnapType.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 630a2cf645..6f59b3e543 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -6,6 +6,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; @@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition); + var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + result.ScreenSpacePosition.X = screenSpacePosition.X; - if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && - Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) + if (snapType.HasFlagFast(SnapType.Grids)) { - result = snapResult; + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && + Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) + { + result = snapResult; + } } return result; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 4bb049b1a4..6130a80bb4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor set => InternalChild = value; } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) - { - throw new System.NotImplementedException(); - } - - public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { throw new System.NotImplementedException(); } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index fef315e2ef..3c022dafc0 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -56,9 +56,9 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition); + var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); switch (ScrollingInfo.Direction.Value) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 02beb0f2a4..b0d6170190 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return snapResult; - return new SnapResult(screenSpacePosition, null); - } - - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) - { - var positionSnap = FindSnappedPosition(screenSpacePosition); - if (positionSnap.ScreenSpacePosition != screenSpacePosition) - return positionSnap; - - if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (snapType.HasFlagFast(SnapType.Grids)) { - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); - return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + { + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + } + + if (rectangularGridSnapToggle.Value == TernaryState.True) + { + Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + } } - if (rectangularGridSnapToggle.Value == TernaryState.True) - { - Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition)); - return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); - } - - return base.FindSnappedPositionAndTime(screenSpacePosition); + return base.FindSnappedPositionAndTime(screenSpacePosition, snapType); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 3aa3481cbf..ef07c3e411 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IDistanceSnapProvider { - public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => - new SnapResult(screenSpacePosition, null); - - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0); public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 216510fcf3..f8d796a778 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -361,20 +362,23 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); double? targetTime = null; - if (playfield is ScrollingPlayfield scrollingPlayfield) + if (snapType.HasFlagFast(SnapType.Grids)) { - targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + if (playfield is ScrollingPlayfield scrollingPlayfield) + { + targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); - // convert back to screen space - screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + } } return new SnapResult(screenSpacePosition, targetTime, playfield); @@ -414,10 +418,7 @@ namespace osu.Game.Rulesets.Edit #region IPositionSnapProvider - public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); - - public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => - new SnapResult(screenSpacePosition, null); + public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); #endregion } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 837b04424a..a6a6e39e23 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -8,7 +8,6 @@ namespace osu.Game.Rulesets.Edit { /// /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. - /// Provided values are inferred in an isolated context, without consideration of other nearby hit objects. /// [Cached] public interface IPositionSnapProvider @@ -16,18 +15,9 @@ namespace osu.Game.Rulesets.Edit /// /// Given a position, find a valid time and position snap. /// - /// - /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. - /// /// The screen-space position to be snapped. + /// The type of snapping to apply. /// The time and position post-snapping. - SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); - - /// - /// Given a position, find a valid position snap, without changing the time value. - /// - /// The screen-space position to be snapped. - /// The position post-snapping. Time will always be null. - SnapResult FindSnappedPosition(Vector2 screenSpacePosition); + SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); } } diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs new file mode 100644 index 0000000000..4ab67fee63 --- /dev/null +++ b/osu.Game/Rulesets/Edit/SnapType.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Edit +{ + [Flags] + public enum SnapType + { + NearbyObjects = 0, + Grids = 1, + All = NearbyObjects | Grids, + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 186c66e0af..d56dc176f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 originalPosition = movementBlueprintOriginalPositions[i]; var testPosition = originalPosition + distanceTravelled; - var positionalResult = snapProvider.FindSnappedPosition(testPosition); + var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); if (positionalResult.ScreenSpacePosition == testPosition) continue; @@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; // Retrieve a snapped position. - var result = snapProvider?.FindSnappedPositionAndTime(movePosition); + var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); if (result == null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 7e66c57917..992ab7947e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -303,10 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => track.Length / Zoom; - public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => - new SnapResult(screenSpacePosition, null); - - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => From f7e055dbfee9b68b6f93b69262e04c281a836245 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 May 2022 16:13:31 +0900 Subject: [PATCH 09/60] Move mania note height offset application to a much more suitable location --- .../Blueprints/ManiaPlacementBlueprint.cs | 25 ++++++++++++++- .../Edit/ManiaHitObjectComposer.cs | 31 +++---------------- .../UI/Scrolling/ScrollingPlayfield.cs | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 8f25668dd0..ad95fd5e87 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -5,7 +5,10 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -52,8 +55,28 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.UpdateTimeAndPosition(result); + var playfield = (Column)result.Playfield; + + // Apply an offset to better align with the visual grid. + // This should only be applied during placement, as during selection / drag operations the movement is relative + // to the initial point of interaction rather than the grid. + switch (playfield.ScrollingInfo.Direction.Value) + { + case ScrollingDirection.Down: + result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(playfield) / 2); + break; + + case ScrollingDirection.Up: + result.ScreenSpacePosition += new Vector2(0, getNoteHeight(playfield) / 2); + break; + } + if (PlacementActive == PlacementState.Waiting) - Column = result.Playfield as Column; + Column = playfield; } + + private float getNoteHeight(Column resultPlayfield) => + resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y - + resultPlayfield.ToScreenSpace(Vector2.Zero).Y; } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 3c022dafc0..c389e1bced 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -1,15 +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 osu.Game.Beatmaps; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Mania.Objects; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input; -using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); - - switch (ScrollingInfo.Direction.Value) - { - case ScrollingDirection.Down: - result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2); - break; - - case ScrollingDirection.Up: - result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2); - break; - } - - return result; - } - - private float getNoteHeight() => - Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y - - Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y; - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods); diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 2b75f93f9e..782255733f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; [Resolved] - protected IScrollingInfo ScrollingInfo { get; private set; } + public IScrollingInfo ScrollingInfo { get; private set; } [BackgroundDependencyLoader] private void load() From 9f5351e5a195d490db4b19b808263aeeac725e85 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 12 May 2022 23:48:58 +0100 Subject: [PATCH 10/60] Add drawable channel caching to new chat overlay --- osu.Game/Overlays/ChatOverlayV2.cs | 37 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 6eec0bbbf4..bd3823f090 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -39,8 +39,9 @@ namespace osu.Game.Overlays private ChatTextBar textBar = null!; private Container currentChannelContainer = null!; - private readonly BindableFloat chatHeight = new BindableFloat(); + private readonly Dictionary loadedChannels = new Dictionary(); + private readonly BindableFloat chatHeight = new BindableFloat(); private bool isDraggingTopBar; private float dragStartChatHeight; @@ -173,7 +174,7 @@ namespace osu.Game.Overlays if (currentChannel.Value?.Id != channel.Id) { if (!channel.Joined.Value) - channel = channelManager.JoinChannel(channel); + channel = channelManager.JoinChannel(channel, false); channelManager.CurrentChannel.Value = channel; } @@ -248,13 +249,26 @@ namespace osu.Game.Overlays channelListing.State.Value = Visibility.Hidden; textBar.ShowSearch.Value = false; - loading.Show(); - LoadComponentAsync(new ChatOverlayDrawableChannel(newChannel), loaded => + if (loadedChannels.ContainsKey(newChannel)) { - currentChannelContainer.Clear(); - currentChannelContainer.Add(loaded); - loading.Hide(); - }); + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loadedChannels[newChannel]); + } + else + { + loading.Show(); + + // Ensure the drawable channel is stored before async load to prevent double loading + ChatOverlayDrawableChannel drawableChannel = new ChatOverlayDrawableChannel(newChannel); + loadedChannels.Add(newChannel, drawableChannel); + + LoadComponentAsync(drawableChannel, loaded => + { + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loaded); + loading.Hide(); + }); + } } } @@ -264,14 +278,21 @@ namespace osu.Game.Overlays { case NotifyCollectionChangedAction.Add: IEnumerable joinedChannels = filterChannels(args.NewItems); + foreach (var channel in joinedChannels) channelList.AddChannel(channel); + break; case NotifyCollectionChangedAction.Remove: IEnumerable leftChannels = filterChannels(args.OldItems); + foreach (var channel in leftChannels) + { channelList.RemoveChannel(channel); + loadedChannels.Remove(channel); + } + break; } } From f88e416d1a7b6f63b0177f8bc042ec7470912d5b Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Fri, 13 May 2022 23:39:09 +0100 Subject: [PATCH 11/60] Ensure the wrong drawable channel isn't shown after load --- osu.Game/Overlays/ChatOverlayV2.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index bd3823f090..361c06b04d 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -264,6 +264,14 @@ namespace osu.Game.Overlays LoadComponentAsync(drawableChannel, loaded => { + // Ensure the current channel hasn't changed by the time the load completes + if (currentChannel.Value != newChannel) + return; + + // Ensure the cached reference hasn't been removed from leaving the channel + if (!loadedChannels.ContainsKey(newChannel)) + return; + currentChannelContainer.Clear(false); currentChannelContainer.Add(loaded); loading.Hide(); From 2163a78b7f591518a3e532ed2a4be7835a2953ea Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 14 May 2022 00:15:02 +0100 Subject: [PATCH 12/60] Ensure drawable channels removed from the cache are disposed --- osu.Game/Overlays/ChatOverlayV2.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 361c06b04d..e76d395204 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -241,6 +241,7 @@ namespace osu.Game.Overlays if (newChannel == null) { // null channel denotes that we should be showing the listing. + currentChannelContainer.Clear(false); channelListing.State.Value = Visibility.Visible; textBar.ShowSearch.Value = true; } @@ -298,7 +299,13 @@ namespace osu.Game.Overlays foreach (var channel in leftChannels) { channelList.RemoveChannel(channel); - loadedChannels.Remove(channel); + if (loadedChannels.ContainsKey(channel)) + { + ChatOverlayDrawableChannel loaded = loadedChannels[channel]; + loadedChannels.Remove(channel); + // DrawableChannel removed from cache must be manually disposed + loaded.Dispose(); + } } break; From bd68ffa805a6f3ae1d1a4a9853ebfa4f9aa1303d Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 14 May 2022 12:16:00 +0100 Subject: [PATCH 13/60] Fix textbox focus test in `ChatOverlayV2` --- osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs | 6 +++--- osu.Game/Overlays/ChatOverlayV2.cs | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index bf1767cc96..82089ff3d1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -365,19 +365,19 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TextBoxRetainsFocus() + public void TestTextBoxRetainsFocus() { AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel)); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel)); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index e76d395204..e91d57f18b 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -242,12 +242,12 @@ namespace osu.Game.Overlays { // null channel denotes that we should be showing the listing. currentChannelContainer.Clear(false); - channelListing.State.Value = Visibility.Visible; + channelListing.Show(); textBar.ShowSearch.Value = true; } else { - channelListing.State.Value = Visibility.Hidden; + channelListing.Hide(); textBar.ShowSearch.Value = false; if (loadedChannels.ContainsKey(newChannel)) @@ -299,6 +299,7 @@ namespace osu.Game.Overlays foreach (var channel in leftChannels) { channelList.RemoveChannel(channel); + if (loadedChannels.ContainsKey(channel)) { ChatOverlayDrawableChannel loaded = loadedChannels[channel]; From 92ae652555a0ca85941d87d1de040d7427cedf5f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 14 May 2022 22:07:42 +0300 Subject: [PATCH 14/60] Enable indirect input events on iOS for proper mouse support --- osu.Game.Rulesets.Catch.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Mania.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Osu.Tests.iOS/Info.plist | 2 ++ osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist | 2 ++ osu.Game.Tests.iOS/Info.plist | 2 ++ osu.iOS/Info.plist | 2 ++ 6 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 33ddac6dfb..16a2b99997 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -37,6 +37,8 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 78349334b4..82d1c8ea24 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -37,6 +37,8 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index b9f371c049..a88b74695c 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -37,6 +37,8 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index 65c47d2115..9628475b3e 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -37,6 +37,8 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index ed0c2e4dbf..31e2b3f257 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -37,6 +37,8 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 02968b87a7..16cb68fa7d 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -33,6 +33,8 @@ UIStatusBarHidden + UIApplicationSupportsIndirectInputEvents + CADisableMinimumFrameDurationOnPhone NSCameraUsageDescription From 170df01b4615b2aa73d6ec67c61b4e7b1101654b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 May 2022 20:10:53 +0200 Subject: [PATCH 15/60] Adjust difficulty multiplier scale transition on mod overlay The previous transition was supposed to be a center-anchored elastic scale-in, but this didn't work as intended - because the multiplier ended up inside of an auto-sized right-aligned container, the animation itself would end up being anchored right. Attempts to remove the scale transition resulted in a rather jarring-looking result, so swap out the elastic scale-in for a sweep-in effect from the top, to match the header and avoid introducing too many directions of movement. Delay values tweaked "to taste" - can be adjusted further if there is an alternative set of values that feels better. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 1dad185b78..8d72855217 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -428,9 +428,9 @@ namespace osu.Game.Overlays.Mods base.PopIn(); multiplierDisplay? - .Delay(fade_in_duration * 0.65f) - .FadeIn(fade_in_duration / 2, Easing.OutQuint) - .ScaleTo(1, fade_in_duration, Easing.OutElastic); + .Delay(fade_in_duration / 5) + .FadeIn(fade_in_duration, Easing.OutQuint) + .MoveToY(0, fade_in_duration, Easing.OutQuint); int nonFilteredColumnCount = 0; @@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Mods multiplierDisplay? .FadeOut(fade_out_duration / 2, Easing.OutQuint) - .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint); + .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint); int nonFilteredColumnCount = 0; From d7aed77340b2d1b9a16f9de41081af910f304a31 Mon Sep 17 00:00:00 2001 From: "Hugo \"ThePooN\" Denizart" Date: Mon, 16 May 2022 16:01:42 +0200 Subject: [PATCH 16/60] =?UTF-8?q?=F0=9F=91=B7=20Push=20tagged=20releases?= =?UTF-8?q?=20to=20Sentry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sentry-release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/sentry-release.yml diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml new file mode 100644 index 0000000000..8ca9f38234 --- /dev/null +++ b/.github/workflows/sentry-release.yml @@ -0,0 +1,26 @@ +name: Add Release to Sentry + +on: + push: + tags: + - '*' + +jobs: + sentry_release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ppy + SENTRY_PROJECT: osu + SENTRY_URL: https://sentry.ppy.sh/ + with: + environment: production + version: ${{ github.ref }} From bc294b81575b92a878c733ec9fe121cd93675ae7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 12:53:13 +0900 Subject: [PATCH 17/60] Add sentry tags for operating system and processor count These would usually be in the log header, but right now that's not conveyed. An example use case would be better understanding https://sentry.ppy.sh/organizations/ppy/issues/846/?project=2&query=is%3Aunresolved&sort=freq&statsPeriod=14d, where we currently don't know whether it is a desktop or mobile OS. --- osu.Game/Utils/SentryLogger.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 5390c666ed..137bf7e0aa 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -160,6 +161,8 @@ namespace osu.Game.Utils }; scope.SetTag(@"ruleset", ruleset.ShortName); + scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})"); + scope.SetTag(@"processor count", Environment.ProcessorCount.ToString()); }); } else From 31137bebf8d414cdbcacce62e0365bc271999e6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 14:52:27 +0900 Subject: [PATCH 18/60] Revert realm to previous release to fix iOS crashes --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b984f528fe..e867de73f3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7f149b4e35..cd3a84a504 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 932421a705..1f677e54fd 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -89,6 +89,6 @@ - + From ef5b2233d7403f671a9ac9e13c708e1d02aef52c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 19:21:19 +0900 Subject: [PATCH 19/60] Improve the first run progress button to show loading and completion status --- .../FirstRunSetup/ProgressRoundedButton.cs | 103 ++++++++++++++++++ .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 45 +------- 2 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs diff --git a/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs new file mode 100644 index 0000000000..ee2db1f3d4 --- /dev/null +++ b/osu.Game/Overlays/FirstRunSetup/ProgressRoundedButton.cs @@ -0,0 +1,103 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Overlays.FirstRunSetup +{ + public class ProgressRoundedButton : RoundedButton + { + public new Action? Action; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private ProgressBar progressBar = null!; + + private LoadingSpinner loading = null!; + + private SpriteIcon tick = null!; + + public ProgressRoundedButton() + { + base.Action = () => + { + loading.Show(); + Enabled.Value = false; + + Action?.Invoke(); + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddRange(new Drawable[] + { + progressBar = new ProgressBar(false) + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + FillColour = BackgroundColour, + Alpha = 0.5f, + Depth = float.MinValue + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding(15), + Size = new Vector2(20), + Children = new Drawable[] + { + loading = new LoadingSpinner + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + }, + tick = new SpriteIcon + { + Icon = FontAwesome.Solid.Check, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Alpha = 0, + } + } + }, + }); + } + + public void Complete() + { + loading.Hide(); + tick.FadeIn(500, Easing.OutQuint); + + Background.FadeColour(colours.Green, 500, Easing.OutQuint); + progressBar.FillColour = colours.Green; + + this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint); + } + + public void Abort() + { + loading.Hide(); + Enabled.Value = true; + this.TransformBindableTo(progressBar.Current, 0, 500, Easing.OutQuint); + } + + public void SetProgress(double progress, bool animated) + { + this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 66acdca8c7..f7615d5ce9 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -14,8 +14,6 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Online; using osuTK; @@ -127,7 +125,10 @@ namespace osu.Game.Overlays.FirstRunSetup if (t.IsCompletedSuccessfully) importBeatmapsButton.Complete(); else + { importBeatmapsButton.Enabled.Value = true; + importBeatmapsButton.Abort(); + } })); } }, @@ -214,45 +215,5 @@ namespace osu.Game.Overlays.FirstRunSetup downloadBundledButton.SetProgress(progress, true); } } - - private class ProgressRoundedButton : RoundedButton - { - [Resolved] - private OsuColour colours { get; set; } = null!; - - private ProgressBar progressBar = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(progressBar = new ProgressBar(false) - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - FillColour = BackgroundColour, - Alpha = 0.5f, - Depth = float.MinValue - }); - } - - public void Complete() - { - Enabled.Value = false; - - Background.FadeColour(colours.Green, 500, Easing.OutQuint); - progressBar.FillColour = colours.Green; - - this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint); - } - - public void SetProgress(double progress, bool animated) - { - if (!Enabled.Value) - return; - - this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint); - } - } } } From 4412fec41a0fdc5ea7ff1c063dcbf0aff594210a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 19:21:26 +0900 Subject: [PATCH 20/60] Add import from stable screen --- .../FirstRunSetupOverlayStrings.cs | 10 ++ .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 34 +----- .../FirstRunSetup/ScreenImportFromStable.cs | 105 ++++++++++++++++++ osu.Game/Overlays/FirstRunSetupOverlay.cs | 25 +++-- 4 files changed, 131 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index 91b427e2ca..6b5ca7534d 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs @@ -74,6 +74,16 @@ We recommend you give the new defaults a try, but if you'd like to have things f /// public static LocalisableString ClassicDefaults => new TranslatableString(getKey(@"classic_defaults"), @"Classic defaults"); + /// + /// "Welcome" + /// + public static LocalisableString ImportTitle => new TranslatableString(getKey(@"import_title"), @"Import"); + + /// + /// "Import content from stable" + /// + public static LocalisableString ImportContentFromStable => new TranslatableString(getKey(@"import_content_from_stable"), @"Import content from osu!(stable)"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index f7615d5ce9..17e04c0c99 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -25,7 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup public class ScreenBeatmaps : FirstRunSetupScreen { private ProgressRoundedButton downloadBundledButton = null!; - private ProgressRoundedButton importBeatmapsButton = null!; private ProgressRoundedButton downloadTutorialButton = null!; private OsuTextFlowContainer currentlyLoadedBeatmaps = null!; @@ -41,8 +40,8 @@ namespace osu.Game.Overlays.FirstRunSetup private IDisposable? beatmapSubscription; - [BackgroundDependencyLoader(permitNulls: true)] - private void load(LegacyImportManager? legacyImportManager) + [BackgroundDependencyLoader] + private void load() { Vector2 buttonSize = new Vector2(400, 50); @@ -104,35 +103,6 @@ namespace osu.Game.Overlays.FirstRunSetup Action = downloadBundled }, new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) - { - Colour = OverlayColourProvider.Content1, - Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }, - importBeatmapsButton = new ProgressRoundedButton - { - Size = buttonSize, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - BackgroundColour = colours.Blue3, - Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable, - Action = () => - { - importBeatmapsButton.Enabled.Value = false; - legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => - { - if (t.IsCompletedSuccessfully) - importBeatmapsButton.Complete(); - else - { - importBeatmapsButton.Enabled.Value = true; - importBeatmapsButton.Abort(); - } - })); - } - }, - new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) { Colour = OverlayColourProvider.Content1, Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps, diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs new file mode 100644 index 0000000000..c55bb07295 --- /dev/null +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Overlays.Settings; +using osuTK; + +namespace osu.Game.Overlays.FirstRunSetup +{ + [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.ImportTitle))] + public class ScreenImportFromStable : FirstRunSetupScreen + { + private ProgressRoundedButton importButton = null!; + + private SettingsCheckbox checkboxSkins = null!; + private SettingsCheckbox checkboxBeatmaps = null!; + private SettingsCheckbox checkboxScores = null!; + private SettingsCheckbox checkboxCollections = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved(canBeNull: true)] + private LegacyImportManager? legacyImportManager { get; set; } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load() + { + Vector2 buttonSize = new Vector2(400, 50); + + Content.Children = new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + Colour = OverlayColourProvider.Content1, + Text = + "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + checkboxBeatmaps = new SettingsCheckbox + { + LabelText = "Beatmaps", + Current = { Value = true } + }, + checkboxScores = new SettingsCheckbox + { + LabelText = "Scores", + Current = { Value = true } + }, + checkboxSkins = new SettingsCheckbox + { + LabelText = "Skins", + Current = { Value = true } + }, + checkboxCollections = new SettingsCheckbox + { + LabelText = "Collections", + Current = { Value = true } + }, + importButton = new ProgressRoundedButton + { + Size = buttonSize, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + BackgroundColour = colours.Blue3, + Text = FirstRunSetupOverlayStrings.ImportContentFromStable, + Action = runImport + }, + }; + } + + private void runImport() + { + importButton.Enabled.Value = false; + + StableContent importableContent = 0; + + if (checkboxBeatmaps.Current.Value) importableContent |= StableContent.Beatmaps; + if (checkboxScores.Current.Value) importableContent |= StableContent.Scores; + if (checkboxSkins.Current.Value) importableContent |= StableContent.Skins; + if (checkboxCollections.Current.Value) importableContent |= StableContent.Collections; + + legacyImportManager?.ImportFromStableAsync(importableContent) + .ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + importButton.Complete(); + else + { + importButton.Enabled.Value = true; + importButton.Abort(); + } + })); + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index cebb2f5e3b..dae2815e98 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,6 +18,7 @@ using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -55,13 +57,7 @@ namespace osu.Game.Overlays /// public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; - private readonly Type[] steps = - { - typeof(ScreenWelcome), - typeof(ScreenBeatmaps), - typeof(ScreenUIScale), - typeof(ScreenBehaviour), - }; + private readonly List steps = new List(); private Container screenContent = null!; @@ -77,9 +73,16 @@ namespace osu.Game.Overlays { } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, LegacyImportManager? legacyImportManager) { + steps.Add(typeof(ScreenWelcome)); + steps.Add(typeof(ScreenBeatmaps)); + if (legacyImportManager?.SupportsImportFromStable == true) + steps.Add(typeof(ScreenImportFromStable)); + steps.Add(typeof(ScreenUIScale)); + steps.Add(typeof(ScreenBehaviour)); + Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; @@ -313,7 +316,7 @@ namespace osu.Game.Overlays currentStepIndex++; - if (currentStepIndex < steps.Length) + if (currentStepIndex < steps.Count) { var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value]); @@ -345,7 +348,7 @@ namespace osu.Game.Overlays return; bool isFirstStep = currentStepIndex == 0; - bool isLastStep = currentStepIndex == steps.Length - 1; + bool isLastStep = currentStepIndex == steps.Count - 1; if (isFirstStep) { From 6448c97929d56ef86edb13a0808c58026fdfcbf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 19:57:00 +0900 Subject: [PATCH 21/60] Allow retrieving count of available stable imports --- osu.Game/Collections/CollectionManager.cs | 12 ++++++++++++ osu.Game/Database/LegacyImportManager.cs | 23 +++++++++++++++++++++++ osu.Game/Database/LegacyModelImporter.cs | 2 ++ 3 files changed, 37 insertions(+) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 5845e0d4d1..700b0f5dcb 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -111,6 +111,18 @@ namespace osu.Game.Collections public Action PostNotification { protected get; set; } + public Task GetAvailableCount(StableStorage stableStorage) + { + if (!stableStorage.Exists(database_name)) + return Task.FromResult(0); + + return Task.Run(() => + { + using (var stream = stableStorage.GetStream(database_name)) + return readCollections(stream).Count; + }); + } + /// /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 59394c2952..a9f37fe4a7 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -49,6 +49,29 @@ namespace osu.Game.Database public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; + public async Task GetImportCount(StableContent content) + { + var stableStorage = await getStableStorage().ConfigureAwait(false); + + switch (content) + { + case StableContent.Beatmaps: + return await new LegacyBeatmapImporter(beatmaps).GetAvailableCount(stableStorage); + + case StableContent.Skins: + return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage); + + case StableContent.Collections: + return await collections.GetAvailableCount(stableStorage); + + case StableContent.Scores: + return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage); + + default: + throw new ArgumentException($"Only one {nameof(StableContent)} flag should be specified."); + } + } + public async Task ImportFromStableAsync(StableContent content) { var stableStorage = await getStableStorage().ConfigureAwait(false); diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs index d85fb5aab2..a6e0f5bf8b 100644 --- a/osu.Game/Database/LegacyModelImporter.cs +++ b/osu.Game/Database/LegacyModelImporter.cs @@ -34,6 +34,8 @@ namespace osu.Game.Database Importer = importer; } + public Task GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(stableStorage).Count()); + public Task ImportFromStableAsync(StableStorage stableStorage) { var storage = PrepareStableStorage(stableStorage); From 1666d13d262455c2323fe097f1d4bfbde0006675 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 20:13:34 +0900 Subject: [PATCH 22/60] Add item counts to import screen --- .../Overlays/FirstRunSetup/ScreenImportFromStable.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index c55bb07295..42a10854e8 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -4,6 +4,7 @@ #nullable enable using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Database; @@ -28,8 +29,8 @@ namespace osu.Game.Overlays.FirstRunSetup [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved(canBeNull: true)] - private LegacyImportManager? legacyImportManager { get; set; } + [Resolved] + private LegacyImportManager legacyImportManager { get; set; } = null!; [BackgroundDependencyLoader(permitNulls: true)] private void load() @@ -76,6 +77,11 @@ namespace osu.Game.Overlays.FirstRunSetup Action = runImport }, }; + + legacyImportManager.GetImportCount(StableContent.Beatmaps).ContinueWith(task => Schedule(() => checkboxBeatmaps.LabelText += $" ({task.GetResultSafely()} items)")); + legacyImportManager.GetImportCount(StableContent.Scores).ContinueWith(task => Schedule(() => checkboxScores.LabelText += $" ({task.GetResultSafely()} items)")); + legacyImportManager.GetImportCount(StableContent.Skins).ContinueWith(task => Schedule(() => checkboxSkins.LabelText += $" ({task.GetResultSafely()} items)")); + legacyImportManager.GetImportCount(StableContent.Collections).ContinueWith(task => Schedule(() => checkboxCollections.LabelText += $" ({task.GetResultSafely()} items)")); } private void runImport() @@ -89,7 +95,7 @@ namespace osu.Game.Overlays.FirstRunSetup if (checkboxSkins.Current.Value) importableContent |= StableContent.Skins; if (checkboxCollections.Current.Value) importableContent |= StableContent.Collections; - legacyImportManager?.ImportFromStableAsync(importableContent) + legacyImportManager.ImportFromStableAsync(importableContent) .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) From 13e70eab515c2aeacec660fdf978908deb93e399 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 20:37:38 +0900 Subject: [PATCH 23/60] Allow cancellation of count operations and bypassing interactive location logic --- osu.Game/Database/LegacyImportManager.cs | 35 +++++--- .../FirstRunSetup/ScreenImportFromStable.cs | 87 +++++++++++++++++-- 2 files changed, 107 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index a9f37fe4a7..9c436e8903 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; @@ -49,9 +50,14 @@ namespace osu.Game.Database public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; - public async Task GetImportCount(StableContent content) + public async Task GetImportCount(StableContent content, CancellationToken cancellationToken) { - var stableStorage = await getStableStorage().ConfigureAwait(false); + var stableStorage = GetCurrentStableStorage(); + + if (stableStorage == null) + return 0; + + cancellationToken.ThrowIfCancellationRequested(); switch (content) { @@ -72,9 +78,22 @@ namespace osu.Game.Database } } - public async Task ImportFromStableAsync(StableContent content) + public async Task ImportFromStableAsync(StableContent content, bool interactiveLocateIfNotFound = true) { - var stableStorage = await getStableStorage().ConfigureAwait(false); + var stableStorage = GetCurrentStableStorage(); + + if (stableStorage == null) + { + if (!interactiveLocateIfNotFound) + return; + + var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource))); + string stablePath = await taskCompletionSource.Task.ConfigureAwait(false); + + stableStorage = cachedStorage = new StableStorage(stablePath, desktopGameHost); + } + var importTasks = new List(); Task beatmapImportTask = Task.CompletedTask; @@ -93,7 +112,7 @@ namespace osu.Game.Database await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); } - private async Task getStableStorage() + public StableStorage GetCurrentStableStorage() { if (cachedStorage != null) return cachedStorage; @@ -102,11 +121,7 @@ namespace osu.Game.Database if (stableStorage != null) return cachedStorage = stableStorage; - var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource))); - string stablePath = await taskCompletionSource.Task.ConfigureAwait(false); - - return cachedStorage = new StableStorage(stablePath, desktopGameHost); + return null; } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 42a10854e8..e31f70b686 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -3,6 +3,8 @@ #nullable enable +using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -10,6 +12,7 @@ using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osuTK; @@ -26,12 +29,16 @@ namespace osu.Game.Overlays.FirstRunSetup private SettingsCheckbox checkboxScores = null!; private SettingsCheckbox checkboxCollections = null!; + private OsuTextFlowContainer currentStablePath = null!; + [Resolved] private OsuColour colours { get; set; } = null!; [Resolved] private LegacyImportManager legacyImportManager { get; set; } = null!; + private CancellationTokenSource? stablePathUpdateCancellation; + [BackgroundDependencyLoader(permitNulls: true)] private void load() { @@ -47,6 +54,21 @@ namespace osu.Game.Overlays.FirstRunSetup RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y }, + currentStablePath = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: HEADER_FONT_SIZE, weight: FontWeight.SemiBold)) + { + Colour = OverlayColourProvider.Content2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + new RoundedButton + { + Size = buttonSize, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + BackgroundColour = colours.Blue3, + Text = "Locate osu!(stable) install", + Action = locateStable, + }, checkboxBeatmaps = new SettingsCheckbox { LabelText = "Beatmaps", @@ -78,10 +100,65 @@ namespace osu.Game.Overlays.FirstRunSetup }, }; - legacyImportManager.GetImportCount(StableContent.Beatmaps).ContinueWith(task => Schedule(() => checkboxBeatmaps.LabelText += $" ({task.GetResultSafely()} items)")); - legacyImportManager.GetImportCount(StableContent.Scores).ContinueWith(task => Schedule(() => checkboxScores.LabelText += $" ({task.GetResultSafely()} items)")); - legacyImportManager.GetImportCount(StableContent.Skins).ContinueWith(task => Schedule(() => checkboxSkins.LabelText += $" ({task.GetResultSafely()} items)")); - legacyImportManager.GetImportCount(StableContent.Collections).ContinueWith(task => Schedule(() => checkboxCollections.LabelText += $" ({task.GetResultSafely()} items)")); + updateStablePath(); + } + + private void locateStable() + { + legacyImportManager.ImportFromStableAsync(0).ContinueWith(task => + { + Schedule(updateStablePath); + }); + } + + private void updateStablePath() + { + stablePathUpdateCancellation?.Cancel(); + + var storage = legacyImportManager.GetCurrentStableStorage(); + + if (storage == null) + { + foreach (var c in Content.Children.OfType()) + c.Current.Disabled = true; + currentStablePath.Text = "No installation found"; + return; + } + + foreach (var c in Content.Children.OfType()) + c.Current.Disabled = false; + + currentStablePath.Text = storage.GetFullPath(string.Empty); + stablePathUpdateCancellation = new CancellationTokenSource(); + + legacyImportManager.GetImportCount(StableContent.Beatmaps, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => + { + if (task.IsCanceled) + return; + + checkboxBeatmaps.LabelText = $"Beatmaps ({task.GetResultSafely()} items)"; + })); + legacyImportManager.GetImportCount(StableContent.Scores, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => + { + if (task.IsCanceled) + return; + + checkboxScores.LabelText = $"Scores ({task.GetResultSafely()} items)"; + })); + legacyImportManager.GetImportCount(StableContent.Skins, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => + { + if (task.IsCanceled) + return; + + checkboxSkins.LabelText = $"Skins ({task.GetResultSafely()} items)"; + })); + legacyImportManager.GetImportCount(StableContent.Collections, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => + { + if (task.IsCanceled) + return; + + checkboxCollections.LabelText = $"Collections ({task.GetResultSafely()} items)"; + })); } private void runImport() @@ -95,7 +172,7 @@ namespace osu.Game.Overlays.FirstRunSetup if (checkboxSkins.Current.Value) importableContent |= StableContent.Skins; if (checkboxCollections.Current.Value) importableContent |= StableContent.Collections; - legacyImportManager.ImportFromStableAsync(importableContent) + legacyImportManager.ImportFromStableAsync(importableContent, false) .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) From 1593f05ff37c34bad7cc25b2f3d0d9ad9c84738d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 20:53:04 +0900 Subject: [PATCH 24/60] Tidy up checkbox implementation --- .../FirstRunSetup/ScreenImportFromStable.cs | 134 ++++++++---------- 1 file changed, 63 insertions(+), 71 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index e31f70b686..dd3580508a 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -24,11 +25,6 @@ namespace osu.Game.Overlays.FirstRunSetup { private ProgressRoundedButton importButton = null!; - private SettingsCheckbox checkboxSkins = null!; - private SettingsCheckbox checkboxBeatmaps = null!; - private SettingsCheckbox checkboxScores = null!; - private SettingsCheckbox checkboxCollections = null!; - private OsuTextFlowContainer currentStablePath = null!; [Resolved] @@ -39,6 +35,8 @@ namespace osu.Game.Overlays.FirstRunSetup private CancellationTokenSource? stablePathUpdateCancellation; + private IEnumerable contentCheckboxes => Content.Children.OfType(); + [BackgroundDependencyLoader(permitNulls: true)] private void load() { @@ -69,26 +67,10 @@ namespace osu.Game.Overlays.FirstRunSetup Text = "Locate osu!(stable) install", Action = locateStable, }, - checkboxBeatmaps = new SettingsCheckbox - { - LabelText = "Beatmaps", - Current = { Value = true } - }, - checkboxScores = new SettingsCheckbox - { - LabelText = "Scores", - Current = { Value = true } - }, - checkboxSkins = new SettingsCheckbox - { - LabelText = "Skins", - Current = { Value = true } - }, - checkboxCollections = new SettingsCheckbox - { - LabelText = "Collections", - Current = { Value = true } - }, + new ImportCheckbox("Beatmaps", StableContent.Beatmaps), + new ImportCheckbox("Scores", StableContent.Scores), + new ImportCheckbox("Skins", StableContent.Skins), + new ImportCheckbox("Collections", StableContent.Collections), importButton = new ProgressRoundedButton { Size = buttonSize, @@ -119,46 +101,20 @@ namespace osu.Game.Overlays.FirstRunSetup if (storage == null) { - foreach (var c in Content.Children.OfType()) + foreach (var c in contentCheckboxes) c.Current.Disabled = true; currentStablePath.Text = "No installation found"; return; } - foreach (var c in Content.Children.OfType()) + foreach (var c in contentCheckboxes) + { c.Current.Disabled = false; + c.UpdateCount(); + } currentStablePath.Text = storage.GetFullPath(string.Empty); stablePathUpdateCancellation = new CancellationTokenSource(); - - legacyImportManager.GetImportCount(StableContent.Beatmaps, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => - { - if (task.IsCanceled) - return; - - checkboxBeatmaps.LabelText = $"Beatmaps ({task.GetResultSafely()} items)"; - })); - legacyImportManager.GetImportCount(StableContent.Scores, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => - { - if (task.IsCanceled) - return; - - checkboxScores.LabelText = $"Scores ({task.GetResultSafely()} items)"; - })); - legacyImportManager.GetImportCount(StableContent.Skins, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => - { - if (task.IsCanceled) - return; - - checkboxSkins.LabelText = $"Skins ({task.GetResultSafely()} items)"; - })); - legacyImportManager.GetImportCount(StableContent.Collections, stablePathUpdateCancellation.Token).ContinueWith(task => Schedule(() => - { - if (task.IsCanceled) - return; - - checkboxCollections.LabelText = $"Collections ({task.GetResultSafely()} items)"; - })); } private void runImport() @@ -167,22 +123,58 @@ namespace osu.Game.Overlays.FirstRunSetup StableContent importableContent = 0; - if (checkboxBeatmaps.Current.Value) importableContent |= StableContent.Beatmaps; - if (checkboxScores.Current.Value) importableContent |= StableContent.Scores; - if (checkboxSkins.Current.Value) importableContent |= StableContent.Skins; - if (checkboxCollections.Current.Value) importableContent |= StableContent.Collections; + foreach (var c in contentCheckboxes.Where(c => c.Current.Value)) + importableContent |= c.StableContent; - legacyImportManager.ImportFromStableAsync(importableContent, false) - .ContinueWith(t => Schedule(() => - { - if (t.IsCompletedSuccessfully) - importButton.Complete(); - else - { - importButton.Enabled.Value = true; - importButton.Abort(); - } - })); + legacyImportManager.ImportFromStableAsync(importableContent, false).ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + importButton.Complete(); + else + { + importButton.Enabled.Value = true; + importButton.Abort(); + } + })); + } + + private class ImportCheckbox : SettingsCheckbox + { + public readonly StableContent StableContent; + + private readonly LocalisableString title; + + [Resolved] + private LegacyImportManager legacyImportManager { get; set; } = null!; + + private CancellationTokenSource? countUpdateCancellation; + + public ImportCheckbox(LocalisableString title, StableContent stableContent) + { + this.title = title; + + StableContent = stableContent; + + Current.Value = true; + + LabelText = title; + } + + public void UpdateCount() + { + LabelText = LocalisableString.Interpolate($"{title} (calculating...)"); + + countUpdateCancellation?.Cancel(); + countUpdateCancellation = new CancellationTokenSource(); + + legacyImportManager.GetImportCount(StableContent, countUpdateCancellation.Token).ContinueWith(task => Schedule(() => + { + if (task.IsCanceled) + return; + + LabelText = LocalisableString.Interpolate($"{title} ({task.GetResultSafely()} items)"); + })); + } } } } From 4af1a788d14fadffcbd98953a769aacdd5b87d6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 21:07:42 +0900 Subject: [PATCH 25/60] Add locate stable button / screen --- osu.Game/Database/LegacyImportManager.cs | 5 +- .../FirstRunSetup/FirstRunSetupScreen.cs | 4 +- .../FirstRunSetup/ScreenImportFromStable.cs | 134 +++++++++++++++++- 3 files changed, 136 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 9c436e8903..35b1d05fa1 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -50,6 +50,8 @@ namespace osu.Game.Database public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; + public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost); + public async Task GetImportCount(StableContent content, CancellationToken cancellationToken) { var stableStorage = GetCurrentStableStorage(); @@ -91,7 +93,8 @@ namespace osu.Game.Database Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource))); string stablePath = await taskCompletionSource.Task.ConfigureAwait(false); - stableStorage = cachedStorage = new StableStorage(stablePath, desktopGameHost); + UpdateStorage(stablePath); + stableStorage = GetCurrentStableStorage(); } var importTasks = new List(); diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs index d1ea91e51a..b043f05bd8 100644 --- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs +++ b/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs @@ -21,6 +21,8 @@ namespace osu.Game.Overlays.FirstRunSetup protected const float CONTENT_FONT_SIZE = 16; + protected const float CONTENT_PADDING = 30; + protected const float HEADER_FONT_SIZE = 24; [Resolved] @@ -41,7 +43,7 @@ namespace osu.Game.Overlays.FirstRunSetup { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 30 }, + Padding = new MarginPadding { Horizontal = CONTENT_PADDING }, Children = new Drawable[] { new OsuSpriteText diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index dd3580508a..acc0fdbb7c 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -4,19 +4,24 @@ #nullable enable using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osuTK; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Overlays.FirstRunSetup { @@ -24,6 +29,7 @@ namespace osu.Game.Overlays.FirstRunSetup public class ScreenImportFromStable : FirstRunSetupScreen { private ProgressRoundedButton importButton = null!; + private RoundedButton locateStableButton = null!; private OsuTextFlowContainer currentStablePath = null!; @@ -58,7 +64,7 @@ namespace osu.Game.Overlays.FirstRunSetup RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, - new RoundedButton + locateStableButton = new RoundedButton { Size = buttonSize, Anchor = Anchor.TopCentre, @@ -85,12 +91,15 @@ namespace osu.Game.Overlays.FirstRunSetup updateStablePath(); } - private void locateStable() + private void locateStable() => this.Push(new LocateStableScreen()); + + public override void OnResuming(ScreenTransitionEvent e) { - legacyImportManager.ImportFromStableAsync(0).ContinueWith(task => - { + base.OnResuming(e); + + if (e.Last is LocateStableScreen) + // stable storage may have changed. Schedule(updateStablePath); - }); } private void updateStablePath() @@ -120,6 +129,7 @@ namespace osu.Game.Overlays.FirstRunSetup private void runImport() { importButton.Enabled.Value = false; + locateStableButton.Enabled.Value = false; StableContent importableContent = 0; @@ -128,6 +138,8 @@ namespace osu.Game.Overlays.FirstRunSetup legacyImportManager.ImportFromStableAsync(importableContent, false).ContinueWith(t => Schedule(() => { + locateStableButton.Enabled.Value = true; + if (t.IsCompletedSuccessfully) importButton.Complete(); else @@ -176,5 +188,117 @@ namespace osu.Game.Overlays.FirstRunSetup })); } } + + private class LocateStableScreen : FirstRunSetupScreen + { + private RoundedButton selectionButton = null!; + + private OsuDirectorySelector directorySelector = null!; + + protected bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; + + public LocalisableString HeaderText => "Please select your osu!stable install location"; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + // Don't want the scroll content provided by `FirstRunSetupScreen` so we don't use `Content`. + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = CONTENT_PADDING }, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OsuTextFlowContainer(cp => + { + cp.Font = OsuFont.Default.With(size: 24); + }) + { + Text = HeaderText, + TextAnchor = Anchor.TopCentre, + Margin = new MarginPadding(10), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }, + new Drawable[] + { + directorySelector = new OsuDirectorySelector + { + RelativeSizeAxes = Axes.Both, + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new RoundedButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 300, + Margin = new MarginPadding(10), + Colour = colours.Pink2, + Text = CommonStrings.ButtonsCancel, + Action = this.Exit + }, + selectionButton = new RoundedButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 300, + Margin = new MarginPadding(10), + Text = MaintenanceSettingsStrings.SelectDirectory, + Action = () => + { + legacyImportManager.UpdateStorage(directorySelector.CurrentPath.Value.FullName); + this.Exit(); + } + }, + } + }, + } + } + } + } + }; + } + + [Resolved] + private LegacyImportManager legacyImportManager { get; set; } = null!; + + protected override void LoadComplete() + { + if (legacyImportManager.GetCurrentStableStorage() is StableStorage storage) + directorySelector.CurrentPath.Value = new DirectoryInfo(storage.GetFullPath(string.Empty)); + + directorySelector.CurrentPath.BindValueChanged(e => selectionButton.Enabled.Value = e.NewValue != null && IsValidDirectory(e.NewValue), true); + base.LoadComplete(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + + this.FadeOut(250); + } + } } } From 98e5ad44a71a090476a55ba40d7a0dfbb67cc327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 21:24:36 +0900 Subject: [PATCH 26/60] Add `OverlayColourProvider` support to `OsuDirectorySelector` --- .../UserInterfaceV2/OsuDirectorySelectorDirectory.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index ce2e7794a9..456bde6d1b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -44,8 +45,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 internal class Background : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider overlayColourProvider, OsuColour colours) { RelativeSizeAxes = Axes.Both; @@ -54,7 +55,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 InternalChild = new Box { - Colour = colours.GreySeaFoamDarker, + Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker, RelativeSizeAxes = Axes.Both, }; } From e2ea98693f8761ba0fc0fd76f786963d434eba5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 May 2022 21:33:15 +0900 Subject: [PATCH 27/60] Tidy up buttons and text --- .../FirstRunSetup/ScreenImportFromStable.cs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index acc0fdbb7c..f867adff79 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -28,6 +28,8 @@ namespace osu.Game.Overlays.FirstRunSetup [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.ImportTitle))] public class ScreenImportFromStable : FirstRunSetupScreen { + private static readonly Vector2 button_size = new Vector2(400, 50); + private ProgressRoundedButton importButton = null!; private RoundedButton locateStableButton = null!; @@ -46,8 +48,6 @@ namespace osu.Game.Overlays.FirstRunSetup [BackgroundDependencyLoader(permitNulls: true)] private void load() { - Vector2 buttonSize = new Vector2(400, 50); - Content.Children = new Drawable[] { new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) @@ -63,14 +63,14 @@ namespace osu.Game.Overlays.FirstRunSetup Colour = OverlayColourProvider.Content2, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.Centre, }, locateStableButton = new RoundedButton { - Size = buttonSize, + Size = button_size, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - BackgroundColour = colours.Blue3, - Text = "Locate osu!(stable) install", + Text = "Change location", Action = locateStable, }, new ImportCheckbox("Beatmaps", StableContent.Beatmaps), @@ -79,10 +79,9 @@ namespace osu.Game.Overlays.FirstRunSetup new ImportCheckbox("Collections", StableContent.Collections), importButton = new ProgressRoundedButton { - Size = buttonSize, + Size = button_size, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - BackgroundColour = colours.Blue3, Text = FirstRunSetupOverlayStrings.ImportContentFromStable, Action = runImport }, @@ -112,7 +111,10 @@ namespace osu.Game.Overlays.FirstRunSetup { foreach (var c in contentCheckboxes) c.Current.Disabled = true; + currentStablePath.FadeColour(colours.Red1, 500, Easing.OutQuint); currentStablePath.Text = "No installation found"; + + importButton.Enabled.Value = false; return; } @@ -122,8 +124,10 @@ namespace osu.Game.Overlays.FirstRunSetup c.UpdateCount(); } - currentStablePath.Text = storage.GetFullPath(string.Empty); + currentStablePath.FadeColour(OverlayColourProvider.Content2); + currentStablePath.Text = $"Found installation: {storage.GetFullPath(string.Empty)}"; stablePathUpdateCancellation = new CancellationTokenSource(); + importButton.Enabled.Value = true; } private void runImport() @@ -138,13 +142,12 @@ namespace osu.Game.Overlays.FirstRunSetup legacyImportManager.ImportFromStableAsync(importableContent, false).ContinueWith(t => Schedule(() => { - locateStableButton.Enabled.Value = true; - if (t.IsCompletedSuccessfully) importButton.Complete(); else { importButton.Enabled.Value = true; + locateStableButton.Enabled.Value = true; importButton.Abort(); } })); @@ -253,9 +256,11 @@ namespace osu.Game.Overlays.FirstRunSetup { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Width = 300, + RelativeSizeAxes = Axes.X, + Width = 0.45f, + Height = button_size.Y, Margin = new MarginPadding(10), - Colour = colours.Pink2, + BackgroundColour = colours.Pink2, Text = CommonStrings.ButtonsCancel, Action = this.Exit }, @@ -263,7 +268,9 @@ namespace osu.Game.Overlays.FirstRunSetup { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Width = 300, + RelativeSizeAxes = Axes.X, + Width = 0.45f, + Height = button_size.Y, Margin = new MarginPadding(10), Text = MaintenanceSettingsStrings.SelectDirectory, Action = () => From 8112416335429ee4d18475887d88c40558fd6c1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 15:25:45 +0900 Subject: [PATCH 28/60] Assert that downloads are queued early enough to work --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index df0a69cb25..84903d381a 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; using osu.Game.Online; @@ -89,6 +91,8 @@ namespace osu.Game.Beatmaps.Drawables private void queueDownloads(string[] sourceFilenames, int? limit = null) { + Debug.Assert(LoadState == LoadState.NotLoaded); + try { // Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps. From 3ff0399281f2f2ca0b5356aa08c48133623962db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 16:32:31 +0900 Subject: [PATCH 29/60] Split out `LabelledTextBoxWithPopover` for reuse --- .../Edit/Setup/FileChooserLabelledTextBox.cs | 37 +------------- .../Edit/Setup/LabelledTextBoxWithPopover.cs | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index d1e35ae20d..aae19396db 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -11,11 +11,8 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -24,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup /// /// A labelled textbox which reveals an inline file chooser when clicked. /// - internal class FileChooserLabelledTextBox : LabelledTextBox, ICanAcceptFiles, IHasPopover + internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles { private readonly string[] handledExtensions; @@ -40,16 +37,6 @@ namespace osu.Game.Screens.Edit.Setup this.handledExtensions = handledExtensions; } - protected override OsuTextBox CreateTextBox() => - new FileChooserOsuTextBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - CornerRadius = CORNER_RADIUS, - OnFocused = this.ShowPopover - }; - protected override void LoadComplete() { base.LoadComplete(); @@ -81,27 +68,7 @@ namespace osu.Game.Screens.Edit.Setup game.UnregisterImportHandler(this); } - internal class FileChooserOsuTextBox : OsuTextBox - { - public Action OnFocused; - - protected override bool OnDragStart(DragStartEvent e) - { - // This text box is intended to be "read only" without actually specifying that. - // As such we don't want to allow the user to select its content with a drag. - return false; - } - - protected override void OnFocus(FocusEvent e) - { - OnFocused?.Invoke(); - base.OnFocus(e); - - GetContainingInputManager().TriggerFocusContention(this); - } - } - - public Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile); + public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile); private class FileChooserPopover : OsuPopover { diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs new file mode 100644 index 0000000000..1136675347 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs @@ -0,0 +1,49 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + internal abstract class LabelledTextBoxWithPopover : LabelledTextBox, IHasPopover + { + public abstract Popover GetPopover(); + + protected override OsuTextBox CreateTextBox() => + new PopoverTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + OnFocused = this.ShowPopover + }; + + internal class PopoverTextBox : OsuTextBox + { + public Action OnFocused; + + protected override bool OnDragStart(DragStartEvent e) + { + // This text box is intended to be "read only" without actually specifying that. + // As such we don't want to allow the user to select its content with a drag. + return false; + } + + protected override void OnFocus(FocusEvent e) + { + OnFocused?.Invoke(); + base.OnFocus(e); + + GetContainingInputManager().TriggerFocusContention(this); + } + } + } +} From adc7b61240c55d9b2bec397f1a05a1427cf56558 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 16:33:02 +0900 Subject: [PATCH 30/60] Switch directory selector to use a semi-shared popover implementation --- .../FirstRunSetup/ScreenImportFromStable.cs | 226 +++++++----------- osu.Game/Overlays/FirstRunSetupOverlay.cs | 3 +- 2 files changed, 86 insertions(+), 143 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index f867adff79..8b7550e301 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -3,25 +3,27 @@ #nullable enable +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; 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.UserInterface; using osu.Framework.Localisation; -using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Overlays.Settings; +using osu.Game.Screens.Edit.Setup; using osuTK; -using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Overlays.FirstRunSetup { @@ -31,18 +33,14 @@ namespace osu.Game.Overlays.FirstRunSetup private static readonly Vector2 button_size = new Vector2(400, 50); private ProgressRoundedButton importButton = null!; - private RoundedButton locateStableButton = null!; - - private OsuTextFlowContainer currentStablePath = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; [Resolved] private LegacyImportManager legacyImportManager { get; set; } = null!; private CancellationTokenSource? stablePathUpdateCancellation; + private StableLocatorLabelledTextBox stableLocatorTextBox = null!; + private IEnumerable contentCheckboxes => Content.Children.OfType(); [BackgroundDependencyLoader(permitNulls: true)] @@ -58,20 +56,10 @@ namespace osu.Game.Overlays.FirstRunSetup RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y }, - currentStablePath = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: HEADER_FONT_SIZE, weight: FontWeight.SemiBold)) + stableLocatorTextBox = new StableLocatorLabelledTextBox { - Colour = OverlayColourProvider.Content2, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.Centre, - }, - locateStableButton = new RoundedButton - { - Size = button_size, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Change location", - Action = locateStable, + Label = "previous osu! install", + PlaceholderText = "Click to locate a previous osu! install" }, new ImportCheckbox("Beatmaps", StableContent.Beatmaps), new ImportCheckbox("Scores", StableContent.Scores), @@ -87,18 +75,7 @@ namespace osu.Game.Overlays.FirstRunSetup }, }; - updateStablePath(); - } - - private void locateStable() => this.Push(new LocateStableScreen()); - - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - - if (e.Last is LocateStableScreen) - // stable storage may have changed. - Schedule(updateStablePath); + stableLocatorTextBox.Current.BindValueChanged(_ => updateStablePath(), true); } private void updateStablePath() @@ -111,9 +88,8 @@ namespace osu.Game.Overlays.FirstRunSetup { foreach (var c in contentCheckboxes) c.Current.Disabled = true; - currentStablePath.FadeColour(colours.Red1, 500, Easing.OutQuint); - currentStablePath.Text = "No installation found"; + stableLocatorTextBox.Current.Value = string.Empty; importButton.Enabled.Value = false; return; } @@ -124,8 +100,8 @@ namespace osu.Game.Overlays.FirstRunSetup c.UpdateCount(); } - currentStablePath.FadeColour(OverlayColourProvider.Content2); - currentStablePath.Text = $"Found installation: {storage.GetFullPath(string.Empty)}"; + stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty); + stablePathUpdateCancellation = new CancellationTokenSource(); importButton.Enabled.Value = true; } @@ -133,7 +109,7 @@ namespace osu.Game.Overlays.FirstRunSetup private void runImport() { importButton.Enabled.Value = false; - locateStableButton.Enabled.Value = false; + stableLocatorTextBox.Current.Disabled = true; StableContent importableContent = 0; @@ -147,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup else { importButton.Enabled.Value = true; - locateStableButton.Enabled.Value = true; + stableLocatorTextBox.Current.Disabled = false; importButton.Abort(); } })); @@ -192,119 +168,85 @@ namespace osu.Game.Overlays.FirstRunSetup } } - private class LocateStableScreen : FirstRunSetupScreen + internal class StableLocatorLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles { - private RoundedButton selectionButton = null!; - - private OsuDirectorySelector directorySelector = null!; - - protected bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false; - - public LocalisableString HeaderText => "Please select your osu!stable install location"; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - // Don't want the scroll content provided by `FirstRunSetupScreen` so we don't use `Content`. - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = CONTENT_PADDING }, - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new OsuTextFlowContainer(cp => - { - cp.Font = OsuFont.Default.With(size: 24); - }) - { - Text = HeaderText, - TextAnchor = Anchor.TopCentre, - Margin = new MarginPadding(10), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }, - new Drawable[] - { - directorySelector = new OsuDirectorySelector - { - RelativeSizeAxes = Axes.Both, - } - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new RoundedButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Width = 0.45f, - Height = button_size.Y, - Margin = new MarginPadding(10), - BackgroundColour = colours.Pink2, - Text = CommonStrings.ButtonsCancel, - Action = this.Exit - }, - selectionButton = new RoundedButton - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - Width = 0.45f, - Height = button_size.Y, - Margin = new MarginPadding(10), - Text = MaintenanceSettingsStrings.SelectDirectory, - Action = () => - { - legacyImportManager.UpdateStorage(directorySelector.CurrentPath.Value.FullName); - this.Exit(); - } - }, - } - }, - } - } - } - } - }; - } - [Resolved] private LegacyImportManager legacyImportManager { get; set; } = null!; + // TODO: test + public IEnumerable HandledExtensions { get; } = new[] { string.Empty }; + + private readonly Bindable currentDirectory = new Bindable(); + + [Resolved] + private OsuGameBase game { get; set; } = null!; + protected override void LoadComplete() { - if (legacyImportManager.GetCurrentStableStorage() is StableStorage storage) - directorySelector.CurrentPath.Value = new DirectoryInfo(storage.GetFullPath(string.Empty)); - - directorySelector.CurrentPath.BindValueChanged(e => selectionButton.Enabled.Value = e.NewValue != null && IsValidDirectory(e.NewValue), true); base.LoadComplete(); + + game.RegisterImportHandler(this); + + currentDirectory.BindValueChanged(onDirectorySelected); + + string? fullPath = legacyImportManager.GetCurrentStableStorage()?.GetFullPath(string.Empty); + if (fullPath != null) + currentDirectory.Value = new DirectoryInfo(fullPath); } - public override void OnSuspending(ScreenTransitionEvent e) + private void onDirectorySelected(ValueChangedEvent directory) { - base.OnSuspending(e); + if (directory.NewValue == null || directory.OldValue == null) + { + Current.Value = string.Empty; + return; + } - this.FadeOut(250); + // DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this. + if (directory.OldValue.FullName == directory.NewValue.FullName) + return; + + if (directory.NewValue?.GetFiles(@"osu!.*.cfg").Any() ?? false) + { + this.HidePopover(); + + string path = directory.NewValue.FullName; + + legacyImportManager.UpdateStorage(path); + Current.Value = path; + } + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => currentDirectory.Value = new DirectoryInfo(paths.First())); + return Task.CompletedTask; + } + + Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + game.UnregisterImportHandler(this); + } + + public override Popover GetPopover() => new DirectoryChooserPopover(currentDirectory); + + private class DirectoryChooserPopover : OsuPopover + { + public DirectoryChooserPopover(Bindable currentDirectory) + { + Child = new Container + { + Size = new Vector2(600, 400), + Child = new OsuDirectorySelector(currentDirectory.Value?.FullName) + { + RelativeSizeAxes = Axes.Both, + CurrentPath = { BindTarget = currentDirectory } + }, + }; + } } } } diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index dae2815e98..890f17b013 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -88,7 +89,7 @@ namespace osu.Game.Overlays MainAreaContent.AddRange(new Drawable[] { - content = new Container + content = new PopoverContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 30fdbc3de070fffecdf6e7666de15b4e6e264b42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 16:55:41 +0900 Subject: [PATCH 31/60] Add safety against calling `GetStableImportPaths` when path doesn't exist --- osu.Game/Database/LegacyModelImporter.cs | 10 ++++++++-- osu.Game/Database/LegacyScoreImporter.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs index a6e0f5bf8b..13a9025ece 100644 --- a/osu.Game/Database/LegacyModelImporter.cs +++ b/osu.Game/Database/LegacyModelImporter.cs @@ -24,8 +24,14 @@ namespace osu.Game.Database /// /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) - .Select(path => storage.GetFullPath(path)); + protected virtual IEnumerable GetStableImportPaths(Storage storage) + { + if (!storage.ExistsDirectory(ImportFromStablePath)) + return Enumerable.Empty(); + + return storage.GetDirectories(ImportFromStablePath) + .Select(path => storage.GetFullPath(path)); + } protected readonly IModelImporter Importer; diff --git a/osu.Game/Database/LegacyScoreImporter.cs b/osu.Game/Database/LegacyScoreImporter.cs index 48445b7bdb..131b4ffb0e 100644 --- a/osu.Game/Database/LegacyScoreImporter.cs +++ b/osu.Game/Database/LegacyScoreImporter.cs @@ -15,8 +15,14 @@ namespace osu.Game.Database protected override string ImportFromStablePath => Path.Combine("Data", "r"); protected override IEnumerable GetStableImportPaths(Storage storage) - => storage.GetFiles(ImportFromStablePath).Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => storage.GetFullPath(path)); + { + if (!storage.ExistsDirectory(ImportFromStablePath)) + return Enumerable.Empty(); + + return storage.GetFiles(ImportFromStablePath) + .Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); + } public LegacyScoreImporter(IModelImporter importer) : base(importer) From 03d3900a02ede396fe0fa0eb095b2579586fde66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:12:10 +0900 Subject: [PATCH 32/60] Fix incorrect default state of checkboxes --- osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 8b7550e301..7791c3614e 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -146,6 +146,7 @@ namespace osu.Game.Overlays.FirstRunSetup StableContent = stableContent; + Current.Default = true; Current.Value = true; LabelText = title; From 17e0105c2cff5b17aefd5f9b52c1dd29d6b9482c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:26:26 +0900 Subject: [PATCH 33/60] Fix interaction with popover when textbox is disabled --- osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs index 1136675347..799311dd2d 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs @@ -39,6 +39,9 @@ namespace osu.Game.Screens.Edit.Setup protected override void OnFocus(FocusEvent e) { + if (Current.Disabled) + return; + OnFocused?.Invoke(); base.OnFocus(e); From c0de1f96ff2158a293fc546a11b729d84ea4df77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:26:44 +0900 Subject: [PATCH 34/60] Tidy up interaction toggling and add progress message --- .../FirstRunSetupOverlayStrings.cs | 4 +- .../FirstRunSetup/ScreenImportFromStable.cs | 42 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index 6b5ca7534d..181e35e4ce 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs @@ -80,9 +80,9 @@ We recommend you give the new defaults a try, but if you'd like to have things f public static LocalisableString ImportTitle => new TranslatableString(getKey(@"import_title"), @"Import"); /// - /// "Import content from stable" + /// "Import content from previous version" /// - public static LocalisableString ImportContentFromStable => new TranslatableString(getKey(@"import_content_from_stable"), @"Import content from osu!(stable)"); + public static LocalisableString ImportContentFromPreviousVersion => new TranslatableString(getKey(@"import_content_from_previous_version"), @"Import content from previous version"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 7791c3614e..3b3cf4caf7 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -34,11 +34,11 @@ namespace osu.Game.Overlays.FirstRunSetup private ProgressRoundedButton importButton = null!; + private OsuTextFlowContainer progressText = null!; + [Resolved] private LegacyImportManager legacyImportManager { get; set; } = null!; - private CancellationTokenSource? stablePathUpdateCancellation; - private StableLocatorLabelledTextBox stableLocatorTextBox = null!; private IEnumerable contentCheckboxes => Content.Children.OfType(); @@ -70,9 +70,18 @@ namespace osu.Game.Overlays.FirstRunSetup Size = button_size, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = FirstRunSetupOverlayStrings.ImportContentFromStable, + Text = FirstRunSetupOverlayStrings.ImportContentFromPreviousVersion, Action = runImport }, + progressText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + Colour = OverlayColourProvider.Content1, + Text = + "Your import will continue in the background. Check on its progress in the notifications sidebar!", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + }, }; stableLocatorTextBox.Current.BindValueChanged(_ => updateStablePath(), true); @@ -80,17 +89,14 @@ namespace osu.Game.Overlays.FirstRunSetup private void updateStablePath() { - stablePathUpdateCancellation?.Cancel(); - var storage = legacyImportManager.GetCurrentStableStorage(); if (storage == null) { - foreach (var c in contentCheckboxes) - c.Current.Disabled = true; + allowInteraction(false); + stableLocatorTextBox.Current.Disabled = false; stableLocatorTextBox.Current.Value = string.Empty; - importButton.Enabled.Value = false; return; } @@ -100,16 +106,15 @@ namespace osu.Game.Overlays.FirstRunSetup c.UpdateCount(); } + allowInteraction(true); stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty); - - stablePathUpdateCancellation = new CancellationTokenSource(); importButton.Enabled.Value = true; } private void runImport() { - importButton.Enabled.Value = false; - stableLocatorTextBox.Current.Disabled = true; + allowInteraction(false); + progressText.FadeIn(1000, Easing.OutQuint); StableContent importableContent = 0; @@ -118,17 +123,26 @@ namespace osu.Game.Overlays.FirstRunSetup legacyImportManager.ImportFromStableAsync(importableContent, false).ContinueWith(t => Schedule(() => { + progressText.FadeOut(500, Easing.OutQuint); + if (t.IsCompletedSuccessfully) importButton.Complete(); else { - importButton.Enabled.Value = true; - stableLocatorTextBox.Current.Disabled = false; + allowInteraction(true); importButton.Abort(); } })); } + private void allowInteraction(bool allow) + { + importButton.Enabled.Value = allow; + stableLocatorTextBox.Current.Disabled = !allow; + foreach (var c in contentCheckboxes) + c.Current.Disabled = !allow; + } + private class ImportCheckbox : SettingsCheckbox { public readonly StableContent StableContent; From 83e781d5a1ef9da5e3c0d6f071ca33c44df20279 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:42:46 +0900 Subject: [PATCH 35/60] Allow localisation of `PlaceholderText` --- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 3 ++- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index fd64cc2056..82b9fe559f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 @@ -25,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.ReadOnly = value; } - public string PlaceholderText + public LocalisableString PlaceholderText { set => Component.PlaceholderText = value; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index ab21a83c43..9abea73f6b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateBankPlaceholderText(IEnumerable objects) { string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray()); - bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null; + bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } private void updateVolumeFor(IEnumerable objects, int? newVolume) From f4e0ad8c4c87c41454ce28033f150569fa01174a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:51:28 +0900 Subject: [PATCH 36/60] Fix drag drop of osu! folder not working as expected --- osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 3b3cf4caf7..b145dadd39 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -211,14 +211,14 @@ namespace osu.Game.Overlays.FirstRunSetup private void onDirectorySelected(ValueChangedEvent directory) { - if (directory.NewValue == null || directory.OldValue == null) + if (directory.NewValue == null) { Current.Value = string.Empty; return; } // DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this. - if (directory.OldValue.FullName == directory.NewValue.FullName) + if (directory.OldValue?.FullName == directory.NewValue.FullName) return; if (directory.NewValue?.GetFiles(@"osu!.*.cfg").Any() ?? false) From bf00b062ad05b000f5cb955881494c6da57ece9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 17:58:24 +0900 Subject: [PATCH 37/60] Add full localisation of import beatmaps screen --- ...RunOverlayImportFromStableScreenStrings.cs | 56 +++++++++++++++++++ .../FirstRunSetupOverlayStrings.cs | 10 ---- .../FirstRunSetup/ScreenImportFromStable.cs | 20 +++---- 3 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs new file mode 100644 index 0000000000..deac7d8628 --- /dev/null +++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs @@ -0,0 +1,56 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class FirstRunOverlayImportFromStableScreenStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.ScreenImportFromStable"; + + /// + /// "Import" + /// + public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import"); + + /// + /// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation." + /// + public static LocalisableString Description => new TranslatableString(getKey(@"description"), + @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."); + + /// + /// "previous osu! install" + /// + public static LocalisableString LocateDirectoryLabel => new TranslatableString(getKey(@"locate_directory_label"), @"previous osu! install"); + + /// + /// "Click to locate a previous osu! install" + /// + public static LocalisableString LocateDirectoryPlaceholder => new TranslatableString(getKey(@"locate_directory_placeholder"), @"Click to locate a previous osu! install"); + + /// + /// "Import content from previous version" + /// + public static LocalisableString ImportButton => new TranslatableString(getKey(@"import_button"), @"Import content from previous version"); + + /// + /// "Your import will continue in the background. Check on its progress in the notifications sidebar!" + /// + public static LocalisableString ImportInProgress => + new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!"); + + /// + /// "calculating..." + /// + public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating..."); + + /// + /// "{0} items" + /// + public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index 181e35e4ce..91b427e2ca 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs @@ -74,16 +74,6 @@ We recommend you give the new defaults a try, but if you'd like to have things f /// public static LocalisableString ClassicDefaults => new TranslatableString(getKey(@"classic_defaults"), @"Classic defaults"); - /// - /// "Welcome" - /// - public static LocalisableString ImportTitle => new TranslatableString(getKey(@"import_title"), @"Import"); - - /// - /// "Import content from previous version" - /// - public static LocalisableString ImportContentFromPreviousVersion => new TranslatableString(getKey(@"import_content_from_previous_version"), @"Import content from previous version"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index b145dadd39..d5ef879479 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -27,7 +27,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { - [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.ImportTitle))] + [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] public class ScreenImportFromStable : FirstRunSetupScreen { private static readonly Vector2 button_size = new Vector2(400, 50); @@ -51,15 +51,14 @@ namespace osu.Game.Overlays.FirstRunSetup new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) { Colour = OverlayColourProvider.Content1, - Text = - "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.", + Text = FirstRunOverlayImportFromStableScreenStrings.Description, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y }, stableLocatorTextBox = new StableLocatorLabelledTextBox { - Label = "previous osu! install", - PlaceholderText = "Click to locate a previous osu! install" + Label = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryLabel, + PlaceholderText = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryPlaceholder }, new ImportCheckbox("Beatmaps", StableContent.Beatmaps), new ImportCheckbox("Scores", StableContent.Scores), @@ -70,14 +69,13 @@ namespace osu.Game.Overlays.FirstRunSetup Size = button_size, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = FirstRunSetupOverlayStrings.ImportContentFromPreviousVersion, + Text = FirstRunOverlayImportFromStableScreenStrings.ImportButton, Action = runImport }, progressText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) { Colour = OverlayColourProvider.Content1, - Text = - "Your import will continue in the background. Check on its progress in the notifications sidebar!", + Text = FirstRunOverlayImportFromStableScreenStrings.ImportInProgress, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Alpha = 0, @@ -168,7 +166,7 @@ namespace osu.Game.Overlays.FirstRunSetup public void UpdateCount() { - LabelText = LocalisableString.Interpolate($"{title} (calculating...)"); + LabelText = LocalisableString.Interpolate($"{title} ({FirstRunOverlayImportFromStableScreenStrings.Calculating})"); countUpdateCancellation?.Cancel(); countUpdateCancellation = new CancellationTokenSource(); @@ -178,7 +176,9 @@ namespace osu.Game.Overlays.FirstRunSetup if (task.IsCanceled) return; - LabelText = LocalisableString.Interpolate($"{title} ({task.GetResultSafely()} items)"); + int count = task.GetResultSafely(); + + LabelText = LocalisableString.Interpolate($"{title} ({FirstRunOverlayImportFromStableScreenStrings.Items(count)})"); })); } } From 1b7ec1be26e806796258298641cba65a9d043370 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 18:15:14 +0900 Subject: [PATCH 38/60] Add basic test coverage of new screen --- ...TestSceneFirstRunScreenImportFromStable.cs | 48 +++++++++++++++++++ osu.Game/Database/LegacyImportManager.cs | 8 ++-- 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.cs new file mode 100644 index 0000000000..081b240795 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenImportFromStable.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 System.Threading; +using System.Threading.Tasks; +using Moq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Database; +using osu.Game.Overlays; +using osu.Game.Overlays.FirstRunSetup; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneFirstRunScreenImportFromStable : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private readonly Mock legacyImportManager = new Mock(); + + [BackgroundDependencyLoader] + private void load() + { + legacyImportManager.Setup(m => m.GetImportCount(It.IsAny(), It.IsAny())).Returns(() => Task.FromResult(RNG.Next(0, 256))); + + Dependencies.CacheAs(legacyImportManager.Object); + } + + public TestSceneFirstRunScreenImportFromStable() + { + AddStep("load screen", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new ScreenStack(new ScreenImportFromStable()) + } + }; + }); + } + } +} diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 35b1d05fa1..af9db1b6ec 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -37,13 +37,13 @@ namespace osu.Game.Database [Resolved] private CollectionManager collections { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } [Resolved] private IDialogOverlay dialogOverlay { get; set; } - [Resolved(CanBeNull = true)] + [Resolved(canBeNull: true)] private DesktopGameHost desktopGameHost { get; set; } private StableStorage cachedStorage; @@ -52,7 +52,7 @@ namespace osu.Game.Database public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, desktopGameHost); - public async Task GetImportCount(StableContent content, CancellationToken cancellationToken) + public virtual async Task GetImportCount(StableContent content, CancellationToken cancellationToken) { var stableStorage = GetCurrentStableStorage(); @@ -120,7 +120,7 @@ namespace osu.Game.Database if (cachedStorage != null) return cachedStorage; - var stableStorage = game.GetStorageForStableInstall(); + var stableStorage = game?.GetStorageForStableInstall(); if (stableStorage != null) return cachedStorage = stableStorage; From 062ffe64acf700d295d22585af9a8d1c95362684 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 May 2022 18:21:19 +0900 Subject: [PATCH 39/60] Remove delay on pop in --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8d72855217..f1a998bd3c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -428,7 +428,6 @@ namespace osu.Game.Overlays.Mods base.PopIn(); multiplierDisplay? - .Delay(fade_in_duration / 5) .FadeIn(fade_in_duration, Easing.OutQuint) .MoveToY(0, fade_in_duration, Easing.OutQuint); From 3f71224dfc26a7cd87c6737bac79f86af84de2b2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 May 2022 14:05:21 +0900 Subject: [PATCH 40/60] Package .json files in nupkg output --- osu.Desktop/osu.nuspec | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index dc1ec17e2c..db58c325bd 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -18,5 +18,6 @@ + From fe49a7e6782e66dedbe5805192c6930589aa6522 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 May 2022 17:10:19 +0900 Subject: [PATCH 41/60] Add failing tests --- .../Formats/LegacyBeatmapDecoderTest.cs | 35 +++++++++++++++++++ .../Resources/adjacent-catmull-segments.osu | 2 ++ 2 files changed, 37 insertions(+) create mode 100644 osu.Game.Tests/Resources/adjacent-catmull-segments.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 468cb7683c..89baaf228d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -863,5 +863,40 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1)); } } + + [Test] + public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; + + Assert.That(controlPoints.Count, Is.EqualTo(6)); + Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull)); + } + } + + [Test] + public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged() + { + var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; + + Assert.That(controlPoints.Count, Is.EqualTo(4)); + Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[3].Type, Is.Null); + } + } } } diff --git a/osu.Game.Tests/Resources/adjacent-catmull-segments.osu b/osu.Game.Tests/Resources/adjacent-catmull-segments.osu new file mode 100644 index 0000000000..a436fe5228 --- /dev/null +++ b/osu.Game.Tests/Resources/adjacent-catmull-segments.osu @@ -0,0 +1,2 @@ +[HitObjects] +200,304,23875,6,0,C|288:304|288:304|288:208|288:208|352:208,1,260,8|0 \ No newline at end of file From 731f0960ec53ed3a6059ae5a29cfd5a27f5c3d67 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 May 2022 17:11:08 +0900 Subject: [PATCH 42/60] Don't merge adjacent legacy Catmull segments --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index b91a74c4a1..3b4200e7a9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapEncoder { - public const int LATEST_VERSION = 128; + public const int FIRST_LAZER_VERSION = 128; /// /// osu! is generally slower than taiko, so a factor is added to increase @@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps.Formats public void Encode(TextWriter writer) { - writer.WriteLine($"osu file format v{LATEST_VERSION}"); + writer.WriteLine($"osu file format v{FIRST_LAZER_VERSION}"); writer.WriteLine(); handleGeneral(writer); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2a7f2b037f..7cf68a2df7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -336,10 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy while (++endIndex < vertices.Length - endPointLength) { - // Keep incrementing while an implicit segment doesn't need to be started + // Keep incrementing while an implicit segment doesn't need to be started. if (vertices[endIndex].Position != vertices[endIndex - 1].Position) continue; + // Adjacent legacy Catmull segments should be treated as a single segment. + if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION && type == PathType.Catmull) + continue; + // The last control point of each segment is not allowed to start a new implicit segment. if (endIndex == vertices.Length - endPointLength - 1) continue; From df4968a55bda4c0d6dcc0cd9bb9d18ca94f511f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 May 2022 21:20:14 +0900 Subject: [PATCH 43/60] Add `new Guid` bannedsymbols rule --- CodeAnalysis/BannedSymbols.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index e96ad48325..b3cca21c98 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. +M:System.Guid.#ctor;Probably meaning to use Guid.New() instead. If actually wanting empty, use Guid.Empty(). M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. From fa09270e6278ad808c82021938227b1fb3f46b36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 May 2022 21:21:20 +0900 Subject: [PATCH 44/60] Remove left-over todo --- osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index d5ef879479..ad6ff21353 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -188,7 +188,6 @@ namespace osu.Game.Overlays.FirstRunSetup [Resolved] private LegacyImportManager legacyImportManager { get; set; } = null!; - // TODO: test public IEnumerable HandledExtensions { get; } = new[] { string.Empty }; private readonly Bindable currentDirectory = new Bindable(); From d54e1503c763f735360bc8caac09019a4a5dbd60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 May 2022 21:22:17 +0900 Subject: [PATCH 45/60] Rename interaction toggle method --- .../Overlays/FirstRunSetup/ScreenImportFromStable.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index ad6ff21353..106e78dc18 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -91,7 +91,7 @@ namespace osu.Game.Overlays.FirstRunSetup if (storage == null) { - allowInteraction(false); + toggleInteraction(false); stableLocatorTextBox.Current.Disabled = false; stableLocatorTextBox.Current.Value = string.Empty; @@ -104,14 +104,14 @@ namespace osu.Game.Overlays.FirstRunSetup c.UpdateCount(); } - allowInteraction(true); + toggleInteraction(true); stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty); importButton.Enabled.Value = true; } private void runImport() { - allowInteraction(false); + toggleInteraction(false); progressText.FadeIn(1000, Easing.OutQuint); StableContent importableContent = 0; @@ -127,13 +127,13 @@ namespace osu.Game.Overlays.FirstRunSetup importButton.Complete(); else { - allowInteraction(true); + toggleInteraction(true); importButton.Abort(); } })); } - private void allowInteraction(bool allow) + private void toggleInteraction(bool allow) { importButton.Enabled.Value = allow; stableLocatorTextBox.Current.Disabled = !allow; From 0cee90e156f31492bd81283c7e4b39238f10734d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 May 2022 21:32:32 +0900 Subject: [PATCH 46/60] Add common strings for missing localisable content --- osu.Game/Localisation/CommonStrings.cs | 22 ++++++++++++++++++- .../FirstRunSetup/ScreenImportFromStable.cs | 8 +++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 9cd626af0f..1fd677034d 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -69,6 +69,26 @@ namespace osu.Game.Localisation /// public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All"); + /// + /// "Beatmaps" + /// + public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Beatmaps"); + + /// + /// "Scores" + /// + public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Scores"); + + /// + /// "Skins" + /// + public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Skins"); + + /// + /// "Collections" + /// + public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 106e78dc18..cf8abf0a76 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -60,10 +60,10 @@ namespace osu.Game.Overlays.FirstRunSetup Label = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryLabel, PlaceholderText = FirstRunOverlayImportFromStableScreenStrings.LocateDirectoryPlaceholder }, - new ImportCheckbox("Beatmaps", StableContent.Beatmaps), - new ImportCheckbox("Scores", StableContent.Scores), - new ImportCheckbox("Skins", StableContent.Skins), - new ImportCheckbox("Collections", StableContent.Collections), + new ImportCheckbox(CommonStrings.Beatmaps, StableContent.Beatmaps), + new ImportCheckbox(CommonStrings.Scores, StableContent.Scores), + new ImportCheckbox(CommonStrings.Skins, StableContent.Skins), + new ImportCheckbox(CommonStrings.Collections, StableContent.Collections), importButton = new ProgressRoundedButton { Size = button_size, From f5235f6a563a013529387a73ea481ea3dc9fd5aa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 May 2022 20:38:13 +0300 Subject: [PATCH 47/60] Correctify recommended method and property --- CodeAnalysis/BannedSymbols.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index b3cca21c98..b72df0a306 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -11,7 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. -M:System.Guid.#ctor;Probably meaning to use Guid.New() instead. If actually wanting empty, use Guid.Empty(). +M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. From 371738b047105ec1f14b84baf992b1acc52b068d Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 18 May 2022 20:00:42 +0100 Subject: [PATCH 48/60] Check against the loaded drawable channel --- osu.Game/Overlays/ChatOverlayV2.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index e91d57f18b..611c539f8f 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -263,18 +263,18 @@ namespace osu.Game.Overlays ChatOverlayDrawableChannel drawableChannel = new ChatOverlayDrawableChannel(newChannel); loadedChannels.Add(newChannel, drawableChannel); - LoadComponentAsync(drawableChannel, loaded => + LoadComponentAsync(drawableChannel, loadedDrawable => { // Ensure the current channel hasn't changed by the time the load completes - if (currentChannel.Value != newChannel) + if (currentChannel.Value != loadedDrawable.Channel) return; // Ensure the cached reference hasn't been removed from leaving the channel - if (!loadedChannels.ContainsKey(newChannel)) + if (!loadedChannels.ContainsKey(loadedDrawable.Channel)) return; currentChannelContainer.Clear(false); - currentChannelContainer.Add(loaded); + currentChannelContainer.Add(loadedDrawable); loading.Hide(); }); } From 5d3878a737ae957c359aff643285954172731a52 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 May 2022 22:58:39 +0300 Subject: [PATCH 49/60] Add test coverage for slow-loading channels --- .../Visual/Online/TestSceneChatOverlayV2.cs | 64 ++++++++++++++++++- osu.Game/Overlays/ChatOverlayV2.cs | 6 +- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 82089ff3d1..3e3c8122b3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -5,6 +5,8 @@ using System; using System.Linq; using System.Collections.Generic; using System.Net; +using System.Threading; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene { - private ChatOverlayV2 chatOverlay; + private TestChatOverlayV2 chatOverlay; private ChannelManager channelManager; private APIUser testUser; @@ -61,7 +63,7 @@ namespace osu.Game.Tests.Visual.Online Children = new Drawable[] { channelManager, - chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both }, + chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both }, }, }; }); @@ -386,6 +388,34 @@ namespace osu.Game.Tests.Visual.Online AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); } + [Test] + public void TestSlowLoadingChannel() + { + AddStep("Show overlay (slow-loading)", () => + { + chatOverlay.Show(); + chatOverlay.SlowLoading = true; + }); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading); + + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2))); + AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading); + + AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set()); + AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready); + AddAssert("Channel 1 not displayed", () => !channelIsVisible); + + AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set()); + AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded); + AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2); + + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded); + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + } + private bool listingIsVisible => chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; @@ -432,5 +462,35 @@ namespace osu.Game.Tests.Visual.Online Topic = $"We talk about the number {id} here", Type = ChannelType.Public, }; + + private class TestChatOverlayV2 : ChatOverlayV2 + { + public bool SlowLoading { get; set; } + + public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel); + + protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) + { + return SlowLoading + ? new SlowLoadingDrawableChannel(newChannel) + : new ChatOverlayDrawableChannel(newChannel); + } + } + + private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel + { + public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim(); + + public SlowLoadingDrawableChannel([NotNull] Channel channel) + : base(channel) + { + } + + [BackgroundDependencyLoader] + private void load() + { + LoadEvent.Wait(10000); + } + } } } diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 611c539f8f..b2c1f6858c 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -41,6 +41,8 @@ namespace osu.Game.Overlays private readonly Dictionary loadedChannels = new Dictionary(); + protected IEnumerable DrawableChannels => loadedChannels.Values; + private readonly BindableFloat chatHeight = new BindableFloat(); private bool isDraggingTopBar; private float dragStartChatHeight; @@ -260,7 +262,7 @@ namespace osu.Game.Overlays loading.Show(); // Ensure the drawable channel is stored before async load to prevent double loading - ChatOverlayDrawableChannel drawableChannel = new ChatOverlayDrawableChannel(newChannel); + ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel); loadedChannels.Add(newChannel, drawableChannel); LoadComponentAsync(drawableChannel, loadedDrawable => @@ -281,6 +283,8 @@ namespace osu.Game.Overlays } } + protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel); + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) From 3bca014b52b2b7ea11b3b0ce491d0f0456854311 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 19 May 2022 09:44:11 +0900 Subject: [PATCH 50/60] Bust CI cache on CodeAnalysis ruleset changes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 729f2f266d..1d5c565d60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: uses: actions/cache@v3 with: path: ${{ github.workspace }}/inspectcode - key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }} + key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }} - name: Dotnet code style run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true From a443200d0d25c54a0e28a0691b9ada5a9bd2bab1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 13:49:52 +0900 Subject: [PATCH 51/60] Make dependency nullable to allow for safer disposal unbinding --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 2 +- osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 391147648f..dd6226e19b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); - [Resolved(canBeNull: true)] + [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes. private DrawableHitObject? drawableObject { get; set; } [Resolved] diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index cf8abf0a76..62b517d982 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -192,14 +192,14 @@ namespace osu.Game.Overlays.FirstRunSetup private readonly Bindable currentDirectory = new Bindable(); - [Resolved] - private OsuGameBase game { get; set; } = null!; + [Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes. + private OsuGameBase? game { get; set; } protected override void LoadComplete() { base.LoadComplete(); - game.RegisterImportHandler(this); + game?.RegisterImportHandler(this); currentDirectory.BindValueChanged(onDirectorySelected); @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.FirstRunSetup protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - game.UnregisterImportHandler(this); + game?.UnregisterImportHandler(this); } public override Popover GetPopover() => new DirectoryChooserPopover(currentDirectory); From f6fb8f3fb99be7352e40d4cd7b4df12e755349b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 13:54:44 +0900 Subject: [PATCH 52/60] Delete and ignore rider's `misc.xml` This file has changed on us too many times. Doesn't seem to contain anything of value. --- .gitignore | 2 ++ .idea/.idea.osu.Desktop/.idea/misc.xml | 11 ----------- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 .idea/.idea.osu.Desktop/.idea/misc.xml diff --git a/.gitignore b/.gitignore index 5b19270ab9..0c7a18b437 100644 --- a/.gitignore +++ b/.gitignore @@ -340,3 +340,5 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd **/FodyWeavers.xml + +.idea/.idea.osu.Desktop/.idea/misc.xml \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/misc.xml b/.idea/.idea.osu.Desktop/.idea/misc.xml deleted file mode 100644 index 4e1d56f4dd..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/misc.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file From 70bd40ce443cf7ece4df644dd013afe9f3842c89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 14:01:24 +0900 Subject: [PATCH 53/60] Fix incorrect count of beatmaps available to import --- osu.Game/Database/LegacyModelImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs index 13a9025ece..9b2a54dada 100644 --- a/osu.Game/Database/LegacyModelImporter.cs +++ b/osu.Game/Database/LegacyModelImporter.cs @@ -40,7 +40,7 @@ namespace osu.Game.Database Importer = importer; } - public Task GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(stableStorage).Count()); + public Task GetAvailableCount(StableStorage stableStorage) => Task.Run(() => GetStableImportPaths(PrepareStableStorage(stableStorage)).Count()); public Task ImportFromStableAsync(StableStorage stableStorage) { From 5af7641e946939a15e7729f58ac76de4b6511269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 15:53:53 +0900 Subject: [PATCH 54/60] Add safety against playfield potentially not being available during mania note placement --- .../Blueprints/ManiaPlacementBlueprint.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index ad95fd5e87..7a99565e8a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -55,24 +55,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.UpdateTimeAndPosition(result); - var playfield = (Column)result.Playfield; - - // Apply an offset to better align with the visual grid. - // This should only be applied during placement, as during selection / drag operations the movement is relative - // to the initial point of interaction rather than the grid. - switch (playfield.ScrollingInfo.Direction.Value) + if (result.Playfield is Column col) { - case ScrollingDirection.Down: - result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(playfield) / 2); - break; + // Apply an offset to better align with the visual grid. + // This should only be applied during placement, as during selection / drag operations the movement is relative + // to the initial point of interaction rather than the grid. + switch (col.ScrollingInfo.Direction.Value) + { + case ScrollingDirection.Down: + result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2); + break; - case ScrollingDirection.Up: - result.ScreenSpacePosition += new Vector2(0, getNoteHeight(playfield) / 2); - break; + case ScrollingDirection.Up: + result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2); + break; + } + + if (PlacementActive == PlacementState.Waiting) + Column = col; } - - if (PlacementActive == PlacementState.Waiting) - Column = playfield; } private float getNoteHeight(Column resultPlayfield) => From b3d6f76cfacdca8ec3906c8f92d324b71f1e54fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 15:57:17 +0900 Subject: [PATCH 55/60] Add "None" snap type to fix flags not working correctly --- osu.Game/Rulesets/Edit/SnapType.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs index 4ab67fee63..6761356331 100644 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ b/osu.Game/Rulesets/Edit/SnapType.cs @@ -8,8 +8,9 @@ namespace osu.Game.Rulesets.Edit [Flags] public enum SnapType { - NearbyObjects = 0, - Grids = 1, + None = 0, + NearbyObjects = 1 << 0, + Grids = 1 << 1, All = NearbyObjects | Grids, } } From 363e7a6f537b7feda0f8825192391d64e791a749 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 May 2022 16:42:43 +0900 Subject: [PATCH 56/60] Simplify toolbar hiding logic in `FirstRunSetupOverlay` Rather than fiddling around with the activation modes, this seems like a much cleaner way to make things work. Closes https://github.com/ppy/osu/issues/18277. --- osu.Game/Overlays/FirstRunSetupOverlay.cs | 22 +++------------------- osu.Game/Screens/Menu/MainMenu.cs | 2 ++ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 890f17b013..7b0de4affe 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -62,8 +62,6 @@ namespace osu.Game.Overlays private Container screenContent = null!; - private Bindable? overlayActivationMode; - private Container content = null!; private LoadingSpinner loading = null!; @@ -225,16 +223,9 @@ namespace osu.Game.Overlays // if we are valid for display, only do so after reaching the main menu. performer.PerformFromScreen(screen => { - MainMenu menu = (MainMenu)screen; - - // Eventually I'd like to replace this with a better method that doesn't access the screen. - // Either this dialog would be converted to its own screen, or at very least be "hosted" by a screen pushed to the main menu. - // Alternatively, another method of disabling notifications could be added to `INotificationOverlay`. - if (menu != null) - { - overlayActivationMode = menu.OverlayActivationMode.GetBoundCopy(); - overlayActivationMode.Value = OverlayActivation.UserTriggered; - } + // Hides the toolbar for us. + if (screen is MainMenu menu) + menu.ReturnToOsuLogo(); base.Show(); }, new[] { typeof(MainMenu) }); @@ -257,13 +248,6 @@ namespace osu.Game.Overlays content.ScaleTo(0.99f, 400, Easing.OutQuint); - if (overlayActivationMode != null) - { - // If this is non-null we are guaranteed to have come from the main menu. - overlayActivationMode.Value = OverlayActivation.All; - overlayActivationMode = null; - } - if (currentStepIndex != null) { notificationOverlay.Post(new SimpleNotification diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 4401ee93ec..6fc8039413 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -150,6 +150,8 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IPerformFromScreenRunner performer { get; set; } + public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; + private void confirmAndExit() { if (exitConfirmed) return; From de8aedf3484229b27920873abd4bc02a50bc81cd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 20 May 2022 17:29:37 +0300 Subject: [PATCH 57/60] Add failing test case --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 644a333fcf..995274e722 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Globalization; using System.Linq; using JetBrains.Annotations; using NUnit.Framework; @@ -127,6 +126,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType().Count() == expectedCount); } + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset mods", () => SelectedMods.SetDefault()); + } + [Test] public void TestNullBeatmap() { @@ -147,24 +152,43 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestBPMUpdates() { - const float bpm = 120; + const double bpm = 120; IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); OsuModDoubleTime doubleTime = null; selectBeatmap(beatmap); - checkDisplayedBPM(bpm); + checkDisplayedBPM($"{bpm}"); AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); - checkDisplayedBPM(bpm * 1.5f); + checkDisplayedBPM($"{bpm * 1.5f}"); AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); - checkDisplayedBPM(bpm * 2); + checkDisplayedBPM($"{bpm * 2}"); + } - void checkDisplayedBPM(float target) => - AddUntilStep($"displayed bpm is {target}", () => this.ChildrenOfType().Any( - label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture))); + [TestCase(120, 125, "120-125 (mostly 120)")] + [TestCase(120, 120.6, "120-121 (mostly 120)")] + [TestCase(120, 120.4, "120")] + public void TestVaryingBPM(double commonBpm, double otherBpm, string expectedDisplay) + { + IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); + beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + + selectBeatmap(beatmap); + checkDisplayedBPM(expectedDisplay); + } + + private void checkDisplayedBPM(string target) + { + AddUntilStep($"displayed bpm is {target}", () => + { + var label = this.ChildrenOfType().Single(l => l.Statistic.Name == "BPM"); + return label.Statistic.Content == target; + }); } private void setRuleset(RulesetInfo rulesetInfo) From 596853da8feb59a5f2c7252e1f0de11763979ad0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 20 May 2022 17:43:40 +0300 Subject: [PATCH 58/60] Fix song select potentially displaying BPM range with equal min/max values --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 7db1016f62..55ade6fa33 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -416,13 +415,13 @@ namespace osu.Game.Screens.Select foreach (var mod in mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); - double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; - double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; - double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate; + int bpmMax = (int)Math.Round(beatmap.ControlPointInfo.BPMMaximum * rate); + int bpmMin = (int)Math.Round(beatmap.ControlPointInfo.BPMMinimum * rate); + int mostCommonBPM = (int)Math.Round(60000 / beatmap.GetMostCommonBeatLength() * rate); - string labelText = Precision.AlmostEquals(bpmMin, bpmMax) - ? $"{bpmMin:0}" - : $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; + string labelText = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic { From 84a3cee452364173ed73d405e3a6caade62263b7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 20 May 2022 17:47:45 +0300 Subject: [PATCH 59/60] Apply rate multiplier outside BPM rounding --- .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 13 +++++++++---- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 995274e722..030055903f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -168,16 +168,21 @@ namespace osu.Game.Tests.Visual.SongSelect checkDisplayedBPM($"{bpm * 2}"); } - [TestCase(120, 125, "120-125 (mostly 120)")] - [TestCase(120, 120.6, "120-121 (mostly 120)")] - [TestCase(120, 120.4, "120")] - public void TestVaryingBPM(double commonBpm, double otherBpm, string expectedDisplay) + [TestCase(120, 125, null, "120-125 (mostly 120)")] + [TestCase(120, 120.6, null, "120-121 (mostly 120)")] + [TestCase(120, 120.4, null, "120")] + [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] + [TestCase(120, 120.4, "DT", "180")] + public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay) { IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + if (mod != null) + AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) }); + selectBeatmap(beatmap); checkDisplayedBPM(expectedDisplay); } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 55ade6fa33..d98238f518 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -415,9 +415,9 @@ namespace osu.Game.Screens.Select foreach (var mod in mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); - int bpmMax = (int)Math.Round(beatmap.ControlPointInfo.BPMMaximum * rate); - int bpmMin = (int)Math.Round(beatmap.ControlPointInfo.BPMMinimum * rate); - int mostCommonBPM = (int)Math.Round(60000 / beatmap.GetMostCommonBeatLength() * rate); + int bpmMax = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMaximum) * rate); + int bpmMin = (int)Math.Round(Math.Round(beatmap.ControlPointInfo.BPMMinimum) * rate); + int mostCommonBPM = (int)Math.Round(Math.Round(60000 / beatmap.GetMostCommonBeatLength()) * rate); string labelText = bpmMin == bpmMax ? $"{bpmMin}" From 1f17652c1d55d0f42a9394af259f668961982077 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 20 May 2022 19:08:41 +0300 Subject: [PATCH 60/60] Fix test failure due to async-loading of content --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 030055903f..ef04baefa2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddUntilStep($"displayed bpm is {target}", () => { - var label = this.ChildrenOfType().Single(l => l.Statistic.Name == "BPM"); + var label = infoWedge.DisplayedContent.ChildrenOfType().Single(l => l.Statistic.Name == "BPM"); return label.Statistic.Content == target; }); }