From 4622255cc7a3d20487fce6378ce4d6dc0f487a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 18:24:15 +0200 Subject: [PATCH 01/71] Move out helper methods to static class --- .../Edit/OsuSelectionHandler.cs | 45 ++----- .../SkinEditor/SkinSelectionHandler.cs | 7 +- .../Compose/Components/SelectionHandler.cs | 94 ------------- osu.Game/Utils/GeometryUtils.cs | 126 ++++++++++++++++++ 4 files changed, 143 insertions(+), 129 deletions(-) create mode 100644 osu.Game/Utils/GeometryUtils.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 2a6d6ce4c3..468d8ae9f5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnSelectionChanged(); - Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad(); + Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; @@ -109,13 +110,13 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects); + var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects); bool didFlip = false; foreach (var h in hitObjects) { - var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position); + var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position); if (!Precision.AlmostEquals(flippedPosition, h.Position)) { @@ -173,18 +174,18 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = getSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); referenceOrigin ??= quad.Centre; foreach (var h in hitObjects) { - h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); + h.Position = GeometryUtils.RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); if (h is IHasPath path) { foreach (PathControlPoint cp in path.Path.ControlPoints) - cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); + cp.Position = GeometryUtils.RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); } } @@ -196,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit { referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList(); - Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)); + Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)); // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; @@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Edit slider.SnapTo(snapProvider); //if sliderhead or sliderend end up outside playfield, revert scaling. - Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); + Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); if (xInBounds && yInBounds && slider.Path.HasValidLength) @@ -238,10 +239,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { scale = getClampedScale(hitObjects, reference, scale); - Quad selectionQuad = getSurroundingQuad(hitObjects); + Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); foreach (var h in hitObjects) - h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position); + h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position); } private (bool X, bool Y) isQuadInBounds(Quad quad) @@ -256,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = getSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); Vector2 delta = Vector2.Zero; @@ -286,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.Edit float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - Quad selectionQuad = getSurroundingQuad(hitObjects); + Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); @@ -311,26 +312,6 @@ namespace osu.Game.Rulesets.Osu.Edit return scale; } - /// - /// Returns a gamefield-space quad surrounding the provided hit objects. - /// - /// The hit objects to calculate a quad for. - private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => - GetSurroundingQuad(hitObjects.SelectMany(h => - { - if (h is IHasPath path) - { - return new[] - { - h.Position, - // can't use EndPosition for reverse slider cases. - h.Position + path.Path.PositionAt(1) - }; - } - - return new[] { h.Position }; - })); - /// /// All osu! hitobjects which can be moved/rotated/scaled. /// diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index b43f4eeb00..4a1ddd9d69 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -40,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor { var drawableItem = (Drawable)b.Item; - var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); updateDrawablePosition(drawableItem, rotatedPosition); drawableItem.Rotation += angle; @@ -137,7 +138,7 @@ namespace osu.Game.Overlays.SkinEditor { var drawableItem = (Drawable)b.Item; - var flippedPosition = GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint); + var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint); updateDrawablePosition(drawableItem, flippedPosition); @@ -275,7 +276,7 @@ namespace osu.Game.Overlays.SkinEditor /// /// private Quad getSelectionQuad() => - GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); + GeometryUtils.GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); private void applyFixedAnchors(Anchor anchor) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 052cb18a5d..9b44b15fe4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; @@ -401,98 +400,5 @@ namespace osu.Game.Screens.Edit.Compose.Components => Enumerable.Empty(); #endregion - - #region Helper Methods - - /// - /// Rotate a point around an arbitrary origin. - /// - /// The point. - /// The centre origin to rotate around. - /// The angle to rotate (in degrees). - protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) - { - angle = -angle; - - point.X -= origin.X; - point.Y -= origin.Y; - - Vector2 ret; - ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); - ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); - - ret.X += origin.X; - ret.Y += origin.Y; - - return ret; - } - - /// - /// Given a flip direction, a surrounding quad for all selected objects, and a position, - /// will return the flipped position in screen space coordinates. - /// - protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) - { - var centre = quad.Centre; - - switch (direction) - { - case Direction.Horizontal: - position.X = centre.X - (position.X - centre.X); - break; - - case Direction.Vertical: - position.Y = centre.Y - (position.Y - centre.Y); - break; - } - - return position; - } - - /// - /// Given a scale vector, a surrounding quad for all selected objects, and a position, - /// will return the scaled position in screen space coordinates. - /// - protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) - { - // adjust the direction of scale depending on which side the user is dragging. - float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; - float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - - // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0) - position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - - if (scale.Y != 0 && selectionQuad.Height > 0) - position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); - - return position; - } - - /// - /// Returns a quad surrounding the provided points. - /// - /// The points to calculate a quad for. - protected static Quad GetSurroundingQuad(IEnumerable points) - { - if (!points.Any()) - return new Quad(); - - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); - - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var p in points) - { - minPosition = Vector2.ComponentMin(minPosition, p); - maxPosition = Vector2.ComponentMax(maxPosition, p); - } - - Vector2 size = maxPosition - minPosition; - - return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); - } - - #endregion } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs new file mode 100644 index 0000000000..725e93d098 --- /dev/null +++ b/osu.Game/Utils/GeometryUtils.cs @@ -0,0 +1,126 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Utils +{ + public static class GeometryUtils + { + /// + /// Rotate a point around an arbitrary origin. + /// + /// The point. + /// The centre origin to rotate around. + /// The angle to rotate (in degrees). + public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) + { + angle = -angle; + + point.X -= origin.X; + point.Y -= origin.Y; + + Vector2 ret; + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + + ret.X += origin.X; + ret.Y += origin.Y; + + return ret; + } + + /// + /// Given a flip direction, a surrounding quad for all selected objects, and a position, + /// will return the flipped position in screen space coordinates. + /// + public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) + { + var centre = quad.Centre; + + switch (direction) + { + case Direction.Horizontal: + position.X = centre.X - (position.X - centre.X); + break; + + case Direction.Vertical: + position.Y = centre.Y - (position.Y - centre.Y); + break; + } + + return position; + } + + /// + /// Given a scale vector, a surrounding quad for all selected objects, and a position, + /// will return the scaled position in screen space coordinates. + /// + public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position) + { + // adjust the direction of scale depending on which side the user is dragging. + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + // guard against no-ops and NaN. + if (scale.X != 0 && selectionQuad.Width > 0) + position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + + if (scale.Y != 0 && selectionQuad.Height > 0) + position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + + return position; + } + + /// + /// Returns a quad surrounding the provided points. + /// + /// The points to calculate a quad for. + public static Quad GetSurroundingQuad(IEnumerable points) + { + if (!points.Any()) + return new Quad(); + + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var p in points) + { + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); + } + + Vector2 size = maxPosition - minPosition; + + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + } + + /// + /// Returns a gamefield-space quad surrounding the provided hit objects. + /// + /// The hit objects to calculate a quad for. + public static Quad GetSurroundingQuad(IEnumerable hitObjects) => + GetSurroundingQuad(hitObjects.SelectMany(h => + { + if (h is IHasPath path) + { + return new[] + { + h.Position, + // can't use EndPosition for reverse slider cases. + h.Position + path.Path.PositionAt(1) + }; + } + + return new[] { h.Position }; + })); + } +} From ba8ebefb50dca3a31462ecf4b282036ada782991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 18:09:07 +0200 Subject: [PATCH 02/71] Add basic structure for new rotation handler --- .../Compose/Components/SelectionHandler.cs | 9 ++++++ .../Components/SelectionRotationHandler.cs | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9b44b15fe4..80df796fd7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] protected IEditorChangeHandler ChangeHandler { get; private set; } + protected SelectionRotationHandler RotationHandler { get; private set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -66,6 +68,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + RotationHandler = CreateRotationHandler(); + InternalChild = SelectionBox = CreateSelectionBox(); SelectedItems.CollectionChanged += (_, _) => @@ -132,6 +136,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any items could be rotated. public virtual bool HandleRotation(float angle) => false; + /// + /// Creates the handler to use for rotation operations. + /// + public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); + /// /// Handles the selected items being scaled. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs new file mode 100644 index 0000000000..595edbb4fc --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// Base handler for editor rotation operations. + /// + public class SelectionRotationHandler + { + /// + /// Whether the rotation can currently be performed. + /// + public Bindable CanRotate { get; private set; } = new BindableBool(); + + public virtual void Begin() + { + } + + public virtual void Update(float rotation, Vector2 origin) + { + } + + public virtual void Commit() + { + } + } +} From ba904fd77bbb530b505817b32dbb31ac0e5baa1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:18:38 +0200 Subject: [PATCH 03/71] Migrate osu! rotation handling to `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 30 +----- .../Edit/OsuSelectionRotationHandler.cs | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 468d8ae9f5..1d46b8ff8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - /// - /// During a transform, the initial origin is stored so it can be used throughout the operation. - /// - private Vector2? referenceOrigin; - /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -54,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Edit protected override void OnOperationEnded() { base.OnOperationEnded(); - referenceOrigin = null; referencePathTypes = null; } @@ -170,28 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override bool HandleRotation(float delta) + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(ChangeHandler) { - var hitObjects = selectedMovableObjects; - - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); - - referenceOrigin ??= quad.Centre; - - foreach (var h in hitObjects) - { - h.Position = GeometryUtils.RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); - - if (h is IHasPath path) - { - foreach (PathControlPoint cp in path.Path.ControlPoints) - cp.Position = GeometryUtils.RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta); - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems } + }; private void scaleSlider(Slider slider, Vector2 scale) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs new file mode 100644 index 0000000000..0eb7637786 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + + public OsuSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); + CanRotate.Value = quad.Width > 0 || quad.Height > 0; + } + + private OsuHitObject[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalPositions; + private Dictionary? originalPathControlPointPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = selectedMovableObjects.ToArray(); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); + originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var ho in objectsInRotation) + { + ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation); + + if (ho is IHasPath withPath) + { + var originalPath = originalPathControlPointPositions[withPath]; + + for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i) + withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation); + } + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalPathControlPointPositions = null; + defaultOrigin = null; + } + + private IEnumerable selectedMovableObjects => SelectedItems.Cast() + .Where(h => h is not Spinner); + } +} From f8047d6ab6d96bf9c7b87fcf50b93e2b084da2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:48:39 +0200 Subject: [PATCH 04/71] Migrate skin element rotation handling to `SelectionRotationHandler` --- .../SkinEditor/SkinSelectionHandler.cs | 29 +----- .../SkinSelectionRotationHandler.cs | 94 +++++++++++++++++++ 2 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 4a1ddd9d69..bee973bea0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -26,31 +26,11 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor skinEditor { get; set; } = null!; - public override bool HandleRotation(float angle) + public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler(ChangeHandler) { - if (SelectedBlueprints.Count == 1) - { - // for single items, rotate around the origin rather than the selection centre. - ((Drawable)SelectedBlueprints.First().Item).Rotation += angle; - } - else - { - var selectionQuad = getSelectionQuad(); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle); - updateDrawablePosition(drawableItem, rotatedPosition); - - drawableItem.Rotation += angle; - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } + SelectedItems = { BindTarget = SelectedItems }, + UpdatePosition = updateDrawablePosition + }; public override bool HandleScale(Vector2 scale, Anchor anchor) { @@ -172,7 +152,6 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanRotate = true; SelectionBox.CanScaleX = true; SelectionBox.CanScaleY = true; SelectionBox.CanFlipX = true; diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs new file mode 100644 index 0000000000..e60e2b1e12 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Skinning; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Overlays.SkinEditor +{ + public class SkinSelectionRotationHandler : SelectionRotationHandler + { + private readonly IEditorChangeHandler? changeHandler; + + public BindableList SelectedItems { get; } = new BindableList(); + public Action UpdatePosition { get; init; } = null!; + + public SkinSelectionRotationHandler(IEditorChangeHandler? changeHandler) + { + this.changeHandler = changeHandler; + + SelectedItems.CollectionChanged += (_, __) => updateState(); + } + + private void updateState() + { + CanRotate.Value = SelectedItems.Count > 0; + } + + private Drawable[]? objectsInRotation; + + private Vector2? defaultOrigin; + private Dictionary? originalRotations; + private Dictionary? originalPositions; + + public override void Begin() + { + if (objectsInRotation != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInRotation = SelectedItems.Cast().ToArray(); + originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); + originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null); + + if (objectsInRotation.Length == 1 && origin == null) + { + // for single items, rotate around the origin rather than the selection centre by default. + objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation; + return; + } + + var actualOrigin = origin ?? defaultOrigin.Value; + + foreach (var drawableItem in objectsInRotation) + { + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation); + UpdatePosition(drawableItem, rotatedPosition); + + drawableItem.Rotation = originalRotations[drawableItem] + rotation; + } + } + + public override void Commit() + { + if (objectsInRotation == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInRotation = null; + originalPositions = null; + originalRotations = null; + defaultOrigin = null; + } + } +} From 21df0e2d60824ea1e34cd88f60741484d251d049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 19:57:31 +0200 Subject: [PATCH 05/71] Migrate test to `SelectionRotationHandler` --- .../Editing/TestSceneComposeSelectBox.cs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 7a0b3d0c1a..147488812e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,13 +36,12 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanRotate = true, CanScaleX = true, CanScaleY = true, CanFlipX = true, CanFlipY = true, - OnRotation = handleRotation, + RotationHandler = new TestSelectionRotationHandler(() => selectionArea), OnScale = handleScale } } @@ -71,11 +72,48 @@ namespace osu.Game.Tests.Visual.Editing return true; } - private bool handleRotation(float angle) + private class TestSelectionRotationHandler : SelectionRotationHandler { - // kinda silly and wrong, but just showing that the drag handles work. - selectionArea.Rotation += angle; - return true; + private readonly Func getTargetContainer; + + public TestSelectionRotationHandler(Func getTargetContainer) + { + this.getTargetContainer = getTargetContainer; + + CanRotate.Value = true; + } + + [CanBeNull] + private Container targetContainer; + + private float? initialRotation; + + public override void Begin() + { + if (targetContainer != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + + targetContainer = getTargetContainer(); + initialRotation = targetContainer!.Rotation; + } + + public override void Update(float rotation, Vector2? origin = null) + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); + + // kinda silly and wrong, but just showing that the drag handles work. + targetContainer.Rotation = initialRotation!.Value + rotation; + } + + public override void Commit() + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + targetContainer = null; + initialRotation = null; + } } [Test] From aec3ca250cc3301415d0ba38bc0058b2a2463205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:01:30 +0200 Subject: [PATCH 06/71] Migrate `SelectionHandler` to use `SelectionRotationHandler` --- .../Edit/OsuSelectionHandler.cs | 1 - .../Edit/Compose/Components/SelectionBox.cs | 41 ++++++++----------- .../Components/SelectionBoxRotationHandle.cs | 20 +++++---- .../Compose/Components/SelectionHandler.cs | 2 +- .../Components/SelectionRotationHandler.cs | 9 +++- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1d46b8ff8a..1dfbf4179b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 5d9fac739c..53442071b5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -22,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public Func? OnRotation; + public SelectionRotationHandler? RotationHandler { get; init; } public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -51,22 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool canRotate; - - /// - /// Whether rotation support should be enabled. - /// - public bool CanRotate - { - get => canRotate; - set - { - if (canRotate == value) return; - - canRotate = value; - recreate(); - } - } + private IBindable canRotate = new BindableBool(); private bool canScaleX; @@ -161,7 +147,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] - private void load() => recreate(); + private void load() + { + if (RotationHandler != null) + canRotate.BindTo(RotationHandler.CanRotate); + + canRotate.BindValueChanged(_ => recreate()); + recreate(); + } protected override bool OnKeyDown(KeyDownEvent e) { @@ -174,10 +167,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return CanReverse && reverseButton?.TriggerClick() == true; case Key.Comma: - return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; case Key.Period: - return CanRotate && rotateClockwiseButton?.TriggerClick() == true; + return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; } return base.OnKeyDown(e); @@ -254,14 +247,14 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); - if (CanRotate) addRotationComponents(); + if (canRotate.Value) addRotationComponents(); if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => RotationHandler?.Rotate(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => RotationHandler?.Rotate(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); @@ -331,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - HandleRotate = angle => OnRotation?.Invoke(angle) + RotationHandler = RotationHandler }; handle.OperationStarted += operationStarted; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c2a3f12efd..4107a09692 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -21,7 +22,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - public Action HandleRotate { get; set; } + [CanBeNull] + public SelectionRotationHandler RotationHandler { get; init; } public LocalisableString TooltipText { get; private set; } @@ -63,10 +65,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { - bool handle = base.OnDragStart(e); - if (handle) - cumulativeRotation.Value = 0; - return handle; + if (RotationHandler == null) return false; + + RotationHandler.Begin(); + return true; } protected override void OnDrag(DragEvent e) @@ -99,7 +101,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { - base.OnDragEnd(e); + RotationHandler?.Commit(); + UpdateHoverState(); + cumulativeRotation.Value = null; rawCumulativeRotation = 0; TooltipText = default; @@ -116,14 +120,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyRotation(bool shouldSnap) { - float oldRotation = cumulativeRotation.Value ?? 0; - float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); newRotation = (newRotation - 180) % 360 + 180; cumulativeRotation.Value = newRotation; - HandleRotate?.Invoke(newRotation - oldRotation); + RotationHandler?.Update(newRotation); TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 80df796fd7..31ad8fa3d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = HandleRotation, + RotationHandler = RotationHandler, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 595edbb4fc..d5dd1d38d4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,11 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + public void Rotate(float rotation, Vector2? origin = null) + { + Begin(); + Update(rotation, origin); + Commit(); + } + public virtual void Begin() { } - public virtual void Update(float rotation, Vector2 origin) + public virtual void Update(float rotation, Vector2? origin = null) { } From a201152b042e0dfb13a4abaca85def6b5fc23577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Jul 2023 20:09:31 +0200 Subject: [PATCH 07/71] Add xmldoc to `SelectionRotationHandler` --- .../Components/SelectionRotationHandler.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index d5dd1d38d4..6524f7fa35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -16,6 +16,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotate { get; private set; } = new BindableBool(); + /// + /// Performs a single, instant, atomic rotation operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public void Rotate(float rotation, Vector2? origin = null) { Begin(); @@ -23,14 +35,48 @@ namespace osu.Game.Screens.Edit.Compose.Components Commit(); } + /// + /// Begins a continuous rotation operation. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Begin() { } + /// + /// Updates a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// As such, the values of and supplied should be relative to the state of the objects being rotated + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// Rotation to apply in degrees. + /// + /// The origin point to rotate around. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// public virtual void Update(float rotation, Vector2? origin = null) { } + /// + /// Ends a continuous rotation operation. + /// Must be preceded by a call. + /// + /// + /// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// public virtual void Commit() { } From 2e9379474d70999019d0c2e38affc421db186b5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jul 2023 19:21:20 +0900 Subject: [PATCH 08/71] Change spinner rotation animation to match input 1:1 --- .../Skinning/Default/SpinnerRotationTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index bf06f513b7..719cf57d98 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default lastAngle = thisAngle; - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f; + IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; - Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed)); + Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } /// From 060ad36d601d613c7f81e06e3e4ca1acca4b1d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jul 2023 20:19:03 +0900 Subject: [PATCH 09/71] Add test coverage of music control in editor from external --- .../TestSceneBeatmapEditorNavigation.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 5483be5676..54ee1659e1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -170,6 +170,39 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); } + [Test] + public void TestAttemptGlobalMusicOperationFromEditor() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); + AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); + AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); + AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true)); + AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); + + AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + + AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); + AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); + AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; From 157b1f301b11ad6887b79edbe31e386d21f2c674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jul 2023 19:58:23 +0900 Subject: [PATCH 10/71] Rename `AllowTrackAdjustments` to more understandable `ApplyModTrackAdjustments` --- .../Visual/TestSceneOsuScreenStack.cs | 26 +++++++++---------- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 2 +- osu.Game/Overlays/MusicController.cs | 14 +++++----- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/IOsuScreen.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 14 +++++----- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs index 7f01a67903..6b39717354 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs @@ -56,38 +56,38 @@ namespace osu.Game.Tests.Visual public void AllowTrackAdjustmentsTest() { AddStep("push allowing screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 1", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 1", () => musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 2", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 2", () => musicController.ApplyModTrackAdjustments); AddStep("push disallowing screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 3", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 3", () => !musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 4", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 4", () => !musicController.ApplyModTrackAdjustments); AddStep("push inheriting screen", () => stack.Push(loadNewScreen())); - AddAssert("disallows adjustments 5", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 5", () => !musicController.ApplyModTrackAdjustments); AddStep("push allowing screen", () => stack.Push(loadNewScreen())); - AddAssert("allows adjustments 6", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 6", () => musicController.ApplyModTrackAdjustments); // Now start exiting from screens AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 7", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 7", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 8", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 8", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("disallows adjustments 9", () => !musicController.AllowTrackAdjustments); + AddAssert("disallows adjustments 9", () => !musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("allows adjustments 10", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 10", () => musicController.ApplyModTrackAdjustments); AddStep("exit screen", () => stack.Exit()); - AddAssert("allows adjustments 11", () => musicController.AllowTrackAdjustments); + AddAssert("allows adjustments 11", () => musicController.ApplyModTrackAdjustments); } public partial class TestScreen : ScreenWithBeatmapBackground @@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual private partial class AllowScreen : OsuScreen { - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; } public partial class DisallowScreen : OsuScreen { - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; } private partial class InheritScreen : OsuScreen diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index e3cd2ae36c..02f0ad9506 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.FirstRunSetup { protected override bool ControlGlobalMusic => false; - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; } private partial class UIScaleSlider : RoundedSliderBar diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0d175a624c..562140f5cb 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -356,20 +356,20 @@ namespace osu.Game.Overlays NextTrack(); } - private bool allowTrackAdjustments; + private bool applyModTrackAdjustments; /// /// Whether mod track adjustments are allowed to be applied. /// - public bool AllowTrackAdjustments + public bool ApplyModTrackAdjustments { - get => allowTrackAdjustments; + get => applyModTrackAdjustments; set { - if (allowTrackAdjustments == value) + if (applyModTrackAdjustments == value) return; - allowTrackAdjustments = value; + applyModTrackAdjustments = value; ResetTrackAdjustments(); } } @@ -377,7 +377,7 @@ namespace osu.Game.Overlays private AudioAdjustments modTrackAdjustments; /// - /// Resets the adjustments currently applied on and applies the mod adjustments if is true. + /// Resets the adjustments currently applied on and applies the mod adjustments if is true. /// /// /// Does not reset any adjustments applied directly to the beatmap track. @@ -390,7 +390,7 @@ namespace osu.Game.Overlays CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo); CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Volume); - if (allowTrackAdjustments) + if (applyModTrackAdjustments) { CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments()); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 17e5eb8ef6..b885eee46f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Edit public override bool DisallowExternalBeatmapRulesetChanges => true; - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index cceede5424..756fbb80a7 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens /// Whether mod track adjustments should be applied on entering this screen. /// A value means that the parent screen's value of this setting will be used. /// - bool? AllowTrackAdjustments { get; } + bool? ApplyModTrackAdjustments { get; } /// /// Invoked when the back button has been pressed to close any overlays before exiting this . diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 75b673cf1b..8d08de4168 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached(typeof(IBindable))] public readonly Bindable SelectedItem = new Bindable(); - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2d2aa0f1d5..92ce86bef2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool DisallowExternalBeatmapRulesetChanges => true; // We are managing our own adjustments. For now, this happens inside the Player instances themselves. - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; /// /// Whether all spectating players have finished loading. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 9c098794a6..869d14c030 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens [Resolved] private MusicController musicController { get; set; } - public virtual bool? AllowTrackAdjustments => null; + public virtual bool? ApplyModTrackAdjustments => null; public Bindable Beatmap { get; private set; } @@ -95,7 +95,7 @@ namespace osu.Game.Screens private OsuScreenDependencies screenDependencies; - private bool? trackAdjustmentStateAtSuspend; + private bool? modTrackAdjustmentStateAtSuspend; internal void CreateLeasedDependencies(IReadOnlyDependencyContainer dependencies) => createDependencies(dependencies); @@ -178,8 +178,8 @@ namespace osu.Game.Screens // it's feasible to resume to a screen if the target screen never loaded successfully. // in such a case there's no need to restore this value. - if (trackAdjustmentStateAtSuspend != null) - musicController.AllowTrackAdjustments = trackAdjustmentStateAtSuspend.Value; + if (modTrackAdjustmentStateAtSuspend != null) + musicController.ApplyModTrackAdjustments = modTrackAdjustmentStateAtSuspend.Value; base.OnResuming(e); } @@ -188,7 +188,7 @@ namespace osu.Game.Screens { base.OnSuspending(e); - trackAdjustmentStateAtSuspend = musicController.AllowTrackAdjustments; + modTrackAdjustmentStateAtSuspend = musicController.ApplyModTrackAdjustments; onSuspendingLogo(); } @@ -197,8 +197,8 @@ namespace osu.Game.Screens { applyArrivingDefaults(false); - if (AllowTrackAdjustments != null) - musicController.AllowTrackAdjustments = AllowTrackAdjustments.Value; + if (ApplyModTrackAdjustments != null) + musicController.ApplyModTrackAdjustments = ApplyModTrackAdjustments.Value; if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 379c10a4a4..e2e8b71c10 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Play protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). - public override bool? AllowTrackAdjustments => false; + public override bool? ApplyModTrackAdjustments => false; private readonly IBindable gameActive = new Bindable(true); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 47e5325baf..58755878d0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Select protected virtual bool ShowFooter => true; - public override bool? AllowTrackAdjustments => true; + public override bool? ApplyModTrackAdjustments => true; /// /// Can be null if is false. From 6146f30541086bc1d9b2bb85a03194d663c7d1b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jul 2023 20:00:18 +0900 Subject: [PATCH 11/71] Allow screens to change the ability to interact with the global track --- .../Overlays/Music/MusicKeyBindingHandler.cs | 8 +----- osu.Game/Overlays/MusicController.cs | 28 +++++++++++++++---- osu.Game/Overlays/NowPlayingOverlay.cs | 22 +++++++++------ osu.Game/Screens/IOsuScreen.cs | 6 ++++ osu.Game/Screens/OsuScreen.cs | 10 +++++++ 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index b7d265c448..78de76b981 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -30,20 +30,14 @@ namespace osu.Game.Overlays.Music [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } - [Resolved] - private OsuGame game { get; set; } = null!; - public bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat) + if (e.Repeat || !musicController.AllowTrackControl.Value) return false; switch (e.Action) { case GlobalAction.MusicPlay: - if (game.LocalUserPlaying.Value) - return false; - // use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842) bool wasPlaying = musicController.IsPlaying; diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 562140f5cb..665c61edf0 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -40,6 +40,11 @@ namespace osu.Game.Overlays /// public bool UserPauseRequested { get; private set; } + /// + /// Whether control of the global track should be allowed. + /// + public readonly BindableBool AllowTrackControl = new BindableBool(true); + /// /// Fired when the global has changed. /// Includes direction information for display purposes. @@ -92,8 +97,10 @@ namespace osu.Game.Overlays seekDelegate?.Cancel(); seekDelegate = Schedule(() => { - if (!beatmap.Disabled) - CurrentTrack.Seek(position); + if (beatmap.Disabled || !AllowTrackControl.Value) + return; + + CurrentTrack.Seek(position); }); } @@ -107,7 +114,7 @@ namespace osu.Game.Overlays if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending) { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return; Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}"); @@ -132,6 +139,9 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool Play(bool restart = false, bool requestedByUser = false) { + if (requestedByUser && !AllowTrackControl.Value) + return false; + if (requestedByUser) UserPauseRequested = false; @@ -153,6 +163,9 @@ namespace osu.Game.Overlays /// public void Stop(bool requestedByUser = false) { + if (requestedByUser && !AllowTrackControl.Value) + return; + UserPauseRequested |= requestedByUser; if (CurrentTrack.IsRunning) CurrentTrack.StopAsync(); @@ -164,6 +177,9 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { + if (!AllowTrackControl.Value) + return false; + if (CurrentTrack.IsRunning) Stop(true); else @@ -189,7 +205,7 @@ namespace osu.Game.Overlays /// The that indicate the decided action. private PreviousTrackResult prev() { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return PreviousTrackResult.None; double currentTrackPosition = CurrentTrack.CurrentTime; @@ -229,7 +245,7 @@ namespace osu.Game.Overlays private bool next() { - if (beatmap.Disabled) + if (beatmap.Disabled || !AllowTrackControl.Value) return false; queuedDirection = TrackChangeDirection.Next; @@ -352,7 +368,7 @@ namespace osu.Game.Overlays private void onTrackCompleted() { - if (!CurrentTrack.Looping && !beatmap.Disabled) + if (!CurrentTrack.Looping && !beatmap.Disabled && AllowTrackControl.Value) NextTrack(); } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 15eefb2d9f..78539fde7a 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -68,6 +68,8 @@ namespace osu.Game.Overlays [Resolved] private OsuColour colours { get; set; } + private Bindable allowTrackControl; + public NowPlayingOverlay() { Width = 400; @@ -220,8 +222,10 @@ namespace osu.Game.Overlays { base.LoadComplete(); - beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(beatmapDisabledChanged)); - beatmapDisabledChanged(); + beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(updateEnabledStates)); + + allowTrackControl = musicController.AllowTrackControl.GetBoundCopy(); + allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true); musicController.TrackChanged += trackChanged; trackChanged(beatmap.Value); @@ -334,16 +338,18 @@ namespace osu.Game.Overlays }; } - private void beatmapDisabledChanged() + private void updateEnabledStates() { - bool disabled = beatmap.Disabled; + bool beatmapDisabled = beatmap.Disabled; + bool trackControlDisabled = !musicController.AllowTrackControl.Value; - if (disabled) + if (beatmapDisabled || trackControlDisabled) playlist?.Hide(); - prevButton.Enabled.Value = !disabled; - nextButton.Enabled.Value = !disabled; - playlistButton.Enabled.Value = !disabled; + prevButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + nextButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + playlistButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled; + playButton.Enabled.Value = !trackControlDisabled; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 756fbb80a7..5b4e2d75f4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -69,6 +69,12 @@ namespace osu.Game.Screens /// bool? ApplyModTrackAdjustments { get; } + /// + /// Whether control of the global track should be allowed via the music controller / now playing overlay. + /// A value means that the parent screen's value of this setting will be used. + /// + bool? AllowGlobalTrackControl { get; } + /// /// Invoked when the back button has been pressed to close any overlays before exiting this . /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 869d14c030..2dc9d5d49d 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -87,6 +87,8 @@ namespace osu.Game.Screens public virtual bool? ApplyModTrackAdjustments => null; + public virtual bool? AllowGlobalTrackControl => null; + public Bindable Beatmap { get; private set; } public Bindable Ruleset { get; private set; } @@ -95,6 +97,8 @@ namespace osu.Game.Screens private OsuScreenDependencies screenDependencies; + private bool? globalMusicControlStateAtSuspend; + private bool? modTrackAdjustmentStateAtSuspend; internal void CreateLeasedDependencies(IReadOnlyDependencyContainer dependencies) => createDependencies(dependencies); @@ -180,6 +184,8 @@ namespace osu.Game.Screens // in such a case there's no need to restore this value. if (modTrackAdjustmentStateAtSuspend != null) musicController.ApplyModTrackAdjustments = modTrackAdjustmentStateAtSuspend.Value; + if (globalMusicControlStateAtSuspend != null) + musicController.AllowTrackControl.Value = globalMusicControlStateAtSuspend.Value; base.OnResuming(e); } @@ -189,6 +195,7 @@ namespace osu.Game.Screens base.OnSuspending(e); modTrackAdjustmentStateAtSuspend = musicController.ApplyModTrackAdjustments; + globalMusicControlStateAtSuspend = musicController.AllowTrackControl.Value; onSuspendingLogo(); } @@ -200,6 +207,9 @@ namespace osu.Game.Screens if (ApplyModTrackAdjustments != null) musicController.ApplyModTrackAdjustments = ApplyModTrackAdjustments.Value; + if (AllowGlobalTrackControl != null) + musicController.AllowTrackControl.Value = AllowGlobalTrackControl.Value; + if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true) { // If the constructed instance was not actually pushed to the background stack, we don't want to track it unnecessarily. From 3485b72eaa3885d337d5b71d9fa7c3a87809270f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jul 2023 20:00:31 +0900 Subject: [PATCH 12/71] Disallow interacting with the global track state in `Player` and `Editor` --- osu.Game/Screens/Edit/Editor.cs | 2 ++ osu.Game/Screens/Play/Player.cs | 2 ++ osu.Game/Screens/Ranking/ResultsScreen.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b885eee46f..d11297b3b2 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Edit public override bool? ApplyModTrackAdjustments => false; + public override bool? AllowGlobalTrackControl => false; + protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty; protected bool HasUnsavedChanges diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e2e8b71c10..956c4d4856 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -71,6 +71,8 @@ namespace osu.Game.Screens.Play // We are managing our own adjustments (see OnEntering/OnExiting). public override bool? ApplyModTrackAdjustments => false; + public override bool? AllowGlobalTrackControl => false; + private readonly IBindable gameActive = new Bindable(true); private readonly Bindable samplePlaybackDisabled = new Bindable(); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 96fed3e6ba..43ddcffb62 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Ranking public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => true; + // Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently. public override bool HideOverlaysOnEnter => true; From 39c2bb240bbd53c1d241313d6160be000fd9a205 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 14:03:12 +0900 Subject: [PATCH 13/71] Apply NRT to `SelectionBoxRotationHandle`. --- .../Compose/Components/SelectionBoxRotationHandle.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 4107a09692..8665ec9b08 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -16,25 +13,24 @@ using osu.Framework.Localisation; using osu.Game.Localisation; using osuTK; using osuTK.Graphics; -using Key = osuTK.Input.Key; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - [CanBeNull] - public SelectionRotationHandler RotationHandler { get; init; } + public SelectionRotationHandler? RotationHandler { get; init; } public LocalisableString TooltipText { get; private set; } - private SpriteIcon icon; + private SpriteIcon icon = null!; private const float snap_step = 15; private readonly Bindable cumulativeRotation = new Bindable(); [Resolved] - private SelectionBox selectionBox { get; set; } + private SelectionBox selectionBox { get; set; } = null!; [BackgroundDependencyLoader] private void load() From 7fd6bb9d5f98c6ba74b06f827292a5cab875d322 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 14:04:16 +0900 Subject: [PATCH 14/71] Fix a couple of code style issues in `SelectionBox` --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 53442071b5..ed6bbf7668 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private IBindable canRotate = new BindableBool(); + private readonly IBindable canRotate = new BindableBool(); private bool canScaleX; @@ -152,8 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (RotationHandler != null) canRotate.BindTo(RotationHandler.CanRotate); - canRotate.BindValueChanged(_ => recreate()); - recreate(); + canRotate.BindValueChanged(_ => recreate(), true); } protected override bool OnKeyDown(KeyDownEvent e) From deba6e2508359d484b63edf51236915681e69a20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jul 2023 02:22:46 +0900 Subject: [PATCH 15/71] Fix osu!taiko editor playfield missing a piece Regressed with recent centering changes in https://github.com/ppy/osu/pull/24220 --- osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs | 2 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cff5731181..add10b0ac5 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoHitObjectComposer : HitObjectComposer { + protected override bool ApplyVerticalCentering => false; + public TaikoHitObjectComposer(TaikoRuleset ruleset) : base(ruleset) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index bad7c55883..6fbc39c8ae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -44,6 +44,11 @@ namespace osu.Game.Rulesets.Edit public abstract partial class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject { + /// + /// Whether the playfield should be centered vertically. Should be disabled for playfields which span the full horizontal width. + /// + protected virtual bool ApplyVerticalCentering => true; + protected IRulesetConfigManager Config { get; private set; } // Provides `Playfield` @@ -242,7 +247,7 @@ namespace osu.Game.Rulesets.Edit base.Update(); // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. - PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (ApplyVerticalCentering ? TOOLBOX_CONTRACTED_SIZE_RIGHT : TOOLBOX_CONTRACTED_SIZE_LEFT) * 2; } public override Playfield Playfield => drawableRulesetWrapper.Playfield; From 269d4d1cd6eda979cb0aa1bf42fc802e247e6886 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 14:49:05 +0900 Subject: [PATCH 16/71] Add test coverage of autoplay restore not working --- .../Navigation/TestScenePerformFromScreen.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 1c8fa775b9..c8f1f081d9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -86,6 +87,29 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("did perform", () => actionPerformed); } + [Test] + public void TestPerformAtMenuFromPlayerLoaderWithAutoplayShortcut() + { + importAndWaitForSongSelect(); + + AddStep("press ctrl+enter", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader); + + AddAssert("Mods include autoplay", () => Game.SelectedMods.Value.Any(m => m is ModAutoplay)); + + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("did perform", () => actionPerformed); + + AddAssert("Mods don't include autoplay", () => !Game.SelectedMods.Value.Any(m => m is ModAutoplay)); + } + [Test] public void TestPerformEnsuresScreenIsLoaded() { From f15394fb6d5975cb599102f7718b496997b11048 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 14:42:43 +0900 Subject: [PATCH 17/71] Fix temporary auto mod (ctrl+enter at song select) not reverting in all scenarios --- osu.Game/Screens/Select/PlaySongSelect.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index b99d949b43..fe13d6d5a8 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -146,12 +146,24 @@ namespace osu.Game.Screens.Select public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + revertMods(); + } - if (playerLoader != null) - { - Mods.Value = modsAtGameplayStart; - playerLoader = null; - } + public override bool OnExiting(ScreenExitEvent e) + { + if (base.OnExiting(e)) + return true; + + revertMods(); + return false; + } + + private void revertMods() + { + if (playerLoader == null) return; + + Mods.Value = modsAtGameplayStart; + playerLoader = null; } } } From 06fe5583cb9ecd4915aad3835b09ae732dfc1aff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 15:47:57 +0900 Subject: [PATCH 18/71] Expose a new SSDQ from playfield for skinnable area bounds --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 17 +++++++++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index e3ebadc836..7b00447238 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; @@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI private readonly List stages = new List(); + public override Quad SkinnableComponentScreenSpaceDrawQuad + { + get + { + if (Stages.Count == 1) + return Stages.First().ScreenSpaceDrawQuad; + + RectangleF area = RectangleF.Empty; + + foreach (var stage in Stages) + area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat); + + return area; + } + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); public ManiaPlayfield(List stageDefinitions) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 3f263aba63..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -23,6 +23,7 @@ using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Objects.Pooling; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Primitives; namespace osu.Game.Rulesets.UI { @@ -94,6 +95,16 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + /// + /// A screen space draw quad which resembles the edges of the playfield for skinning purposes. + /// This will allow users / components to snap objects to the "edge" of the playfield. + /// + /// + /// Rulesets which reduce the visible area further than the full relative playfield space itself + /// should retarget this to the ScreenSpaceDrawQuad of the appropriate container. + /// + public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad; + [Resolved(CanBeNull = true)] [CanBeNull] protected IReadOnlyList Mods { get; private set; } From 5bd06832d0a4e35bbbd52eea2ffb3970764e6d0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 15:48:40 +0900 Subject: [PATCH 19/71] Fix skin component toolbox not working correctly for ruleset matching Until now, the only usage of ruleset layers was where there is both a ruleset specific and non-ruleset-specific layer present. The matching code was making assumptions about this. As I tried to add a new playfield layer which breaks this assumption, non-ruleset-specifc components were not being displayed in the toolbox. This turned out to be due to a `target` of `null` being provided due to the weird `getTarget` matching (that happened to *just* do what we wanted previously due to the equals implementation, but only because there was a container without the ruleset present in the available targets). I've changed this to be a more appropriate lookup method, where the target for dependency sourcing is provided separately from the ruleset filter. --- .../Overlays/SkinEditor/SkinComponentToolbox.cs | 17 +++++++++++++---- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index 1ce253d67c..a476fc1a6d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -13,6 +13,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Rulesets; using osu.Game.Screens.Edit.Components; using osu.Game.Skinning; using osuTK; @@ -23,14 +24,22 @@ namespace osu.Game.Overlays.SkinEditor { public Action? RequestPlacement; - private readonly SkinComponentsContainer? target; + private readonly SkinComponentsContainer target; + + private readonly RulesetInfo? ruleset; private FillFlowContainer fill = null!; - public SkinComponentToolbox(SkinComponentsContainer? target = null) - : base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})")) + /// + /// Create a new component toolbox for the specified taget. + /// + /// The target. This is mainly used as a dependency source to find candidate components. + /// A ruleset to filter components by. If null, only components which are not ruleset-specific will be included. + public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset) + : base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})")) { this.target = target; + this.ruleset = ruleset; } [BackgroundDependencyLoader] @@ -51,7 +60,7 @@ namespace osu.Game.Overlays.SkinEditor { fill.Clear(); - var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset); + var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(ruleset); foreach (var type in skinnableTypes) attemptAddComponent(type); } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 2b23ce290f..67cf0eab18 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -366,14 +366,14 @@ namespace osu.Game.Overlays.SkinEditor // If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below. if (target.NewValue.Ruleset != null) { - componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer) + componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, target.NewValue.Ruleset) { RequestPlacement = requestPlacement }); } // Remove the ruleset from the lookup to get base components. - componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target))) + componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, null) { RequestPlacement = requestPlacement }); From 6cf065f6d1c92ddcdf36efa97fbe4634ee3cd9f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 15:48:21 +0900 Subject: [PATCH 20/71] Add playfield layer to skin editor --- osu.Game/Screens/Play/HUDOverlay.cs | 14 ++++++++++++-- osu.Game/Skinning/SkinComponentsContainerLookup.cs | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d11171e3fe..6696332307 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -103,10 +104,11 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; + private readonly Drawable playfieldComponents; + public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { Drawable rulesetComponents; - this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -123,6 +125,9 @@ namespace osu.Game.Screens.Play rulesetComponents = drawableRuleset != null ? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, } : Empty(), + playfieldComponents = drawableRuleset != null + ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } + : Empty(), topRightElements = new FillFlowContainer { Anchor = Anchor.TopRight, @@ -162,7 +167,7 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, rulesetComponents, topRightElements }; + hideTargets = new List { mainComponents, rulesetComponents, playfieldComponents, topRightElements }; if (!alwaysShowLeaderboard) hideTargets.Add(LeaderboardFlow); @@ -230,6 +235,11 @@ namespace osu.Game.Screens.Play { base.Update(); + Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; + + playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + playfieldComponents.Size = ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + float? lowestTopScreenSpaceLeft = null; float? lowestTopScreenSpaceRight = null; diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/SkinComponentsContainerLookup.cs index fbc0ab58ad..34358c3f06 100644 --- a/osu.Game/Skinning/SkinComponentsContainerLookup.cs +++ b/osu.Game/Skinning/SkinComponentsContainerLookup.cs @@ -68,7 +68,10 @@ namespace osu.Game.Skinning MainHUDComponents, [Description("Song select")] - SongSelect + SongSelect, + + [Description("Playfield")] + Playfield } } } From 0e4db9b4390198a4619ac04399960bedcedafd14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 16:25:40 +0900 Subject: [PATCH 21/71] Add safety in `RectangularPositionSnapGrid` that size is greater than zero Would crash otherwise --- .../Compose/Components/RectangularPositionSnapGrid.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 063ea23281..bec1d148be 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -54,8 +54,12 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!gridCache.IsValid) { ClearInternal(); - createContent(); - gridCache.Validate(); + + if (DrawSize != Vector2.Zero) + { + createContent(); + gridCache.Validate(); + } } } From b5f0d739e63ce9324baa689701b0227b873f5b97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 16:25:52 +0900 Subject: [PATCH 22/71] Allow ladder editor grid to scale with content Closes https://github.com/ppy/osu/issues/24378. --- .../Screens/Editors/LadderEditorScreen.cs | 16 ++++++++++++++-- .../Screens/Ladder/LadderScreen.cs | 7 +++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 62d18ac9e5..f3c5566565 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tournament.Screens.Editors private WarningBox rightClickMessage; + private RectangularPositionSnapGrid grid; + [Resolved(canBeNull: true)] [CanBeNull] private IDialogOverlay dialogOverlay { get; set; } @@ -53,10 +55,12 @@ namespace osu.Game.Tournament.Screens.Editors AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); - ScrollContent.Add(new RectangularPositionSnapGrid(Vector2.Zero) + ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero) { Spacing = new Vector2(GRID_SPACING), - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, Depth = float.MaxValue }); @@ -64,6 +68,14 @@ namespace osu.Game.Tournament.Screens.Editors updateMessage(); } + protected override void Update() + { + base.Update(); + + // Expand grid with the content to allow going beyond the bounds of the screen. + grid.Size = ScrollContent.Size + new Vector2(GRID_SPACING * 2); + } + private void updateMessage() { rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index a74c9a9429..2d5281b893 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -57,12 +57,15 @@ namespace osu.Game.Tournament.Screens.Ladder }, ScrollContent = new LadderDragContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { paths = new Container { RelativeSizeAxes = Axes.Both }, headings = new Container { RelativeSizeAxes = Axes.Both }, - MatchesContainer = new Container { RelativeSizeAxes = Axes.Both }, + MatchesContainer = new Container + { + AutoSizeAxes = Axes.Both + }, } }, } From b5c3e2a648e78a62729033b8229f5360f1d176dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 16:29:39 +0900 Subject: [PATCH 23/71] Fix placing new match via right click not using original click position --- .../Screens/Editors/LadderEditorScreen.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index f3c5566565..2ff86ab17c 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -76,6 +76,14 @@ namespace osu.Game.Tournament.Screens.Editors grid.Size = ScrollContent.Size + new Vector2(GRID_SPACING * 2); } + private Vector2 lastMatchesContainerMouseDownPosition; + + protected override bool OnMouseDown(MouseDownEvent e) + { + lastMatchesContainerMouseDownPosition = MatchesContainer.ToLocalSpace(e.ScreenSpaceMouseDownPosition); + return base.OnMouseDown(e); + } + private void updateMessage() { rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; @@ -97,7 +105,8 @@ namespace osu.Game.Tournament.Screens.Editors { new OsuMenuItem("Create new match", MenuItemType.Highlighted, () => { - Vector2 pos = MatchesContainer.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position); + Vector2 pos = lastMatchesContainerMouseDownPosition; + TournamentMatch newMatch = new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } }; LadderInfo.Matches.Add(newMatch); From aa910005053d3ff51ba124cf07480e16b6642072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 16:29:55 +0900 Subject: [PATCH 24/71] Always place first match at (0,0) --- osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 2ff86ab17c..9411892dc5 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Screens.Editors { new OsuMenuItem("Create new match", MenuItemType.Highlighted, () => { - Vector2 pos = lastMatchesContainerMouseDownPosition; + Vector2 pos = MatchesContainer.Count == 0 ? Vector2.Zero : lastMatchesContainerMouseDownPosition; TournamentMatch newMatch = new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } }; From c9155f85ab83d12771621dc82240320f2f09ec19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 16:40:14 +0900 Subject: [PATCH 25/71] Fix playfield not taking up full width correclty when not vertically centered --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 6fbc39c8ae..dbca8694ae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -124,8 +124,6 @@ namespace osu.Game.Rulesets.Edit { Name = "Playfield content", RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Children = new Drawable[] { // layers below playfield @@ -247,7 +245,22 @@ namespace osu.Game.Rulesets.Edit base.Update(); // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. - PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (ApplyVerticalCentering ? TOOLBOX_CONTRACTED_SIZE_RIGHT : TOOLBOX_CONTRACTED_SIZE_LEFT) * 2; + if (ApplyVerticalCentering) + { + PlayfieldContentContainer.Anchor = Anchor.Centre; + PlayfieldContentContainer.Origin = Anchor.Centre; + + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; + PlayfieldContentContainer.X = 0; + } + else + { + PlayfieldContentContainer.Anchor = Anchor.CentreLeft; + PlayfieldContentContainer.Origin = Anchor.CentreLeft; + + PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT); + PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT; + } } public override Playfield Playfield => drawableRulesetWrapper.Playfield; From f58c69e639b1bc5f72c4680f48d90cedb0d7a718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jul 2023 17:17:14 +0900 Subject: [PATCH 26/71] Fix potential startup crash due to early application of animations This was always haphazard code, but by luck it never triggered before drawable load until now. With the recently nullability changes, this would be triggered when `flash` is not yet constructed. Switching to `AddOnce` seems safer to avoid multiple applications, regardless. --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 6933eafc29..49478a2174 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -136,11 +136,11 @@ namespace osu.Game.Tournament.Components if (match.NewValue != null) match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged; - updateState(); + Scheduler.AddOnce(updateState); } private void picksBansOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - => updateState(); + => Scheduler.AddOnce(updateState); private BeatmapChoice? choice; From 6d018c08afc1573a921e3cfb28e1e13b67f45cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jul 2023 22:09:28 +0200 Subject: [PATCH 27/71] Rename `Apply{Vertical -> Horizontal}Centering` to match common understanding --- osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index add10b0ac5..3e63d624e7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoHitObjectComposer : HitObjectComposer { - protected override bool ApplyVerticalCentering => false; + protected override bool ApplyHorizontalCentering => false; public TaikoHitObjectComposer(TaikoRuleset ruleset) : base(ruleset) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index dbca8694ae..71ca13e4ba 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Edit where TObject : HitObject { /// - /// Whether the playfield should be centered vertically. Should be disabled for playfields which span the full horizontal width. + /// Whether the playfield should be centered horizontally. Should be disabled for playfields which span the full horizontal width. /// - protected virtual bool ApplyVerticalCentering => true; + protected virtual bool ApplyHorizontalCentering => true; protected IRulesetConfigManager Config { get; private set; } @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Edit base.Update(); // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. - if (ApplyVerticalCentering) + if (ApplyHorizontalCentering) { PlayfieldContentContainer.Anchor = Anchor.Centre; PlayfieldContentContainer.Origin = Anchor.Centre; From caad931a16c3712130f01b1bc54a3c58a3728c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jul 2023 22:10:10 +0200 Subject: [PATCH 28/71] Move comment to more fitting place --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 71ca13e4ba..c967187b5c 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -244,12 +244,12 @@ namespace osu.Game.Rulesets.Edit { base.Update(); - // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. if (ApplyHorizontalCentering) { PlayfieldContentContainer.Anchor = Anchor.Centre; PlayfieldContentContainer.Origin = Anchor.Centre; + // Ensure that the playfield is always centered but also doesn't get cut off by toolboxes. PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2; PlayfieldContentContainer.X = 0; } From c6f0cf50caa0a310ff9dc3f1533302cce269ea90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jul 2023 22:57:02 +0200 Subject: [PATCH 29/71] Use better safety in rectangular grid - Checking `DrawSize != Vector2.Zero` is too specific. It could also crash on zero-height-but-non-zero-width, or zero-width-but-non-zero-height. - Take the `gridCache.Validate()` call out of the zero checks, because even if the width or height are zero, not generating anything is valid and there is no reason to validate every frame until `gridCache` gets invalidated again. --- .../Edit/Compose/Components/RectangularPositionSnapGrid.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index bec1d148be..cfc01fe17b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -55,11 +55,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { ClearInternal(); - if (DrawSize != Vector2.Zero) - { + if (DrawWidth > 0 && DrawHeight > 0) createContent(); - gridCache.Validate(); - } + + gridCache.Validate(); } } From aca8310cd1df87a131e1e1dfc7d0bc047101c578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jul 2023 23:36:57 +0200 Subject: [PATCH 30/71] Fix non-compiling test To be fair, currently the test is a bit pointless (as it has no reason to be a `SkinnableTestScene`, it gains precisely nothing from it - all that is shown there is some generic components on song select). But that is no worse then `master`, so look away for now. --- .../Visual/Gameplay/TestSceneSkinEditorComponentsList.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index 515bc09bbb..b7b2a6c175 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -8,6 +8,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay { @@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox + var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)); + + AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, From cd416e09f9cb0e7110c79bf84c4a99c17c5dee0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Jul 2023 00:36:27 +0200 Subject: [PATCH 31/71] Add test scene for checking spinner judgements --- .../TestSceneSpinnerJudgement.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs new file mode 100644 index 0000000000..c969cb11b4 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -0,0 +1,147 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene + { + private const double time_spinner_start = 2000; + private const double time_spinner_end = 4000; + + private List judgementResults = new List(); + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + [Test] + public void TestHitNothing() + { + performTest(new List()); + + AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult)); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public void TestNumberOfSpins(int spins) + { + performTest(generateReplay(spins)); + + for (int i = 0; i < spins; ++i) + assertResult(i, HitResult.SmallBonus); + + assertResult(spins, HitResult.IgnoreMiss); + } + + [Test] + public void TestHitEverything() + { + performTest(generateReplay(20)); + + AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); + } + + private static List generateReplay(int spins) + { + var replayFrames = new List(); + + const int frames_per_spin = 30; + + for (int i = 0; i < spins * frames_per_spin; ++i) + { + float totalProgress = i / (float)(spins * frames_per_spin); + float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; + double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; + float posX = MathF.Cos(2 * MathF.PI * spinProgress); + float posY = MathF.Sin(2 * MathF.PI * spinProgress); + Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; + + replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); + } + + return replayFrames; + } + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private void assertResult(int index, HitResult expectedResult) + { + AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}", + () => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type, + () => Is.EqualTo(expectedResult)); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} From 1fd4a6dc96fe5b87ee9c616a2518775a731a765e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Jul 2023 01:07:08 +0200 Subject: [PATCH 32/71] Fix tests crashing due to `HUDOverlay` not finding `DrawableRuleset` in `Update()` --- osu.Game/Screens/Play/HUDOverlay.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 6696332307..536d3ac146 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -70,7 +70,9 @@ namespace osu.Game.Screens.Play public Bindable ShowHealthBar = new Bindable(true); + [CanBeNull] private readonly DrawableRuleset drawableRuleset; + private readonly IReadOnlyList mods; /// @@ -106,7 +108,7 @@ namespace osu.Game.Screens.Play private readonly Drawable playfieldComponents; - public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) + public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { Drawable rulesetComponents; this.drawableRuleset = drawableRuleset; @@ -235,10 +237,13 @@ namespace osu.Game.Screens.Play { base.Update(); - Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; + if (drawableRuleset != null) + { + Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; - playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); - playfieldComponents.Size = ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + playfieldComponents.Size = ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + } float? lowestTopScreenSpaceLeft = null; float? lowestTopScreenSpaceRight = null; From 43a51671ac83a9f548577037cbeb68827af192a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Jul 2023 16:12:02 +0200 Subject: [PATCH 33/71] Fix wrong test step name --- osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index c8f1f081d9..5fe4bb9340 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Mods include autoplay", () => Game.SelectedMods.Value.Any(m => m is ModAutoplay)); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); - AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddUntilStep("returned to main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddAssert("did perform", () => actionPerformed); AddAssert("Mods don't include autoplay", () => !Game.SelectedMods.Value.Any(m => m is ModAutoplay)); From 5fa0a21b564890637beda25061c454573317ae9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 18:14:20 +0900 Subject: [PATCH 34/71] Add corner radius around player areas --- osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index dc4a2df9d8..54f03569de 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -68,6 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate RelativeSizeAxes = Axes.Both; Masking = true; + CornerRadius = 5; AudioContainer audioContainer; InternalChildren = new Drawable[] From 1826819663776a737ab77ad2c1265266067cea2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 18:56:29 +0900 Subject: [PATCH 35/71] Move `Facade` to nested class --- .../Multiplayer/Spectate/PlayerGrid.cs | 12 ++++++++++ .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 22 ------------------- 2 files changed, 12 insertions(+), 22 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 771a8c0de4..87072e8d9e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -169,5 +169,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate foreach (var cell in facadeContainer) cell.Size = cellSize; } + + /// + /// A facade of the grid which is used as a dummy object to store the required position/size of cells. + /// + public partial class Facade : Drawable + { + public Facade() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs deleted file mode 100644 index 934c22c918..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public partial class PlayerGrid - { - /// - /// A facade of the grid which is used as a dummy object to store the required position/size of cells. - /// - private partial class Facade : Drawable - { - public Facade() - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - } - } - } -} From 84bc14c1ddc1e878a0245fce39aa1252f2c398cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 18:56:41 +0900 Subject: [PATCH 36/71] Improve animation and sizing of maximised screen display --- .../Multiplayer/Spectate/PlayerGrid.cs | 42 +++++++++------ .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 52 +++++++++---------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index 87072e8d9e..c88feb12bd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -15,20 +16,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public partial class PlayerGrid : CompositeDrawable { + public const float ANIMATION_DELAY = 400; + /// /// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen. /// Todo: Can be removed in the future with scrolling support + performance improvements. /// public const int MAX_PLAYERS = 16; - private const float player_spacing = 5; + private const float player_spacing = 6; /// /// The currently-maximised facade. /// - public Drawable MaximisedFacade => maximisedFacade; + public Facade MaximisedFacade { get; } - private readonly Facade maximisedFacade; private readonly Container paddingContainer; private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; @@ -48,12 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate RelativeSizeAxes = Axes.Both, Child = facadeContainer = new FillFlowContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(player_spacing), } }, - maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both } + MaximisedFacade = new Facade + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + } } }, cellContainer = new Container { RelativeSizeAxes = Axes.Both } @@ -91,26 +99,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { - // Iterate through all cells to ensure only one is maximised at any time. - foreach (var i in cellContainer.ToList()) - { - if (i == target) - i.IsMaximised = !i.IsMaximised; - else - i.IsMaximised = false; + bool anyMaximised = target.IsMaximised = !target.IsMaximised; - if (i.IsMaximised) + // Iterate through all cells to ensure only one is maximised at any time. + foreach (var cell in cellContainer.ToList()) + { + if (cell != target) + cell.IsMaximised = false; + + if (cell.IsMaximised) { // Transfer cell to the maximised facade. - i.SetFacade(maximisedFacade); - cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f); + cell.SetFacade(MaximisedFacade); + cellContainer.ChangeChildDepth(cell, maximisedInstanceDepth -= 0.001f); } else { // Transfer cell back to its original facade. - i.SetFacade(facadeContainer[i.FacadeIndex]); + cell.SetFacade(facadeContainer[cell.FacadeIndex]); } + + cell.FadeColour(anyMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.Out); } + + facadeContainer.ScaleTo(anyMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint); } protected override void Update() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 4a8b8f49e1..d11e79d820 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -40,7 +41,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public bool IsMaximised; private Facade facade; - private bool isTracking = true; + + private bool isAnimating; public Cell(int facadeIndex, Drawable content) { @@ -54,11 +56,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); - if (isTracking) - { - Position = getFinalPosition(); - Size = getFinalSize(); - } + var targetPos = getFinalPosition(); + var targetSize = getFinalSize(); + + double duration = isAnimating ? 60 : 0; + + Position = new Vector2( + (float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + + Size = new Vector2( + (float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed) + ); + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimating &= !Precision.AlmostEquals(Position, targetPos, 0.01f); } /// @@ -66,30 +80,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public void SetFacade([NotNull] Facade newFacade) { - Facade lastFacade = facade; facade = newFacade; - - if (lastFacade == null || lastFacade == newFacade) - return; - - isTracking = false; - - this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint) - .Then() - .OnComplete(_ => - { - if (facade == newFacade) - isTracking = true; - }); + isAnimating = true; } - private Vector2 getFinalPosition() - { - var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero)); - return topLeft + facade.DrawSize / 2; - } + private Vector2 getFinalPosition() => + Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre); - private Vector2 getFinalSize() => facade.DrawSize; + private Vector2 getFinalSize() => + Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight) + - Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft); protected override bool OnClick(ClickEvent e) { From 38244c081ff942940a95df3b6a74f27bb3c24368 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 19:08:46 +0900 Subject: [PATCH 37/71] Further refactorings along with shadow implementation --- .../Multiplayer/Spectate/PlayerArea.cs | 2 -- .../Multiplayer/Spectate/PlayerGrid.cs | 19 ++++++-------- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 26 +++++++++++++------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 54f03569de..1b03452df7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -67,8 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate SpectatorPlayerClock = clock; RelativeSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 5; AudioContainer audioContainer; InternalChildren = new Drawable[] diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index c88feb12bd..c162b87727 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -83,8 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var facade = new Facade(); facadeContainer.Add(facade); - var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState }; - cell.SetFacade(facade); + var cell = new Cell(index, content, facade) { ToggleMaximisationState = toggleMaximisationState }; cellContainer.Add(cell); } @@ -99,30 +98,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { - bool anyMaximised = target.IsMaximised = !target.IsMaximised; + // in the case the target is the already maximised cell, no cell should be maximised. + bool hasMaximised = !target.IsMaximised; // Iterate through all cells to ensure only one is maximised at any time. foreach (var cell in cellContainer.ToList()) { - if (cell != target) - cell.IsMaximised = false; - - if (cell.IsMaximised) + if (hasMaximised && cell == target) { // Transfer cell to the maximised facade. - cell.SetFacade(MaximisedFacade); + cell.SetFacade(MaximisedFacade, true); cellContainer.ChangeChildDepth(cell, maximisedInstanceDepth -= 0.001f); } else { // Transfer cell back to its original facade. - cell.SetFacade(facadeContainer[cell.FacadeIndex]); + cell.SetFacade(facadeContainer[cell.FacadeIndex], false); } - cell.FadeColour(anyMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.Out); + cell.FadeColour(hasMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.OutQuint); } - facadeContainer.ScaleTo(anyMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint); + facadeContainer.ScaleTo(hasMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint); } protected override void Update() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index d11e79d820..4e624da17f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Input.Events; using osu.Framework.Utils; using osuTK; @@ -33,23 +31,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// An action that toggles the maximisation state of this cell. /// - public Action ToggleMaximisationState; + public Action? ToggleMaximisationState; /// /// Whether this cell is currently maximised. /// - public bool IsMaximised; + public bool IsMaximised { get; private set; } private Facade facade; private bool isAnimating; - public Cell(int facadeIndex, Drawable content) + public Cell(int facadeIndex, Drawable content, Facade facade) { FacadeIndex = facadeIndex; + this.facade = facade; Origin = Anchor.Centre; InternalChild = Content = content; + + Masking = true; + CornerRadius = 5; } protected override void Update() @@ -78,10 +80,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Makes this cell track a new facade. /// - public void SetFacade([NotNull] Facade newFacade) + public void SetFacade(Facade newFacade, bool isMaximised) { facade = newFacade; + IsMaximised = isMaximised; isAnimating = true; + + TweenEdgeEffectTo(new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = isMaximised ? 30 : 10, + Colour = Colour4.Black.Opacity(isMaximised ? 0.5f : 0.2f), + }, ANIMATION_DELAY, Easing.OutQuint); } private Vector2 getFinalPosition() => @@ -93,7 +103,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override bool OnClick(ClickEvent e) { - ToggleMaximisationState(this); + ToggleMaximisationState?.Invoke(this); return true; } } From 75625f089e1f8f9957bf6a7fb52bc48f552f38d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jul 2023 19:22:21 +0900 Subject: [PATCH 38/71] Hide toolbar when entering multiplayer spectator --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2d2aa0f1d5..58eed7e85c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // We are managing our own adjustments. For now, this happens inside the Player instances themselves. public override bool? AllowTrackAdjustments => false; + public override bool HideOverlaysOnEnter => true; + /// /// Whether all spectating players have finished loading. /// From 945d89e955cb350b55b383a5cd5f90dda72ad10c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 13:45:42 +0900 Subject: [PATCH 39/71] Move disables to loading screens for better coverage of edge cases --- osu.Game/Screens/Edit/Editor.cs | 2 -- osu.Game/Screens/Edit/EditorLoader.cs | 2 ++ osu.Game/Screens/Play/Player.cs | 2 -- osu.Game/Screens/Play/PlayerLoader.cs | 2 ++ 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d11297b3b2..b885eee46f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -68,8 +68,6 @@ namespace osu.Game.Screens.Edit public override bool? ApplyModTrackAdjustments => false; - public override bool? AllowGlobalTrackControl => false; - protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty; protected bool HasUnsavedChanges diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index f665b7c511..8bcfa7b9f0 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -42,6 +42,8 @@ namespace osu.Game.Screens.Edit public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => false; + [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 956c4d4856..e2e8b71c10 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -71,8 +71,6 @@ namespace osu.Game.Screens.Play // We are managing our own adjustments (see OnEntering/OnExiting). public override bool? ApplyModTrackAdjustments => false; - public override bool? AllowGlobalTrackControl => false; - private readonly IBindable gameActive = new Bindable(true); private readonly Bindable samplePlaybackDisabled = new Bindable(); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4b15bac0f3..872425e3fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -46,6 +46,8 @@ namespace osu.Game.Screens.Play public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? AllowGlobalTrackControl => false; + // Here because IsHovered will not update unless we do so. public override bool HandlePositionalInput => true; From 713829163689bea7bd053a62d5edcbc30e12097e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 13:52:58 +0900 Subject: [PATCH 40/71] Adjust xmldoc to explicitly metnion it only affects end user control --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 665c61edf0..0986c0513c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays public bool UserPauseRequested { get; private set; } /// - /// Whether control of the global track should be allowed. + /// Whether user control of the global track should be allowed. /// public readonly BindableBool AllowTrackControl = new BindableBool(true); From feea412bec6d4d015a2a4fdcc9fd373ddee817ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 14:40:00 +0900 Subject: [PATCH 41/71] Add test with only one player --- .../TestSceneMultiSpectatorScreen.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index e09496b6e9..e81dc87d4f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -65,6 +65,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear playing users", () => playingUsers.Clear()); } + [TestCase(1)] + [TestCase(4)] + public void TestGeneral(int count) + { + int[] userIds = getPlayerIds(count); + + start(userIds); + loadSpectateScreen(); + + sendFrames(userIds, 1000); + AddWaitStep("wait a bit", 20); + } + [Test] public void TestDelayedStart() { @@ -88,18 +101,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } - [Test] - public void TestGeneral() - { - int[] userIds = getPlayerIds(4); - - start(userIds); - loadSpectateScreen(); - - sendFrames(userIds, 1000); - AddWaitStep("wait a bit", 20); - } - [Test] public void TestSpectatorPlayerInteractiveElementsHidden() { From 45ceaba00d83027f679a3130bd0c269ebb24f790 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 14:40:58 +0900 Subject: [PATCH 42/71] Disable multiplayer spectator zoom when there's only one player's screen visible --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index c162b87727..6e71c010e5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -98,8 +98,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void toggleMaximisationState(Cell target) { - // in the case the target is the already maximised cell, no cell should be maximised. - bool hasMaximised = !target.IsMaximised; + // in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised. + bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1; // Iterate through all cells to ensure only one is maximised at any time. foreach (var cell in cellContainer.ToList()) From 4f83c8661a67685f4897e99679d987f416104437 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 16:12:55 +0900 Subject: [PATCH 43/71] Remove unnecessary async fetch of beatmap in `NowPlayingOverlay` No idea if this was historically required for some reason, but it's definitely not required now. --- osu.Game/Overlays/NowPlayingOverlay.cs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 15eefb2d9f..f6fad0ef6c 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -293,21 +292,10 @@ namespace osu.Game.Overlays // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. pendingBeatmapSwitch = delegate { - // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() - Task.Run(() => - { - if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists - { - title.Text = @"Nothing to play"; - artist.Text = @"Nothing to play"; - } - else - { - BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); - artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - } - }); + BeatmapMetadata metadata = beatmap.Metadata; + + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => { From 07d224ecb6bfab5f72bb8f79210a8f43bd9a39e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 16:17:04 +0900 Subject: [PATCH 44/71] Apply NRT to `NowPlayingOverlay` --- osu.Game/Overlays/NowPlayingOverlay.cs | 39 +++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index f6fad0ef6c..37cb85983c 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -1,12 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -39,33 +38,33 @@ namespace osu.Game.Overlays private const float bottom_black_area_height = 55; private const float margin = 10; - private Drawable background; - private ProgressBar progressBar; + private Drawable background = null!; + private ProgressBar progressBar = null!; - private IconButton prevButton; - private IconButton playButton; - private IconButton nextButton; - private IconButton playlistButton; + private IconButton prevButton = null!; + private IconButton playButton = null!; + private IconButton nextButton = null!; + private IconButton playlistButton = null!; - private SpriteText title, artist; + private SpriteText title = null!, artist = null!; - private PlaylistOverlay playlist; + private PlaylistOverlay? playlist; - private Container dragContainer; - private Container playerContainer; - private Container playlistContainer; + private Container dragContainer = null!; + private Container playerContainer = null!; + private Container playlistContainer = null!; protected override string PopInSampleName => "UI/now-playing-pop-in"; protected override string PopOutSampleName => "UI/now-playing-pop-out"; [Resolved] - private MusicController musicController { get; set; } + private MusicController musicController { get; set; } = null!; [Resolved] - private Bindable beatmap { get; set; } + private Bindable beatmap { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; public NowPlayingOverlay() { @@ -285,7 +284,7 @@ namespace osu.Game.Overlays } } - private Action pendingBeatmapSwitch; + private Action? pendingBeatmapSwitch; private void trackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction = TrackChangeDirection.None) { @@ -338,7 +337,7 @@ namespace osu.Game.Overlays { base.Dispose(isDisposing); - if (musicController != null) + if (musicController.IsNotNull()) musicController.TrackChanged -= trackChanged; } @@ -371,7 +370,7 @@ namespace osu.Game.Overlays private readonly Sprite sprite; private readonly WorkingBeatmap beatmap; - public Background(WorkingBeatmap beatmap = null) + public Background(WorkingBeatmap beatmap) : base(cachedFrameBuffer: true) { this.beatmap = beatmap; @@ -401,7 +400,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4"); + sprite.Texture = beatmap.GetBackground() ?? textures.Get(@"Backgrounds/bg4"); } } From de61b74e910d2342a6eeafac5a804796fe8418aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 16:21:35 +0900 Subject: [PATCH 45/71] Add proper cancellation and out-of-order blocking logic to `NowPlayingOverlay`'s background carousel --- osu.Game/Overlays/NowPlayingOverlay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 37cb85983c..89442e29bc 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -286,8 +287,14 @@ namespace osu.Game.Overlays private Action? pendingBeatmapSwitch; + private CancellationTokenSource? backgroundLoadCancellation; + + private WorkingBeatmap? currentBeatmap; + private void trackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction = TrackChangeDirection.None) { + currentBeatmap = beatmap; + // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. pendingBeatmapSwitch = delegate { @@ -296,8 +303,16 @@ namespace osu.Game.Overlays title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + backgroundLoadCancellation?.Cancel(); + LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => { + if (beatmap != currentBeatmap) + { + newBackground.Dispose(); + return; + } + switch (direction) { case TrackChangeDirection.Next: @@ -317,7 +332,7 @@ namespace osu.Game.Overlays background = newBackground; playerContainer.Add(newBackground); - }); + }, (backgroundLoadCancellation = new CancellationTokenSource()).Token); }; } From 35ec55c1f6df3d9ded611c6cd8afb3f696c101ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 16:41:33 +0900 Subject: [PATCH 46/71] Don't queue export replay operations if button is disabled --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index a5b33e584d..aa734d2077 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -117,11 +117,17 @@ namespace osu.Game.Screens.Ranking return true; case GlobalAction.ExportReplay: - State.BindValueChanged(exportWhenReady, true); - - // start the import via button - if (State.Value != DownloadState.LocallyAvailable) + if (State.Value == DownloadState.LocallyAvailable) + { + State.BindValueChanged(exportWhenReady, true); + } + else + { + // A download needs to be performed before we can export this replay. button.TriggerClick(); + if (button.Enabled.Value) + State.BindValueChanged(exportWhenReady, true); + } return true; } From 6d5b3617b3cf8af702573d0443c7f0da276834c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jul 2023 16:41:45 +0900 Subject: [PATCH 47/71] Remove pending export operation if active score is changed --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index aa734d2077..b6166e97f6 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -83,6 +83,10 @@ namespace osu.Game.Screens.Ranking Score.BindValueChanged(score => { + // An export may be pending from the last score. + // Reset this to meet user expectations (a new score which has just been switched to shouldn't export) + State.ValueChanged -= exportWhenReady; + downloadTracker?.RemoveAndDisposeImmediately(); if (score.NewValue != null) From 1981e49a40723f16476fae208a28c693fca03c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 30 Jul 2023 14:28:16 +0200 Subject: [PATCH 48/71] Fix nullability inspection --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 4132db42bc..6abde713b6 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays [Resolved] private OsuColour colours { get; set; } = null!; - private Bindable allowTrackControl; + private Bindable allowTrackControl = null!; public NowPlayingOverlay() { From d3435483eba7d8cd2dc113e1deeeb5ce2a13779b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 30 Jul 2023 15:50:10 +0200 Subject: [PATCH 49/71] Fix multiplayer match screen being exited from when not current This was supposed to be fixed by #24255, but has popped up as a regression on Sentry since: https://sentry.ppy.sh/organizations/ppy/issues/22749/?project=2&referrer=regression_activity-email On a fifteen-minute check I cannot figure out how to reproduce, so rather than spending further brain cycles on this, just apply the same explicit guard that like fifteen other places do. --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f5746ca96c..f13b47c0f5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -265,7 +265,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { exitConfirmed = true; - this.Exit(); + if (this.IsCurrentScreen()) + this.Exit(); })); } From 7763f3dd404afa4a1dea1039804e36ca4c469213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 30 Jul 2023 19:05:08 +0200 Subject: [PATCH 50/71] Fix osu! logo suddenly disappearing during rapid exit --- osu.Game/Screens/Menu/MainMenu.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 510c9a5373..22040b4f0b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -202,6 +203,9 @@ namespace osu.Game.Screens.Menu dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); } + [CanBeNull] + private Drawable proxiedLogo; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -211,7 +215,7 @@ namespace osu.Game.Screens.Menu logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint); - logo.ProxyToContainer(logoTarget); + proxiedLogo = logo.ProxyToContainer(logoTarget); if (resuming) { @@ -250,12 +254,27 @@ namespace osu.Game.Screens.Menu var seq = logo.FadeOut(300, Easing.InSine) .ScaleTo(0.2f, 300, Easing.InSine); - logo.ReturnProxy(); + if (proxiedLogo != null) + { + logo.ReturnProxy(); + proxiedLogo = null; + } seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null)); } + protected override void LogoExiting(OsuLogo logo) + { + base.LogoExiting(logo); + + if (proxiedLogo != null) + { + logo.ReturnProxy(); + proxiedLogo = null; + } + } + public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); From 262f25dce826574cbfd466fbecf52e151dabe642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 30 Jul 2023 19:39:30 +0200 Subject: [PATCH 51/71] Make `SelectionRotationHandler` a `Component` --- .../Edit/OsuSelectionHandler.cs | 5 +--- .../Edit/OsuSelectionRotationHandler.cs | 23 +++++++++----- .../Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionHandler.cs | 3 +- .../SkinSelectionRotationHandler.cs | 30 ++++++++++++------- .../Compose/Components/SelectionHandler.cs | 8 +++-- .../Components/SelectionRotationHandler.cs | 3 +- 7 files changed, 46 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1dfbf4179b..e81941d254 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -163,10 +163,7 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(ChangeHandler) - { - SelectedItems = { BindTarget = SelectedItems } - }; + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); private void scaleSlider(Slider slider, Vector2 scale) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 0eb7637786..21fb8a67de 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -16,17 +17,25 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { - public class OsuSelectionRotationHandler : SelectionRotationHandler + public partial class OsuSelectionRotationHandler : SelectionRotationHandler { - private readonly IEditorChangeHandler? changeHandler; + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } - public BindableList SelectedItems { get; } = new BindableList(); + private BindableList selectedItems { get; } = new BindableList(); - public OsuSelectionRotationHandler(IEditorChangeHandler? changeHandler) + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) { - this.changeHandler = changeHandler; + selectedItems.BindTo(editorBeatmap.SelectedHitObjects); + } - SelectedItems.CollectionChanged += (_, __) => updateState(); + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); } private void updateState() @@ -92,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit defaultOrigin = null; } - private IEnumerable selectedMovableObjects => SelectedItems.Cast() + private IEnumerable selectedMovableObjects => selectedItems.Cast() .Where(h => h is not Spinner); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 147488812e..9901118ce8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing return true; } - private class TestSelectionRotationHandler : SelectionRotationHandler + private partial class TestSelectionRotationHandler : SelectionRotationHandler { private readonly Func getTargetContainer; diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index bee973bea0..72216f040e 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -26,9 +26,8 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor skinEditor { get; set; } = null!; - public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler(ChangeHandler) + public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler { - SelectedItems = { BindTarget = SelectedItems }, UpdatePosition = updateDrawablePosition }; diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index e60e2b1e12..60f69000a2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Screens.Edit; @@ -15,23 +16,32 @@ using osuTK; namespace osu.Game.Overlays.SkinEditor { - public class SkinSelectionRotationHandler : SelectionRotationHandler + public partial class SkinSelectionRotationHandler : SelectionRotationHandler { - private readonly IEditorChangeHandler? changeHandler; - - public BindableList SelectedItems { get; } = new BindableList(); public Action UpdatePosition { get; init; } = null!; - public SkinSelectionRotationHandler(IEditorChangeHandler? changeHandler) - { - this.changeHandler = changeHandler; + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } - SelectedItems.CollectionChanged += (_, __) => updateState(); + private BindableList selectedItems { get; } = new BindableList(); + + [BackgroundDependencyLoader] + private void load(SkinEditor skinEditor) + { + selectedItems.BindTo(skinEditor.SelectedComponents); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); } private void updateState() { - CanRotate.Value = SelectedItems.Count > 0; + CanRotate.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; @@ -47,7 +57,7 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); - objectsInRotation = SelectedItems.Cast().ToArray(); + objectsInRotation = selectedItems.Cast().ToArray(); originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 31ad8fa3d7..57f9513cc1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -68,9 +68,11 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - RotationHandler = CreateRotationHandler(); - - InternalChild = SelectionBox = CreateSelectionBox(); + AddRangeInternal(new Drawable[] + { + RotationHandler = CreateRotationHandler(), + SelectionBox = CreateSelectionBox(), + }); SelectedItems.CollectionChanged += (_, _) => { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 6524f7fa35..5faa4a108d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Graphics; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -9,7 +10,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Base handler for editor rotation operations. /// - public class SelectionRotationHandler + public partial class SelectionRotationHandler : Component { /// /// Whether the rotation can currently be performed. From ebe5dd2ac97683761d1712b896235c5d53a2611e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 30 Jul 2023 20:21:41 +0200 Subject: [PATCH 52/71] Interface with `SelectionRotationHandler` via DI rather than explicit passing --- .../Visual/Editing/TestSceneComposeSelectBox.cs | 10 +++++++++- .../Screens/Edit/Compose/Components/SelectionBox.cs | 13 +++++++------ .../Components/SelectionBoxRotationHandle.cs | 13 +++++++------ .../Edit/Compose/Components/SelectionHandler.cs | 10 ++++++++-- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 9901118ce8..80c69aacf6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using JetBrains.Annotations; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; @@ -22,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing private Container selectionArea; private SelectionBox selectionBox; + [Cached(typeof(SelectionRotationHandler))] + private TestSelectionRotationHandler rotationHandler; + + public TestSceneComposeSelectBox() + { + rotationHandler = new TestSelectionRotationHandler(() => selectionArea); + } + [SetUp] public void SetUp() => Schedule(() => { @@ -41,7 +50,6 @@ namespace osu.Game.Tests.Visual.Editing CanFlipX = true, CanFlipY = true, - RotationHandler = new TestSelectionRotationHandler(() => selectionArea), OnScale = handleScale } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index ed6bbf7668..876e8ccbe9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -23,7 +23,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private const float button_padding = 5; - public SelectionRotationHandler? RotationHandler { get; init; } + [Resolved] + private SelectionRotationHandler? rotationHandler { get; set; } + public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -149,8 +151,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - if (RotationHandler != null) - canRotate.BindTo(RotationHandler.CanRotate); + if (rotationHandler != null) + canRotate.BindTo(rotationHandler.CanRotate); canRotate.BindValueChanged(_ => recreate(), true); } @@ -252,8 +254,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addRotationComponents() { - rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => RotationHandler?.Rotate(-90)); - rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => RotationHandler?.Rotate(90)); + rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => rotationHandler?.Rotate(-90)); + rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => rotationHandler?.Rotate(90)); addRotateHandle(Anchor.TopLeft); addRotateHandle(Anchor.TopRight); @@ -323,7 +325,6 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxRotationHandle { Anchor = anchor, - RotationHandler = RotationHandler }; handle.OperationStarted += operationStarted; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 8665ec9b08..024749a701 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -19,8 +19,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip { - public SelectionRotationHandler? RotationHandler { get; init; } - public LocalisableString TooltipText { get; private set; } private SpriteIcon icon = null!; @@ -32,6 +30,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private SelectionBox selectionBox { get; set; } = null!; + [Resolved] + private SelectionRotationHandler? rotationHandler { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -61,9 +62,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnDragStart(DragStartEvent e) { - if (RotationHandler == null) return false; + if (rotationHandler == null) return false; - RotationHandler.Begin(); + rotationHandler.Begin(); return true; } @@ -97,7 +98,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { - RotationHandler?.Commit(); + rotationHandler?.Commit(); UpdateHoverState(); cumulativeRotation.Value = null; @@ -121,7 +122,7 @@ namespace osu.Game.Screens.Edit.Compose.Components cumulativeRotation.Value = newRotation; - RotationHandler?.Update(newRotation); + rotationHandler?.Update(newRotation); TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 57f9513cc1..158b4066bc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -65,12 +65,19 @@ namespace osu.Game.Screens.Edit.Compose.Components AlwaysPresent = true; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(RotationHandler = CreateRotationHandler()); + return dependencies; + } + [BackgroundDependencyLoader] private void load() { AddRangeInternal(new Drawable[] { - RotationHandler = CreateRotationHandler(), + RotationHandler, SelectionBox = CreateSelectionBox(), }); @@ -86,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - RotationHandler = RotationHandler, OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, From 72005bef7c037977b084f8c5b8eb60a516610573 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Jul 2023 15:10:58 +0900 Subject: [PATCH 53/71] Fix skin editor crashing if the same component is provided twice --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 2b23ce290f..8179d58ddf 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -356,7 +356,7 @@ namespace osu.Game.Overlays.SkinEditor { new SettingsDropdown { - Items = availableTargets.Select(t => t.Lookup), + Items = availableTargets.Select(t => t.Lookup).Distinct(), Current = selectedTarget, } } From d78cc6085194de5f321f8a22354f7d9ddc73696b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Aug 2023 07:12:40 +0900 Subject: [PATCH 54/71] Update framework --- 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 7f15d9fafd..651e5b1fe6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - +