From cda9440a296304bd710c1787436ea1a948f6c999 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Dec 2023 14:39:50 +0900 Subject: [PATCH 01/91] Fix JuiceStream velocity calculation --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/JuiceStream.cs | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 8c460586b0..f5c5ffb529 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; @@ -48,16 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects public double TickDistanceMultiplier = 1; [JsonIgnore] - private double velocityFactor; + public double Velocity { get; private set; } [JsonIgnore] - private double tickDistanceFactor; - - [JsonIgnore] - public double Velocity => velocityFactor * SliderVelocityMultiplier; - - [JsonIgnore] - public double TickDistance => tickDistanceFactor * TickDistanceMultiplier; + public double TickDistance { get; private set; } /// /// The length of one span of this . @@ -70,8 +64,13 @@ namespace osu.Game.Rulesets.Catch.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; + Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME); + + // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier` + // for backwards compatibility reasons (intentionally introducing floating point errors to match stable). + double scoringDistance = Velocity * timingPoint.BeatLength; + + TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From 6bb72a9fccb92760377e3b081b99088a480c33c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 1 Jan 2024 15:46:07 +0100 Subject: [PATCH 02/91] Revert "Remove other grid types" This reverts commit de14da95fa6d0230af1aeef7e9b0afd5caaa059e. --- .../Editor/TestSceneOsuEditorGrids.cs | 3 + .../Edit/OsuGridToolboxGroup.cs | 79 ++++++++++++++ .../Edit/OsuHitObjectComposer.cs | 41 +++++-- .../Editing/TestScenePositionSnapGrid.cs | 45 ++++++++ .../Components/CircularPositionSnapGrid.cs | 101 ++++++++++++++++++ .../Components/TriangularPositionSnapGrid.cs | 89 +++++++++++++++ 6 files changed, 351 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 7cafd10454..21427ba281 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -100,6 +101,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return grid switch { RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), + TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value), + CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45), _ => Vector2.Zero }; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index e82ca780ad..76e735449a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -12,7 +16,9 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit { @@ -76,10 +82,13 @@ namespace osu.Game.Rulesets.Osu.Edit /// public Bindable SpacingVector { get; } = new Bindable(); + public Bindable GridType { get; } = new Bindable(); + private ExpandableSlider startPositionXSlider = null!; private ExpandableSlider startPositionYSlider = null!; private ExpandableSlider spacingSlider = null!; private ExpandableSlider gridLinesRotationSlider = null!; + private EditorRadioButtonCollection gridTypeButtons = null!; public OsuGridToolboxGroup() : base("grid") @@ -113,6 +122,31 @@ namespace osu.Game.Rulesets.Osu.Edit Current = GridLinesRotation, KeyboardStep = 1, }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + gridTypeButtons = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Square", + () => GridType.Value = PositionSnapGridType.Square, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Triangle", + () => GridType.Value = PositionSnapGridType.Triangle, + () => new OutlineTriangle(true, 20)), + new RadioButton("Circle", + () => GridType.Value = PositionSnapGridType.Circle, + () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), + } + }, + } + }, }; Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; @@ -122,6 +156,8 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); + gridTypeButtons.Items.First().Select(); + StartPositionX.BindValueChanged(x => { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; @@ -149,6 +185,12 @@ namespace osu.Game.Rulesets.Osu.Edit gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; }, true); + + expandingContainer?.Expanded.BindValueChanged(v => + { + gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + }, true); } private void nextGridSize() @@ -171,5 +213,42 @@ namespace osu.Game.Rulesets.Osu.Edit public void OnReleased(KeyBindingReleaseEvent e) { } + + public partial class OutlineTriangle : BufferedContainer + { + public OutlineTriangle(bool outlineOnly, float size) + : base(cachedFrameBuffer: true) + { + Size = new Vector2(size); + + InternalChildren = new Drawable[] + { + new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, + }; + + if (outlineOnly) + { + AddInternal(new EquilateralTriangle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = 0.48f, + Colour = Color4.Black, + Size = new Vector2(size - 7), + Blending = BlendingParameters.None, + }); + } + + Blending = BlendingParameters.Additive; + } + } + } + + public enum PositionSnapGridType + { + Square, + Triangle, + Circle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 51bb74926f..84d5adbc52 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - updatePositionSnapGrid(); + OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true); RightToolbox.AddRange(new EditorToolboxGroup[] { @@ -110,18 +110,45 @@ namespace osu.Game.Rulesets.Osu.Edit ); } - private void updatePositionSnapGrid() + private void updatePositionSnapGrid(ValueChangedEvent obj) { if (positionSnapGrid != null) LayerBelowRuleset.Remove(positionSnapGrid, true); - var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); + switch (obj.NewValue) + { + case PositionSnapGridType.Square: + var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); - rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); - rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); - rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); + rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); + rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); - positionSnapGrid = rectangularPositionSnapGrid; + positionSnapGrid = rectangularPositionSnapGrid; + break; + + case PositionSnapGridType.Triangle: + var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); + + triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); + + positionSnapGrid = triangularPositionSnapGrid; + break; + + case PositionSnapGridType.Circle: + var circularPositionSnapGrid = new CircularPositionSnapGrid(); + + circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + + positionSnapGrid = circularPositionSnapGrid; + break; + + default: + throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); + } + + // Bind the start position to the toolbox sliders. + positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); positionSnapGrid.RelativeSizeAxes = Axes.Both; LayerBelowRuleset.Add(positionSnapGrid); diff --git a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index 2721bc3602..7e66edc2dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing })); } + [TestCaseSource(nameof(test_cases))] + public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation) + { + TriangularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new TriangularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + grid.GridLineRotation.Value = rotation; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + [TestCaseSource(nameof(test_cases))] + public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation) + { + CircularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new CircularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + private partial class SnappingCursorContainer : CompositeDrawable { public Func GetSnapPosition; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs new file mode 100644 index 0000000000..403a270359 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -0,0 +1,101 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class CircularPositionSnapGrid : PositionSnapGrid + { + /// + /// The spacing between grid lines of this . + /// + public BindableFloat Spacing { get; } = new BindableFloat(1f) + { + MinValue = 0f, + }; + + public CircularPositionSnapGrid() + { + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + } + + protected override void CreateContent() + { + var drawSize = DrawSize; + + // Calculate the maximum distance from the origin to the edge of the grid. + float maxDist = MathF.Max( + MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), + MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) + ); + + generateCircles((int)(maxDist / Spacing.Value) + 1); + + GenerateOutline(drawSize); + } + + private void generateCircles(int count) + { + // Make lines the same width independent of display resolution. + float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; + + List generatedCircles = new List(); + + for (int i = 0; i < count; i++) + { + // Add a minimum diameter so the center circle is clearly visible. + float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); + + var gridCircle = new CircularContainer + { + BorderColour = Colour4.White, + BorderThickness = lineWidth, + Alpha = 0.2f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = diameter, + Height = diameter, + Position = StartPosition.Value, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0f, + } + }; + + generatedCircles.Add(gridCircle); + } + + if (generatedCircles.Count == 0) + return; + + generatedCircles.First().Alpha = 0.8f; + + AddRangeInternal(generatedCircles); + } + + public override Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = original - StartPosition.Value; + + if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON) + return StartPosition.Value; + + float length = relativeToStart.Length; + float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value; + + return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs new file mode 100644 index 0000000000..93d2c6a74a --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid + { + /// + /// The spacing between grid lines of this . + /// + public BindableFloat Spacing { get; } = new BindableFloat(1f) + { + MinValue = 0f, + }; + + /// + /// The rotation in degrees of the grid lines of this . + /// + public BindableFloat GridLineRotation { get; } = new BindableFloat(); + + public TriangularPositionSnapGrid() + { + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); + } + + private const float sqrt3 = 1.73205080757f; + private const float sqrt3_over2 = 0.86602540378f; + private const float one_over_sqrt3 = 0.57735026919f; + + protected override void CreateContent() + { + var drawSize = DrawSize; + float stepSpacing = Spacing.Value * sqrt3_over2; + var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30); + var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90); + var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150); + + GenerateGridLines(step1, drawSize); + GenerateGridLines(-step1, drawSize); + + GenerateGridLines(step2, drawSize); + GenerateGridLines(-step2, drawSize); + + GenerateGridLines(step3, drawSize); + GenerateGridLines(-step3, drawSize); + + GenerateOutline(drawSize); + } + + public override Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value); + Vector2 hex = pixelToHex(relativeToStart); + + return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value); + } + + private Vector2 pixelToHex(Vector2 pixel) + { + float x = pixel.X / Spacing.Value; + float y = pixel.Y / Spacing.Value; + // Algorithm from Charles Chambers + // with modifications and comments by Chris Cox 2023 + // + float t = sqrt3 * y + 1; // scaled y, plus phase + float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor + float temp2 = t - x; // (y-x) diagonal, no floor needed + float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase + float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction + float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction + float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor + float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor + return new Vector2(q, r); + } + + private Vector2 hexToPixel(Vector2 hex) + { + // Taken from + // with modifications for the different definition of size. + return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y); + } + } +} From f807a3fd971527ba62938b674946989c44a4966c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 03/91] Remove Masking from PositionSnapGrid This caused issues in rendering the outline of the grid because the outline was getting masked at some resolutions. --- .../Components/LinedPositionSnapGrid.cs | 128 +++++++++++++++--- .../Compose/Components/PositionSnapGrid.cs | 2 - 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index ebdd76a4e2..8a7f6b5344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -15,18 +15,29 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { + if (Precision.AlmostEquals(step, Vector2.Zero)) + return; + int index = 0; - var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; + float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)); List generatedLines = new List(); - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) + while (true) { + Vector2 currentPosition = StartPosition.Value + index++ * step; + + if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) + { + if (!isMovingTowardsBox(currentPosition, step, drawSize)) + break; + + continue; + } + var gridLine = new Box { Colour = Colour4.White, @@ -34,15 +45,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.Centre, RelativeSizeAxes = Axes.None, Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + Height = Vector2.Distance(p1, p2), + Position = (p1 + p2) / 2, + Rotation = rotation, }; generatedLines.Add(gridLine); - - index += 1; - currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) @@ -59,23 +67,99 @@ namespace osu.Game.Screens.Edit.Compose.Components (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; } - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2) { - var p2 = lineStart + lineDir; + p1 = Vector2.Zero; + p2 = Vector2.Zero; - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); + if (Precision.AlmostEquals(lineDir.X, 0)) + { + // If the line is vertical, we only need to check if the X coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X)) + return false; - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + p1 = new Vector2(lineStart.X, 0); + p2 = new Vector2(lineStart.X, box.Y); + return true; + } - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + if (Precision.AlmostEquals(lineDir.Y, 0)) + { + // If the line is horizontal, we only need to check if the Y coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y)) + return false; - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); + p1 = new Vector2(0, lineStart.Y); + p2 = new Vector2(box.X, lineStart.Y); + return true; + } + + float m = lineDir.Y / lineDir.X; + float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero. + float b = lineStart.Y - m * lineStart.X; + + // Calculate intersection points with the sides of the box. + var p = new List(4); + + if (0 <= b && b <= box.Y) + p.Add(new Vector2(0, b)); + if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X) + p.Add(new Vector2((box.Y - b) * mInv, box.Y)); + if (0 <= m * box.X + b && m * box.X + b <= box.Y) + p.Add(new Vector2(box.X, m * box.X + b)); + if (0 <= -b * mInv && -b * mInv <= box.X) + p.Add(new Vector2(-b * mInv, 0)); + + switch (p.Count) + { + case 4: + // If there are 4 intersection points, the line is a diagonal of the box. + if (m > 0) + { + p1 = Vector2.Zero; + p2 = box; + } + else + { + p1 = new Vector2(0, box.Y); + p2 = new Vector2(box.X, 0); + } + + break; + + case 3: + // If there are 3 intersection points, the line goes through a corner of the box. + if (p[0] == p[1]) + { + p1 = p[0]; + p2 = p[2]; + } + else + { + p1 = p[0]; + p2 = p[1]; + } + + break; + + case 2: + p1 = p[0]; + p2 = p[1]; + + break; + } + + return !Precision.AlmostEquals(p1, p2); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index 36687ef73a..e576ac1e49 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected PositionSnapGrid() { - Masking = true; - StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); From 576d6ff7990a7bbb4842cc943c8842c3ed562a3b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 17:07:03 +0100 Subject: [PATCH 04/91] Fix masking in circular snap grid --- .../Edit/Compose/Components/CircularPositionSnapGrid.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 403a270359..791cb33439 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -82,7 +82,12 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedCircles.First().Alpha = 0.8f; - AddRangeInternal(generatedCircles); + AddInternal(new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = generatedCircles, + }); } public override Vector2 GetSnappedPosition(Vector2 original) From fe738a09517d7638781efffbd1efde61e05cb861 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 5 Jun 2024 18:12:02 +0200 Subject: [PATCH 05/91] Fix missing container inject --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index b7bc533296..76e735449a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -26,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + /// /// X position of the grid's origin. /// From 4f8c167cf96bb9732abd50788f5f003aa5ec38c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 5 Jun 2024 18:56:18 +0200 Subject: [PATCH 06/91] clean up to match logic in CircularDistanceSnapGrid --- .../Components/CircularPositionSnapGrid.cs | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 791cb33439..8e63d6bcc0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -7,7 +7,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osuTK; @@ -32,14 +32,14 @@ namespace osu.Game.Screens.Edit.Compose.Components { var drawSize = DrawSize; - // Calculate the maximum distance from the origin to the edge of the grid. - float maxDist = MathF.Max( - MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), - MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) - ); - - generateCircles((int)(maxDist / Spacing.Value) + 1); + // Calculate the required number of circles based on the maximum distance from the origin to the edge of the grid. + float dx = Math.Max(StartPosition.Value.X, DrawWidth - StartPosition.Value.X); + float dy = Math.Max(StartPosition.Value.Y, DrawHeight - StartPosition.Value.Y); + float maxDistance = new Vector2(dx, dy).Length; + // We need to add one because the first circle starts at zero radius. + int requiredCircles = (int)(maxDistance / Spacing.Value) + 1; + generateCircles(requiredCircles); GenerateOutline(drawSize); } @@ -48,30 +48,22 @@ namespace osu.Game.Screens.Edit.Compose.Components // Make lines the same width independent of display resolution. float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; - List generatedCircles = new List(); + List generatedCircles = new List(); for (int i = 0; i < count; i++) { // Add a minimum diameter so the center circle is clearly visible. float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); - var gridCircle = new CircularContainer + var gridCircle = new CircularProgress { - BorderColour = Colour4.White, - BorderThickness = lineWidth, - Alpha = 0.2f, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = diameter, - Height = diameter, Position = StartPosition.Value, - Masking = true, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0f, - } + Origin = Anchor.Centre, + Size = new Vector2(diameter), + InnerRadius = lineWidth * 1f / diameter, + Colour = Colour4.White, + Alpha = 0.2f, + Progress = 1, }; generatedCircles.Add(gridCircle); From b5f0e585245b1ec4993a1cb208e09bbeabccdf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:06:17 +0200 Subject: [PATCH 07/91] Add ability to better control slider control point type during placement via `Tab` --- .../Sliders/SliderPlacementBlueprint.cs | 83 +++++++++++++++---- 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 0fa84c91fc..f21a1279e5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private PathControlPoint segmentStart; private PathControlPoint cursor; private int currentSegmentLength; + private bool usingCustomSegmentType; [Resolved(CanBeNull = true)] [CanBeNull] @@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders case SliderPlacementState.ControlPoints: if (canPlaceNewControlPoint(out var lastPoint)) - { - // Place a new point by detatching the current cursor. - updateCursor(); - cursor = null; - } + placeNewControlPoint(); else - { - // Transform the last point into a new segment. - Debug.Assert(lastPoint != null); - - segmentStart = lastPoint; - segmentStart.Type = PathType.LINEAR; - - currentSegmentLength = 1; - } + beginNewSegment(lastPoint); break; } @@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + private void beginNewSegment(PathControlPoint lastPoint) + { + // Transform the last point into a new segment. + Debug.Assert(lastPoint != null); + + segmentStart = lastPoint; + segmentStart.Type = PathType.LINEAR; + + currentSegmentLength = 1; + usingCustomSegmentType = false; + } + protected override bool OnDragStart(DragStartEvent e) { if (e.Button != MouseButton.Left) @@ -223,6 +224,47 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnMouseUp(e); } + private static readonly PathType[] path_types = + [ + PathType.LINEAR, + PathType.BEZIER, + PathType.PERFECT_CURVE, + PathType.BSpline(4), + ]; + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + if (state != SliderPlacementState.ControlPoints) + return false; + + switch (e.Key) + { + case Key.Tab: + { + usingCustomSegmentType = true; + + int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1; + + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + do + { + currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length; + segmentStart.Type = path_types[currentTypeIndex]; + controlPointVisualiser.EnsureValidPathTypes(); + } while (segmentStart.Type != path_types[currentTypeIndex]); + + return true; + } + } + + return true; + } + protected override void Update() { base.Update(); @@ -246,6 +288,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePathType() { + if (usingCustomSegmentType) + { + controlPointVisualiser.EnsureValidPathTypes(); + return; + } + if (state == SliderPlacementState.Drawing) { segmentStart.Type = PathType.BSpline(4); @@ -316,6 +364,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return lastPiece.IsHovered != true; } + private void placeNewControlPoint() + { + // Place a new point by detatching the current cursor. + updateCursor(); + cursor = null; + } + private void updateSlider() { if (state == SliderPlacementState.Drawing) From 16ea8f67b00b88fd714c3aa87b42987836a8a5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:06:27 +0200 Subject: [PATCH 08/91] Add ability to start a new segment during placement via `S` key --- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f21a1279e5..7fac95ab91 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -242,6 +242,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders switch (e.Key) { + case Key.S: + { + if (!canPlaceNewControlPoint(out _)) + return false; + + placeNewControlPoint(); + var last = HitObject.Path.ControlPoints.Last(p => p != cursor); + beginNewSegment(last); + return true; + } + case Key.Tab: { usingCustomSegmentType = true; From 88bdc12022b3bb90e4b70877450c8dc5a0b57d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:28:52 +0200 Subject: [PATCH 09/91] Add ability to cycle through available types when selecting single control point on a slider --- .../Components/PathControlPointVisualiser.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 47af16ffa6..2bdef4afe8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -245,6 +245,43 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || e.Key != Key.Tab) + return false; + + var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); + if (selectedPieces.Length != 1) + return false; + + var selectedPoint = selectedPieces.Single().ControlPoint; + var validTypes = getValidPathTypes(selectedPoint).ToArray(); + int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); + + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + do + { + currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; + selectedPoint.Type = validTypes[currentTypeIndex]; + EnsureValidPathTypes(); + } while (selectedPoint.Type != validTypes[currentTypeIndex]); + + return true; + + IEnumerable getValidPathTypes(PathControlPoint pathControlPoint) + { + if (pathControlPoint != controlPoints[0]) + yield return null; + + yield return PathType.LINEAR; + yield return PathType.BEZIER; + yield return PathType.PERFECT_CURVE; + yield return PathType.BSpline(4); + } + } + private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) From 1e137271abcb020276ec12020301fdd5a487b4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:47:22 +0200 Subject: [PATCH 10/91] Add testing for keyboard control of path during placement --- .../TestSceneSliderPlacementBlueprint.cs | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index bbded55732..bc1e4f9864 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; @@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(200); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); } [Test] @@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); } [Test] @@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100, 100)); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.LINEAR); } [Test] @@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); assertLength(100); } @@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(4); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.LINEAR); } [Test] @@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.PERFECT_CURVE); } [Test] @@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(4, new Vector2(200)); - assertControlPointType(0, PathType.PERFECT_CURVE); - assertControlPointType(2, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(2, PathType.PERFECT_CURVE); + } + + [Test] + public void TestManualPathTypeControlViaKeyboard() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + + assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE); + + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2); + assertControlPointTypeDuringPlacement(0, PathType.LINEAR); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + assertControlPointTypeDuringPlacement(0, PathType.BSpline(4)); + + AddStep("start new segment via S", () => InputManager.Key(Key.S)); + assertControlPointTypeDuringPlacement(2, PathType.LINEAR); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(2, PathType.PERFECT_CURVE); } [Test] @@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addClickStep(MouseButton.Right); assertPlaced(true); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(808, tolerance: 10); assertControlPointCount(5); - assertControlPointType(0, PathType.BSpline(4)); - assertControlPointType(1, null); - assertControlPointType(2, null); - assertControlPointType(3, null); - assertControlPointType(4, null); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(1, null); + assertFinalControlPointType(2, null); + assertFinalControlPointType(3, null); + assertFinalControlPointType(4, null); } [Test] @@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(600, tolerance: 10); assertControlPointCount(4); - assertControlPointType(0, PathType.BSpline(4)); - assertControlPointType(1, PathType.BSpline(4)); - assertControlPointType(2, PathType.BSpline(4)); - assertControlPointType(3, null); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(1, PathType.BSpline(4)); + assertFinalControlPointType(2, PathType.BSpline(4)); + assertFinalControlPointType(3, null); } [Test] @@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); @@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); - private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); + private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", + () => this.ChildrenOfType>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type)); + + private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointPosition(int index, Vector2 position) => AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); From 789810069858456380b52403a3de6fde6d065b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:54:45 +0200 Subject: [PATCH 11/91] Add testing for keyboard control of path during selection --- .../TestScenePathControlPointVisualiser.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 9af028fd8c..4813cc089c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -177,6 +178,60 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addAssertPointPositionChanged(points, i); } + [Test] + public void TestChangingControlPointTypeViaTab() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.LINEAR); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true); + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(0, PathType.BEZIER); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.LINEAR); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.BSpline(4)); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.BEZIER); + + AddStep("select third last control point", () => + { + visualiser.Pieces[0].IsSelected.Value = false; + visualiser.Pieces[2].IsSelected.Value = true; + }); + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 3); + assertControlPointPathType(2, PathType.PERFECT_CURVE); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(2, PathType.BSpline(4)); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(2, null); + } + private void addAssertPointPositionChanged(Vector2[] points, int index) { AddAssert($"Point at {points.ElementAt(index)} changed", From 5652a558f98d2eca210901303bcb190f7e1eccd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 17:07:47 +0200 Subject: [PATCH 12/91] Allow to jump to a specific timestamp via bottom bar in editor Apparently this is a stable feature and is helpful for modding. --- .../Edit/Components/TimeInfoContainer.cs | 116 +++++++++++++++--- 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 4747828bca..b0e0d95132 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -1,11 +1,17 @@ // 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.Globalization; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -13,7 +19,6 @@ namespace osu.Game.Screens.Edit.Components { public partial class TimeInfoContainer : BottomBarContainer { - private OsuSpriteText trackTimer = null!; private OsuSpriteText bpm = null!; [Resolved] @@ -29,14 +34,7 @@ namespace osu.Game.Screens.Edit.Components Children = new Drawable[] { - trackTimer = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Spacing = new Vector2(-2, 0), - Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), - Y = -10, - }, + new TimestampControl(), bpm = new OsuSpriteText { Colour = colours.Orange1, @@ -47,19 +45,12 @@ namespace osu.Game.Screens.Edit.Components }; } - private double? lastTime; private double? lastBPM; protected override void Update() { base.Update(); - if (lastTime != editorClock.CurrentTime) - { - lastTime = editorClock.CurrentTime; - trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); - } - double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; if (lastBPM != newBPM) @@ -68,5 +59,98 @@ namespace osu.Game.Screens.Edit.Components bpm.Text = @$"{newBPM:0} BPM"; } } + + private partial class TimestampControl : OsuClickableContainer + { + private Container hoverLayer = null!; + private OsuSpriteText trackTimer = null!; + private OsuTextBox inputTextBox = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + public TimestampControl() + : base(HoverSampleSet.Button) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + hoverLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Top = 5, + Horizontal = -5 + }, + Child = new Box { RelativeSizeAxes = Axes.Both, }, + Alpha = 0, + }, + trackTimer = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(-2, 0), + Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), + }, + inputTextBox = new OsuTextBox + { + Width = 150, + Height = 36, + Alpha = 0, + CommitOnFocusLost = true, + }, + }); + + Action = () => + { + trackTimer.Alpha = 0; + inputTextBox.Alpha = 1; + inputTextBox.Text = editorClock.CurrentTime.ToEditorFormattedString(); + Schedule(() => + { + GetContainingFocusManager().ChangeFocus(inputTextBox); + inputTextBox.SelectAll(); + }); + }; + + inputTextBox.OnCommit += (_, __) => + { + if (TimeSpan.TryParseExact(inputTextBox.Text, @"mm\:ss\:fff", CultureInfo.InvariantCulture, out var timestamp)) + editorClock.SeekSmoothlyTo(timestamp.TotalMilliseconds); + + trackTimer.Alpha = 1; + inputTextBox.Alpha = 0; + }; + } + + private double? lastTime; + private bool showingHoverLayer; + + protected override void Update() + { + base.Update(); + + if (lastTime != editorClock.CurrentTime) + { + lastTime = editorClock.CurrentTime; + trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); + } + + bool shouldShowHoverLayer = IsHovered && inputTextBox.Alpha == 0; + + if (shouldShowHoverLayer != showingHoverLayer) + { + hoverLayer.FadeTo(shouldShowHoverLayer ? 0.2f : 0, 400, Easing.OutQuint); + showingHoverLayer = shouldShowHoverLayer; + } + } + } } } From a631d245daa18d0cc2e6cf239ad9e0f36e9a8864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 18:14:33 +0200 Subject: [PATCH 13/91] Fix test failure --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 7fac95ab91..fdfb52008c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - return true; + return false; } protected override void Update() From 683d5310b14c6b366b8b29ceaf20dffd1bcc775b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 18:33:36 +0200 Subject: [PATCH 14/91] Implement direct choice of slider control point path type via `Alt`-number --- .../Components/PathControlPointVisualiser.cs | 123 +++++++++++------- .../Sliders/SliderPlacementBlueprint.cs | 14 ++ 2 files changed, 91 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 2bdef4afe8..3d6e529afa 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -245,40 +245,73 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + // ReSharper disable once StaticMemberInGenericType + private static readonly PathType?[] path_types = + [ + null, + PathType.LINEAR, + PathType.BEZIER, + PathType.PERFECT_CURVE, + PathType.BSpline(4), + ]; + protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Repeat || e.Key != Key.Tab) + if (e.Repeat) return false; var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); if (selectedPieces.Length != 1) return false; - var selectedPoint = selectedPieces.Single().ControlPoint; - var validTypes = getValidPathTypes(selectedPoint).ToArray(); - int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); + var selectedPiece = selectedPieces.Single(); + var selectedPoint = selectedPiece.ControlPoint; - if (currentTypeIndex < 0 && e.ShiftPressed) - currentTypeIndex = 0; - - do + switch (e.Key) { - currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; - selectedPoint.Type = validTypes[currentTypeIndex]; - EnsureValidPathTypes(); - } while (selectedPoint.Type != validTypes[currentTypeIndex]); + case Key.Tab: + { + var validTypes = path_types; - return true; + if (selectedPoint == controlPoints[0]) + validTypes = validTypes.Where(t => t != null).ToArray(); - IEnumerable getValidPathTypes(PathControlPoint pathControlPoint) - { - if (pathControlPoint != controlPoints[0]) - yield return null; + int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); - yield return PathType.LINEAR; - yield return PathType.BEZIER; - yield return PathType.PERFECT_CURVE; - yield return PathType.BSpline(4); + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + changeHandler?.BeginChange(); + + do + { + currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; + + updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]); + } while (selectedPoint.Type != validTypes[currentTypeIndex]); + + changeHandler?.EndChange(); + + return true; + } + + case Key.Number0: + case Key.Number1: + case Key.Number2: + case Key.Number3: + case Key.Number4: + { + var type = path_types[e.Key - Key.Number0]; + + if (selectedPoint == controlPoints[0] && type == null) + return false; + + updatePathTypeOfSelectedPieces(type); + return true; + } + + default: + return false; } } @@ -291,30 +324,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } /// - /// Attempts to set the given control point piece to the given path type. + /// Attempts to set all selected control point pieces to the given path type. /// If that would fail, try to change the path such that it instead succeeds /// in a UX-friendly way. /// - /// The control point piece that we want to change the path type of. /// The path type we want to assign to the given control point piece. - private void updatePathType(PathControlPointPiece piece, PathType? type) + private void updatePathTypeOfSelectedPieces(PathType? type) { - var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint); - int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint); + changeHandler?.BeginChange(); - if (type?.Type == SplineType.PerfectCurve) + foreach (var p in Pieces.Where(p => p.IsSelected.Value)) { - // Can't always create a circular arc out of 4 or more points, - // so we split the segment into one 3-point circular arc segment - // and one segment of the previous type. - int thirdPointIndex = indexInSegment + 2; + var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint); + int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint); - if (pointsInSegment.Count > thirdPointIndex + 1) - pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; + if (type?.Type == SplineType.PerfectCurve) + { + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; + + if (pointsInSegment.Count > thirdPointIndex + 1) + pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; + } + + hitObject.Path.ExpectedDistance.Value = null; + p.ControlPoint.Type = type; } - hitObject.Path.ExpectedDistance.Value = null; - piece.ControlPoint.Type = type; + EnsureValidPathTypes(); + + changeHandler?.EndChange(); } [Resolved(CanBeNull = true)] @@ -470,17 +511,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => - { - changeHandler?.BeginChange(); - - foreach (var p in Pieces.Where(p => p.IsSelected.Value)) - updatePathType(p, type); - - EnsureValidPathTypes(); - - changeHandler?.EndChange(); - }); + var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => updatePathTypeOfSelectedPieces(type)); if (countOfState == totalCount) item.State.Value = TernaryState.True; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fdfb52008c..91cd270af6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -253,6 +253,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + case Key.Number1: + case Key.Number2: + case Key.Number3: + case Key.Number4: + { + if (!e.AltPressed) + return false; + + usingCustomSegmentType = true; + segmentStart.Type = path_types[e.Key - Key.Number1]; + controlPointVisualiser.EnsureValidPathTypes(); + return true; + } + case Key.Tab: { usingCustomSegmentType = true; From 310265c43fa95458e61899d60b72d32366c1dcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:16:22 +0200 Subject: [PATCH 15/91] Add slider placement binding description in tooltip --- .../Edit/SliderCompositionTool.cs | 7 ++++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 ++-- .../Edit/HitObjectCompositionToolButton.cs | 22 +++++++++++++++++++ .../Edit/Tools/HitObjectCompositionTool.cs | 7 +++--- .../Components/RadioButtons/RadioButton.cs | 4 ++-- 5 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 676205c8d7..617cc1c19b 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit public SliderCompositionTool() : base(nameof(Slider)) { + TooltipText = """ + Left click for new point. + Left click twice or S key for new segment. + Tab, Shift-Tab, or Alt-1~4 to change current segment type. + Right click to finish. + Click and drag for drawing mode. + """; } public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index d0c6078c9d..a34717e7ae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -215,14 +215,14 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items = CompositionTools .Prepend(new SelectTool()) - .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) + .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .ToList(); foreach (var item in toolboxCollection.Items) { item.Selected.DisabledChanged += isDisabled => { - item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty; + item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText; }; } diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs new file mode 100644 index 0000000000..ba566ff5c0 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Screens.Edit.Components.RadioButtons; + +namespace osu.Game.Rulesets.Edit +{ + public class HitObjectCompositionToolButton : RadioButton + { + public HitObjectCompositionTool Tool { get; } + + public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action) + : base(tool.Name, action, tool.CreateIcon) + { + Tool = tool; + + TooltipText = tool.TooltipText; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 707645edeb..26e88aa530 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -1,9 +1,8 @@ // 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 osu.Framework.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Edit.Tools { @@ -11,6 +10,8 @@ namespace osu.Game.Rulesets.Edit.Tools { public readonly string Name; + public LocalisableString TooltipText { get; init; } = default; + protected HitObjectCompositionTool(string name) { Name = name; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Edit.Tools public abstract PlacementBlueprint CreatePlacementBlueprint(); - public virtual Drawable CreateIcon() => null; + public virtual Drawable? CreateIcon() => null; public override string ToString() => Name; } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index f49fc6f6ab..26022aa746 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. /// - public readonly Func? CreateIcon; + public readonly Func? CreateIcon; private readonly Action? action; - public RadioButton(string label, Action? action, Func? createIcon = null) + public RadioButton(string label, Action? action, Func? createIcon = null) { Label = label; CreateIcon = createIcon; From a3326086f79513cdfe613e3304b1ca55c6558f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:25:05 +0200 Subject: [PATCH 16/91] Adjust hotkeys to address feedback --- .../Components/PathControlPointVisualiser.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 3d6e529afa..775604174b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -248,11 +248,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // ReSharper disable once StaticMemberInGenericType private static readonly PathType?[] path_types = [ - null, PathType.LINEAR, PathType.BEZIER, PathType.PERFECT_CURVE, PathType.BSpline(4), + null, ]; protected override bool OnKeyDown(KeyDownEvent e) @@ -260,17 +260,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (e.Repeat) return false; - var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); - if (selectedPieces.Length != 1) - return false; - - var selectedPiece = selectedPieces.Single(); - var selectedPoint = selectedPiece.ControlPoint; - switch (e.Key) { case Key.Tab: { + var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); + if (selectedPieces.Length != 1) + return false; + + var selectedPiece = selectedPieces.Single(); + var selectedPoint = selectedPiece.ControlPoint; + var validTypes = path_types; if (selectedPoint == controlPoints[0]) @@ -295,15 +295,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } - case Key.Number0: case Key.Number1: case Key.Number2: case Key.Number3: case Key.Number4: + case Key.Number5: { - var type = path_types[e.Key - Key.Number0]; + var type = path_types[e.Key - Key.Number1]; - if (selectedPoint == controlPoints[0] && type == null) + if (Pieces[0].IsSelected.Value && type == null) return false; updatePathTypeOfSelectedPieces(type); From 73786a6f9f1039b7c64587db8bb22a402ebe7c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:32:12 +0200 Subject: [PATCH 17/91] Adjust & expand test coverage --- .../TestScenePathControlPointVisualiser.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 4813cc089c..93eb76aba6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -215,21 +215,40 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.Key(Key.Tab); InputManager.ReleaseKey(Key.LShift); }); - assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(0, PathType.PERFECT_CURVE); + assertControlPointPathType(2, PathType.BSpline(4)); AddStep("select third last control point", () => { visualiser.Pieces[0].IsSelected.Value = false; visualiser.Pieces[2].IsSelected.Value = true; }); - AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 3); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); assertControlPointPathType(2, PathType.PERFECT_CURVE); - AddStep("press tab", () => InputManager.Key(Key.Tab)); - assertControlPointPathType(2, PathType.BSpline(4)); - - AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2); + assertControlPointPathType(0, PathType.BEZIER); assertControlPointPathType(2, null); + + AddStep("select first and third control points", () => + { + visualiser.Pieces[0].IsSelected.Value = true; + visualiser.Pieces[2].IsSelected.Value = true; + }); + AddStep("press alt-1", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.AltLeft); + }); + assertControlPointPathType(0, PathType.LINEAR); + assertControlPointPathType(2, PathType.LINEAR); } private void addAssertPointPositionChanged(Vector2[] points, int index) From 24217514192a8ef58384c9a93e2a8b74cf91444a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:33:02 +0200 Subject: [PATCH 18/91] Fix code quality inspections --- osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 26e88aa530..ba1dc817bb 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -10,14 +10,14 @@ namespace osu.Game.Rulesets.Edit.Tools { public readonly string Name; - public LocalisableString TooltipText { get; init; } = default; + public LocalisableString TooltipText { get; init; } protected HitObjectCompositionTool(string name) { Name = name; } - public abstract PlacementBlueprint CreatePlacementBlueprint(); + public abstract PlacementBlueprint? CreatePlacementBlueprint(); public virtual Drawable? CreateIcon() => null; From 1b4a3b0e2ebcba788fccdc353613f170c883551c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 09:37:43 +0200 Subject: [PATCH 19/91] Change editor speed adjustment back to adjusting tempo - Partially reverts https://github.com/ppy/osu/pull/12080 - Addresses https://github.com/ppy/osu/discussions/27830, https://github.com/ppy/osu/discussions/23789, https://github.com/ppy/osu/discussions/15368, et al. The important distinction here is that to prevent misuse when timing, the control will revert to 1.0x speed and disable when moving to timing screen, with a tooltip explaining why. --- .../Edit/Components/PlaybackControl.cs | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 9e27f0e57d..0546878788 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -10,9 +10,11 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components public partial class PlaybackControl : BottomBarContainer { private IconButton playButton = null!; + private PlaybackSpeedControl playbackSpeedControl = null!; [Resolved] private EditorClock editorClock { get; set; } = null!; - private readonly BindableNumber freqAdjust = new BindableDouble(1); + private readonly Bindable currentScreenMode = new Bindable(); + private readonly BindableNumber tempoAdjustment = new BindableDouble(1); [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, Editor? editor) { Background.Colour = colourProvider.Background4; @@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components Icon = FontAwesome.Regular.PlayCircle, Action = togglePause, }, - new OsuSpriteText + playbackSpeedControl = new PlaybackSpeedControl { - Origin = Anchor.BottomLeft, - Text = EditorStrings.PlaybackSpeed, - RelativePositionAxes = Axes.Y, - Y = 0.5f, - Padding = new MarginPadding { Left = 45 } - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Padding = new MarginPadding { Left = 45 }, - Child = new PlaybackTabControl { Current = freqAdjust }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Left = 45, }, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = EditorStrings.PlaybackSpeed, + }, + new PlaybackTabControl + { + Current = tempoAdjustment, + RelativeSizeAxes = Axes.X, + Height = 16, + }, + } } }; - Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true); + Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true); + + if (editor != null) + currentScreenMode.BindTo(editor.Mode); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentScreenMode.BindValueChanged(_ => + { + if (currentScreenMode.Value == EditorScreenMode.Timing) + { + tempoAdjustment.Value = 1; + tempoAdjustment.Disabled = true; + playbackSpeedControl.FadeTo(0.5f, 400, Easing.OutQuint); + playbackSpeedControl.TooltipText = "Speed adjustment is unavailable in timing mode. Timing at slower speeds is inaccurate due to resampling artifacts."; + } + else + { + tempoAdjustment.Disabled = false; + playbackSpeedControl.FadeTo(1, 400, Easing.OutQuint); + playbackSpeedControl.TooltipText = default; + } + }); } protected override void Dispose(bool isDisposing) { - Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust); + Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment); base.Dispose(isDisposing); } @@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon; } + private partial class PlaybackSpeedControl : FillFlowContainer, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } + private partial class PlaybackTabControl : OsuTabControl { private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; @@ -174,7 +213,7 @@ namespace osu.Game.Screens.Edit.Components protected override bool OnHover(HoverEvent e) { updateState(); - return true; + return false; } protected override void OnHoverLost(HoverLostEvent e) => updateState(); From 87888ff0bbca43e94a5383939f37e01aec1d7419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 10:28:36 +0200 Subject: [PATCH 20/91] Extend slider selection box bounds to contain all control points inside Previously, the selection box was only guaranteed to contain the actual body of the slider itself, the control point nodes were allowed to exit it. This lead to a lot of weird interactions with the selection box controls (rotation/drag handles, also the buttons under/over it) as the slider anchors could overlap with them. To bypass this issue entirely just ensure that the selection box's size does include the control point nodes at all times. --- .../Sliders/SliderSelectionBlueprint.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 49fdf12d60..2f1e2a9fdd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -54,7 +54,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private BindableBeatDivisor beatDivisor { get; set; } - public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; + public override Quad SelectionQuad + { + get + { + var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat; + + if (ControlPointVisualiser != null) + { + foreach (var piece in ControlPointVisualiser.Pieces) + result = RectangleF.Union(result, piece.ScreenSpaceDrawQuad.AABBFloat); + } + + return result; + } + } private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); From 5fe21f16b994d0c7e040d9e012b92ff977099487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 10:41:26 +0200 Subject: [PATCH 21/91] Fix test failures --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 775604174b..ddf6cd0f57 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -301,6 +301,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case Key.Number4: case Key.Number5: { + if (!e.AltPressed) + return false; + var type = path_types[e.Key - Key.Number1]; if (Pieces[0].IsSelected.Value && type == null) From e1827ac28d7c344deea1764a7ab8f1cb796fb0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 12:33:12 +0200 Subject: [PATCH 22/91] Address review feedback --- osu.Game/OsuGame.cs | 2 +- .../Edit/Components/TimeInfoContainer.cs | 10 +++++----- osu.Game/Screens/Edit/Editor.cs | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 667c3ecb99..63aa4564bf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -595,7 +595,7 @@ namespace osu.Game return; } - editor.HandleTimestamp(timestamp); + editor.HandleTimestamp(timestamp, notifyOnError: true); } /// diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index b0e0d95132..9e14ec851b 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Globalization; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; @@ -66,6 +64,9 @@ namespace osu.Game.Screens.Edit.Components private OsuSpriteText trackTimer = null!; private OsuTextBox inputTextBox = null!; + [Resolved] + private Editor? editor { get; set; } + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -120,11 +121,10 @@ namespace osu.Game.Screens.Edit.Components }); }; + inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue)); + inputTextBox.OnCommit += (_, __) => { - if (TimeSpan.TryParseExact(inputTextBox.Text, @"mm\:ss\:fff", CultureInfo.InvariantCulture, out var timestamp)) - editorClock.SeekSmoothlyTo(timestamp.TotalMilliseconds); - trackTimer.Alpha = 1; inputTextBox.Alpha = 0; }; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 02dcad46f7..a37d3763a5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1275,16 +1275,20 @@ namespace osu.Game.Screens.Edit return tcs.Task; } - public void HandleTimestamp(string timestamp) + public bool HandleTimestamp(string timestamp, bool notifyOnError = false) { if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) { - Schedule(() => notifications?.Post(new SimpleErrorNotification + if (notifyOnError) { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.FailedToParseEditorLink - })); - return; + Schedule(() => notifications?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.FailedToParseEditorLink + })); + } + + return false; } editorBeatmap.SelectedHitObjects.Clear(); @@ -1297,7 +1301,7 @@ namespace osu.Game.Screens.Edit if (string.IsNullOrEmpty(selection)) { clock.SeekSmoothlyTo(position); - return; + return true; } // Seek to the next closest HitObject instead @@ -1312,6 +1316,7 @@ namespace osu.Game.Screens.Edit // Delegate handling the selection to the ruleset. currentScreen.Dependencies.Get().SelectFromTimestamp(position, selection); + return true; } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 44b9a066393d8468ae5728140ce592caaca0d565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 13:00:43 +0200 Subject: [PATCH 23/91] Allow more lenient parsing of incoming timestamps --- .../Editing/EditorTimestampParserTest.cs | 43 ++++++++++++++++++ osu.Game/Online/Chat/MessageFormatter.cs | 2 +- .../Rulesets/Edit/EditorTimestampParser.cs | 44 +++++++++++++------ 3 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Tests/Editing/EditorTimestampParserTest.cs diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs new file mode 100644 index 0000000000..24ac8e32a4 --- /dev/null +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Rulesets.Edit; + +namespace osu.Game.Tests.Editing +{ + [TestFixture] + public class EditorTimestampParserTest + { + public static readonly object?[][] test_cases = + { + new object?[] { ":", false, null, null }, + new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, + new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, + new object?[] { "300", false, null, null }, + new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, + new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, + new object?[] { "1:92", false, null, null }, + new object?[] { "1:002", false, null, null }, + new object?[] { "1:02:3", true, new TimeSpan(0, 0, 1, 2, 3), null }, + new object?[] { "1:02:300", true, new TimeSpan(0, 0, 1, 2, 300), null }, + new object?[] { "1:02:3000", false, null, null }, + new object?[] { "1:02:300 ()", false, null, null }, + new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestTryParse(string timestamp, bool expectedSuccess, TimeSpan? expectedParsedTime, string? expectedSelection) + { + bool actualSuccess = EditorTimestampParser.TryParse(timestamp, out var actualParsedTime, out string? actualSelection); + + Assert.Multiple(() => + { + Assert.That(actualSuccess, Is.EqualTo(expectedSuccess)); + Assert.That(actualParsedTime, Is.EqualTo(expectedParsedTime)); + Assert.That(actualSelection, Is.EqualTo(expectedSelection)); + }); + } + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index f055633d64..77454c4775 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -271,7 +271,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(EditorTimestampParser.TIME_REGEX_STRICT, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index bdfdce432e..9c3119d8f4 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -9,13 +9,34 @@ namespace osu.Game.Rulesets.Edit { public static class EditorTimestampParser { - // 00:00:000 (...) - test - // original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 - public static readonly Regex TIME_REGEX = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); + /// + /// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat) + /// Original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + /// + /// + /// 00:00:000 (...) - test + /// + public static readonly Regex TIME_REGEX_STRICT = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); + + /// + /// Used for editor-specific context wherein we want to try as hard as we can to process user input as a timestamp. + /// + /// + /// + /// 1 - parses to 01:00:000 + /// 1:2 - parses to 01:02:000 + /// 1:02 - parses to 01:02:000 + /// 1:92 - does not parse + /// 1:02:3 - parses to 01:02:003 + /// 1:02:300 - parses to 01:02:300 + /// 1:02:300 (1,2,3) - parses to 01:02:300 with selection + /// + /// + private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3})(:(?([0-5]?\d))([:.](?\d{0,3}))?)?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { - Match match = TIME_REGEX.Match(timestamp); + Match match = time_regex_lenient.Match(timestamp); if (!match.Success) { @@ -24,16 +45,14 @@ namespace osu.Game.Rulesets.Edit return false; } - bool result = true; + int timeMin, timeSec, timeMsec; - result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin); - result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec); - result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec); + int.TryParse(match.Groups[@"minutes"].Value, out timeMin); + int.TryParse(match.Groups[@"seconds"].Value, out timeSec); + int.TryParse(match.Groups[@"milliseconds"].Value, out timeMsec); // somewhat sane limit for timestamp duration (10 hours). - result &= timeMin < 600; - - if (!result) + if (timeMin >= 600) { parsedTime = null; parsedSelection = null; @@ -42,8 +61,7 @@ namespace osu.Game.Rulesets.Edit parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec); parsedSelection = match.Groups[@"selection"].Value.Trim(); - if (!string.IsNullOrEmpty(parsedSelection)) - parsedSelection = parsedSelection[1..^1]; + parsedSelection = !string.IsNullOrEmpty(parsedSelection) ? parsedSelection[1..^1] : null; return true; } } From 623055b60a027d55643d8a12eef9a660111c5f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 16:41:13 +0200 Subject: [PATCH 24/91] Fix tests --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index 24ac8e32a4..5b9663bcfe 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -10,12 +10,12 @@ namespace osu.Game.Tests.Editing [TestFixture] public class EditorTimestampParserTest { - public static readonly object?[][] test_cases = + private static readonly object?[][] test_cases = { new object?[] { ":", false, null, null }, new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, - new object?[] { "300", false, null, null }, + new object?[] { "3000", false, null, null }, new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, From 7ee29667db991e1ff37a860283f2eff6ecd9aa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 16:46:47 +0200 Subject: [PATCH 25/91] Parse plain numbers as millisecond count when parsing timestamp --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 6 +++--- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index 5b9663bcfe..9c7fae0eaf 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -13,9 +13,9 @@ namespace osu.Game.Tests.Editing private static readonly object?[][] test_cases = { new object?[] { ":", false, null, null }, - new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, - new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, - new object?[] { "3000", false, null, null }, + new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null }, + new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null }, + new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null }, new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 9c3119d8f4..e6bce12170 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -32,10 +32,17 @@ namespace osu.Game.Rulesets.Edit /// 1:02:300 (1,2,3) - parses to 01:02:300 with selection /// /// - private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3})(:(?([0-5]?\d))([:.](?\d{0,3}))?)?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); + private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3}):(?([0-5]?\d))([:.](?\d{0,3}))?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { + if (double.TryParse(timestamp, out double msec)) + { + parsedTime = TimeSpan.FromMilliseconds(msec); + parsedSelection = null; + return true; + } + Match match = time_regex_lenient.Match(timestamp); if (!match.Success) From f764ec24cd543cc450f609a7243045ec0ac42316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 18:32:24 +0200 Subject: [PATCH 26/91] Correct xmldoc --- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index e6bce12170..92a692b94e 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Edit /// /// /// - /// 1 - parses to 01:00:000 + /// 1 - parses to 00:00:001 (bare numbers are treated as milliseconds) /// 1:2 - parses to 01:02:000 /// 1:02 - parses to 01:02:000 /// 1:92 - does not parse From 8836b98070cda3691ad432bc07097732ef7df00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 18:32:58 +0200 Subject: [PATCH 27/91] Fix new inspection after framework bump --- osu.Game/Screens/Edit/Components/TimeInfoContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 9e14ec851b..9365402c1c 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Components inputTextBox.Text = editorClock.CurrentTime.ToEditorFormattedString(); Schedule(() => { - GetContainingFocusManager().ChangeFocus(inputTextBox); + GetContainingFocusManager()!.ChangeFocus(inputTextBox); inputTextBox.SelectAll(); }); }; From ce4567f87b3abd3e436212a6177ee5c960d2bb56 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 20:46:55 +0200 Subject: [PATCH 28/91] adjust rotation bounds based on grid type --- .../Edit/OsuGridToolboxGroup.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 76e735449a..8cffdfbe1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit /// public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) { - MinValue = -45f, - MaxValue = 45f, + MinValue = -180f, + MaxValue = 180f, Precision = 1f }; @@ -191,6 +191,26 @@ namespace osu.Game.Rulesets.Osu.Edit gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); + + GridType.BindValueChanged(v => + { + GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle; + + switch (v.NewValue) + { + case PositionSnapGridType.Square: + GridLinesRotation.Value = (GridLinesRotation.Value + 405) % 90 - 45; + GridLinesRotation.MinValue = -45; + GridLinesRotation.MaxValue = 45; + break; + + case PositionSnapGridType.Triangle: + GridLinesRotation.Value = (GridLinesRotation.Value + 390) % 60 - 30; + GridLinesRotation.MinValue = -30; + GridLinesRotation.MaxValue = 30; + break; + } + }, true); } private void nextGridSize() From d5397a213974688344a43e593ce514441955724d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 20:59:14 +0200 Subject: [PATCH 29/91] fix alpha value in disabled state --- osu.Game/Graphics/UserInterface/ExpandableSlider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index a7a8561b94..4cc77e218f 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -119,9 +119,14 @@ namespace osu.Game.Graphics.UserInterface Expanded.BindValueChanged(v => { label.Text = v.NewValue ? expandedLabelText : contractedLabelText; - slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint); slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); + + Current.BindDisabledChanged(disabled => + { + slider.Alpha = Expanded.Value ? disabled ? 0.3f : 1 : 0f; + }); } } From f2bd6fac47481b71650af5e2a2822dd7b8286412 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 21:10:30 +0200 Subject: [PATCH 30/91] fix codefactor --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 8cffdfbe1d..73ecb2fe7c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -199,13 +199,13 @@ namespace osu.Game.Rulesets.Osu.Edit switch (v.NewValue) { case PositionSnapGridType.Square: - GridLinesRotation.Value = (GridLinesRotation.Value + 405) % 90 - 45; + GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45; GridLinesRotation.MinValue = -45; GridLinesRotation.MaxValue = 45; break; case PositionSnapGridType.Triangle: - GridLinesRotation.Value = (GridLinesRotation.Value + 390) % 60 - 30; + GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30; GridLinesRotation.MinValue = -30; GridLinesRotation.MaxValue = 30; break; From 74399542d2e72c4bff4d7b6155202477b9bc5567 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 20 Jun 2024 17:27:15 +0200 Subject: [PATCH 31/91] Use math instead of hardcoded constant values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Edit/Compose/Components/TriangularPositionSnapGrid.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index 93d2c6a74a..91aea1de8d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -29,9 +29,9 @@ namespace osu.Game.Screens.Edit.Compose.Components GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); } - private const float sqrt3 = 1.73205080757f; - private const float sqrt3_over2 = 0.86602540378f; - private const float one_over_sqrt3 = 0.57735026919f; + private static readonly float sqrt3 = float.Sqrt(3); + private static readonly float sqrt3_over2 = sqrt3 / 2; + private static readonly float one_over_sqrt3 = 1 / sqrt3; protected override void CreateContent() { From 4c6741e8aaba74b4cd47430ee0596940b5306fe4 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 20 Jun 2024 17:27:38 +0200 Subject: [PATCH 32/91] Fix exception type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index fb2e3fec27..c553f9d640 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit break; default: - throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); + throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type."); } // Bind the start position to the toolbox sliders. From 8f0198ba0f775be29a79ab194c8cb316d2f1a050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 15:42:10 +0200 Subject: [PATCH 33/91] Add test coverage for encode-after-decode stability of slider sample volume specs --- .../per-slider-node-sample-settings.osu | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Tests/Resources/per-slider-node-sample-settings.osu diff --git a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu new file mode 100644 index 0000000000..2f56465d90 --- /dev/null +++ b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu @@ -0,0 +1,27 @@ +osu file format v128 + +[General] +SampleSet: Normal + +[TimingPoints] +15,1000,4,1,0,100,1,0 +2271,-100,4,1,0,5,0,0 +6021,-100,4,1,0,100,0,0 +9515,-100,4,1,0,5,0,0 +9521,-100,4,1,0,100,0,0 +10265,-100,4,1,0,5,0,0 +13765,-100,4,1,0,100,0,0 +13771,-100,4,1,0,5,0,0 +14770,-100,4,1,0,50,0,0 +18264,-100,4,1,0,100,0,0 +18270,-100,4,1,0,50,0,0 +21764,-100,4,1,0,5,0,0 +21770,-100,4,1,0,50,0,0 +25264,-100,4,1,0,100,0,0 +25270,-100,4,1,0,50,0,0 + +[HitObjects] +113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0: +82,206,6015,2,0,L|457:204,1,350,0|0,2:0|2:0,2:0:0:0: +75,310,10265,2,0,L|435:312,1,350,0|0,3:0|3:0,3:0:0:0: +75,310,14764,2,0,L|435:312,3,350,0|0|0|0,3:0|3:0|3:0|3:0,3:0:0:0: From fff27e619d41802702722f7426b8000c5f1ebd6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 14:27:14 +0200 Subject: [PATCH 34/91] Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon. --- .../Formats/LegacyBeatmapDecoderTest.cs | 13 ++++++-- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 16 ++++++---- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 32 +++++++++++++++---- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a4cd888823..19378821b3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -528,8 +528,17 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); - // The control point at the end time of the slider should be applied - Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + // The fourth object is a slider. + // `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider + // (such as ticks, slider slide sounds, etc.) + // Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider. + Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + + // That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc. + // Therefore, they should be read at the time instant correspondent to the given node. + // This means that the tail should use bank 8 rather than 11. + Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First()); + Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index c2f4097889..5fa85f189c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Formats /// /// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319 /// - private const double control_point_leniency = 5; + public const double CONTROL_POINT_LENIENCY = 5; internal static RulesetStore? RulesetStore; @@ -160,20 +160,24 @@ namespace osu.Game.Beatmaps.Formats private void applySamples(HitObject hitObject) { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT; - - hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); - if (hitObject is IHasRepeats hasRepeats) { + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT; + hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) { - double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency; + double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + CONTROL_POINT_LENIENCY; var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT; hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList(); } } + else + { + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT; + hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); + } } /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 186b565c39..09e3150359 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -282,19 +282,39 @@ namespace osu.Game.Beatmaps.Formats { foreach (var hitObject in hitObjects) { - if (hitObject.Samples.Count > 0) + if (hitObject is IHasRepeats hasNodeSamples) { - int volume = hitObject.Samples.Max(o => o.Volume); - int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) - ? hitObject.Samples.OfType().Max(o => o.CustomSampleBank) - : -1; + double spanDuration = hasNodeSamples.Duration / hasNodeSamples.SpanCount(); - yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex }; + for (int i = 0; i < hasNodeSamples.NodeSamples.Count; ++i) + { + double nodeTime = hitObject.StartTime + i * spanDuration; + + if (hasNodeSamples.NodeSamples[i].Any()) + yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); + + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1) + yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); + } + } + else if (hitObject.Samples.Count > 0) + { + yield return createSampleControlPointFor(hitObject.GetEndTime(), hitObject.Samples); } foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) yield return nested; } + + SampleControlPoint createSampleControlPointFor(double time, IList samples) + { + int volume = samples.Max(o => o.Volume); + int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) + ? samples.OfType().Max(o => o.CustomSampleBank) + : -1; + + return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex }; + } } void extractSampleControlPoints(IEnumerable hitObject) From 847946937ed993a0784c108411140b3202c5da19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 16:56:43 +0200 Subject: [PATCH 35/91] Fix test failures --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 09e3150359..54f23d8ecc 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -290,10 +290,10 @@ namespace osu.Game.Beatmaps.Formats { double nodeTime = hitObject.StartTime + i * spanDuration; - if (hasNodeSamples.NodeSamples[i].Any()) + if (hasNodeSamples.NodeSamples[i].Count > 0) yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); - if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1) + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0) yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); } } From 55b80f70f68f1c4040fb6f3e4558de79907c9b27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 18:12:20 +0900 Subject: [PATCH 36/91] Change "playfield" skin layer to respect shifting playfield border in osu! ruleset --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index b39fc34d5d..df7f279656 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer(); + public override Quad SkinnableComponentScreenSpaceDrawQuad => playfieldBorder.ScreenSpaceDrawQuad; + private readonly Container judgementAboveHitObjectLayer; public OsuPlayfield() From deeb2e99a2711ab9e9ef7bbd06249fb1723eab01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 10:46:17 +0200 Subject: [PATCH 37/91] Add test for correct juice stream tick counts in editor cda9440a296304bd710c1787436ea1a948f6c999 inadvertently fixes this in the most frequent case by inverting the `TickDistanceMultiplier` from being not-1 to 1 on beatmap versions above v8. This can still potentially go wrong if a beatmap from a version below v8 is edited, because upon save it will be reencoded at the latest version, meaning that the multiplier will change from not-1 to 1 - but this can be handled separately. --- .../Editor/TestSceneCatchEditorSaving.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs new file mode 100644 index 0000000000..53ef24e02c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public partial class TestSceneCatchEditorSaving : EditorSavingTestScene + { + protected override Ruleset CreateRuleset() => new CatchRuleset(); + + [Test] + public void TestCatchJuiceStreamTickCorrect() + { + AddStep("enter timing mode", () => InputManager.Key(Key.F3)); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("enter compose mode", () => InputManager.Key(Key.F1)); + + Vector2 startPoint = Vector2.Zero; + float increment = 0; + + AddUntilStep("wait for playfield", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("move to centre", () => + { + var playfield = this.ChildrenOfType().Single(); + startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3); + increment = playfield.ScreenSpaceDrawQuad.Height / 10; + InputManager.MoveMouseTo(startPoint); + }); + AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment))); + AddStep("add node", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment))); + AddStep("add node", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1)); + + int largeDropletCount = 0, tinyDropletCount = 0; + AddStep("store droplet count", () => + { + largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)); + tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)); + }); + + SaveEditor(); + ReloadEditorToSameBeatmap(); + + AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount)); + AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount)); + } + } +} From 960d552dc1c1f2c34f6ac4cb79f652dce8fe70b5 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 28 Jun 2024 19:43:45 +0800 Subject: [PATCH 38/91] Initial implemention of the No Release mod This commit adds a new osu!mania mod No Release that relaxes tail judgements. The current implementation automatically awards Perfect (or Meh if the hold note is broken midway) for a hold note tail at the end of its Perfect window, as long as it is held by then. Tests are pending for the next commit. --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + .../Mods/ManiaModNoRelease.cs | 35 +++++++++++++++++++ .../Objects/Drawables/DrawableHoldNoteTail.cs | 24 +++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 40eb44944c..667002533d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -241,6 +241,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModEasy(), new ManiaModNoFail(), new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), + new ManiaModNoRelease(), }; case ModType.DifficultyIncrease: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs new file mode 100644 index 0000000000..f370ef15bd --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModNoRelease : Mod, IApplicableToDrawableHitObject + { + public override string Name => "No Release"; + + public override string Acronym => "NR"; + + public override LocalisableString Description => "No more timing the end of hold notes."; + + public override double ScoreMultiplier => 0.9; + + public override ModType Type => ModType.DifficultyReduction; + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (drawable is DrawableHoldNote hold) + { + hold.HitObjectApplied += dho => + { + ((DrawableHoldNote)dho).Tail.LateReleaseResult = HitResult.Perfect; + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 79002b3819..eb1637b0ea 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; + /// + /// The minimum uncapped result for a late release. + /// + public HitResult LateReleaseResult { get; set; } = HitResult.Miss; + public DrawableHoldNoteTail() : this(null) { @@ -32,9 +38,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) => + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + Debug.Assert(HitObject.HitWindows != null); + // Factor in the release lenience - base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); + double scaledTimeOffset = timeOffset / TailNote.RELEASE_WINDOW_LENIENCE; + + // Check for late release + if (HoldNote.HoldStartTime != null && scaledTimeOffset > HitObject.HitWindows.WindowFor(LateReleaseResult)) + { + ApplyResult(GetCappedResult(LateReleaseResult)); + } + else + { + base.CheckForResult(userTriggered, scaledTimeOffset); + } + } protected override HitResult GetCappedResult(HitResult result) { From 679f4735b34eae83e8d9ef48a3fd7ee5270d19e0 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 29 Jun 2024 16:08:40 +0800 Subject: [PATCH 39/91] add tests for No Release mod The new test scene is essentially a copy of TestSceneHoldNoteInput, modified to test the judgement changes applied by the new mod. A base class might need to be abstracted out for them. --- .../Mods/TestSceneManiaModNoRelease.cs | 644 ++++++++++++++++++ .../TestSceneHoldNoteInput.cs | 2 +- 2 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs new file mode 100644 index 0000000000..11dcdd4f8d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -0,0 +1,644 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene + { + private const double time_before_head = 250; + private const double time_head = 1500; + private const double time_during_hold_1 = 2500; + private const double time_tail = 4000; + private const double time_after_tail = 5250; + + private List judgementResults = new List(); + + /// + /// -----[ ]----- + /// o o + /// + [Test] + public void TestNoInput() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + assertNoteJudgement(HitResult.IgnoreMiss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestCorrectInput() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestLateRelease() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail, ManiaAction.Key1), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAtStartAndBreak() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xox o + /// + [Test] + public void TestPressAtStartThenReleaseAndImmediatelyRepress() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 1), + new ManiaReplayFrame(time_head + 2, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertComboAtJudgement(0, 1); + assertTailJudgement(HitResult.Meh); + assertComboAtJudgement(1, 0); + assertComboAtJudgement(3, 1); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// xo x o o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressDuringNoteAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o o + /// + [Test] + public void TestPressDuringNoteAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]-------------- + /// xo + /// + [Test] + public void TestPressAndReleaseJustAfterTailWithCloseByHead() + { + const int duration = 30; + + var beatmap = new Beatmap + { + HitObjects = + { + // hold note is very short, to make the head still in range + new HoldNote + { + StartTime = time_head, + Duration = duration, + Column = 0, + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head + duration + 20, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 30), + }, beatmap); + + assertHeadJudgement(HitResult.Good); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]-O------------- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead() + { + Note note; + + const int duration = 50; + + var beatmap = new Beatmap + { + HitObjects = + { + // hold note is very short, to make the head still in range + new HoldNote + { + StartTime = time_head, + Duration = duration, + Column = 0, + }, + { + // Next note within tail lenience + note = new Note + { + StartTime = time_head + duration + 10 + } + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head + duration, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 10), + }, beatmap); + + assertHeadJudgement(HitResult.Good); + assertTailJudgement(HitResult.Perfect); + + assertHitObjectJudgement(note, HitResult.Miss); + } + + /// + /// -----[ ]--O-- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustBeforeTailWithNearbyNote() + { + Note note; + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + }, + { + // Next note within tail lenience + note = new Note + { + StartTime = time_tail + 50 + } + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }, beatmap); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + + assertHitObjectJudgement(note, HitResult.Good); + } + + /// + /// -----[ ]----- + /// xo + /// + [Test] + public void TestPressAndReleaseJustAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 30), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]--O-- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustAfterTailWithNearbyNote() + { + // Next note within tail lenience + Note note = new Note { StartTime = time_tail + 50 }; + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + }, + note + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 20), + }, beatmap); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + + assertHitObjectJudgement(note, HitResult.Great); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 10), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + [Test] + public void TestMissReleaseAndHitSecondRelease() + { + var windows = new ManiaHitWindows(); + windows.SetDifficulty(10); + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 500, + Column = 0, + }, + new HoldNote + { + StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10, + Duration = 500, + Column = 0, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 10, + }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()), + }, beatmap); + + AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => !j.Type.IsHit())); + + AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type.IsHit())); + } + + [Test] + public void TestZeroLength() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 0, + Column = 0, + }, + }, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1), + }, beatmap); + + AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type.IsHit())); + } + + private void assertHitObjectJudgement(HitObject hitObject, HitResult result) + => AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result)); + + private void assertHeadJudgement(HitResult result) + => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result)); + + private void assertTailJudgement(HitResult result) + => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result)); + + private void assertNoteJudgement(HitResult result) + => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); + + private void assertComboAtJudgement(int judgementIndex, int combo) + => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo)); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private void performTest(List frames, Beatmap? beatmap = null) + { + if (beatmap == null) + { + beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo, + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + } + + AddStep("load player", () => + { + SelectedMods.Value = new List + { + new ManiaModNoRelease() + }; + + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + 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 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, + }) + { + } + } + + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5f299f419d..ef96ddb880 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) .All(j => !j.Type.IsHit())); - AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) + AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) .All(j => j.Type.IsHit())); } From 463ab46feec22971b6a1e187cf48cba80c552ff9 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 29 Jun 2024 16:46:16 +0800 Subject: [PATCH 40/91] formatting --- .../Mods/TestSceneManiaModNoRelease.cs | 3 +-- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs index 11dcdd4f8d..82534ee019 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -523,7 +523,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods .All(j => !j.Type.IsHit())); AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type.IsHit())); + .All(j => j.Type.IsHit())); } [Test] @@ -639,6 +639,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { } } - } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index ef96ddb880..e328d23ed4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -475,7 +475,7 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => !j.Type.IsHit())); AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type.IsHit())); + .All(j => j.Type.IsHit())); } [Test] From 8bb51d5a4f08e8750dbad5ee0f97ba221be81fd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jun 2024 20:32:16 +0900 Subject: [PATCH 41/91] Fix summary timeline not correctly updating after changes to breaks Closes https://github.com/ppy/osu/issues/28678. Oops. --- .../Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 50062e8465..ed42ade490 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts breaks.BindTo(beatmap.Breaks); breaks.BindCollectionChanged((_, _) => { + Clear(); foreach (var breakPeriod in beatmap.Breaks) Add(new BreakVisualisation(breakPeriod)); }, true); From 4cb58fbe474c3c0663fb19284ae6de23ca53b2d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Jul 2024 14:58:32 +0900 Subject: [PATCH 42/91] Add failing test --- .../Mods/TestSceneManiaModInvert.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 2977241dc6..95fe73db50 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Tests.Visual; @@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Mod = new ManiaModInvert(), PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); + + [Test] + public void TestBreaksPreservedOnOriginalBeatmap() + { + var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); + beatmap.Breaks.Clear(); + beatmap.Breaks.Add(new BreakPeriod(0, 1000)); + + var workingBeatmap = new FlatWorkingBeatmap(beatmap); + + var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() }); + Assert.That(playableWithInvert.Breaks.Count, Is.Zero); + + var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo); + Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero); + Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000))); + } } } From f942595829336ce1334ec86b88fb04be9068412c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Jul 2024 14:58:53 +0900 Subject: [PATCH 43/91] Fix `ManiaModInvert` permanently messing up the beatmap --- osu.Game/Beatmaps/BeatmapConverter.cs | 5 +++++ osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 7 ++++++- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b68c80d4b3..0ec8eab5d8 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Framework.Bindables; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -49,6 +51,9 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = new BindableList(original.Breaks); + return ConvertBeatmap(original, cancellationToken); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 072e246a36..d8a2560559 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - BindableList Breaks { get; } + BindableList Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 4c6a4cc9c2..97037302c6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -328,7 +328,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public BindableList Breaks => baseBeatmap.Breaks; + public BindableList Breaks + { + get => baseBeatmap.Breaks; + set => baseBeatmap.Breaks = value; + } + public List UnhandledEventLines => baseBeatmap.UnhandledEventLines; public double TotalBreakTime => baseBeatmap.TotalBreakTime; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index ae0fd9130f..331da51888 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -172,7 +172,11 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.ControlPointInfo = value; } - public BindableList Breaks => PlayableBeatmap.Breaks; + public BindableList Breaks + { + get => PlayableBeatmap.Breaks; + set => PlayableBeatmap.Breaks = value; + } public List UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; From 1eb10e029caf98aa112800a3f16c8783be44ed73 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 1 Jul 2024 19:34:33 +0800 Subject: [PATCH 44/91] Rewrite no release mod Per the request of spaceman_atlas, the No Release mod is rewritten to avoid modifications to DrawableHoldNoteTail. The approach is based on that of the Strict Tracking mod for the osu!(standard) ruleset, injecting the mod behavior by replacing the normal hold note with the mod's variant. The variant inherits most bevaior from the normal hold note, but when creating nested hitobjects, it creates its own hold note tail variant instead, which in turn is used to instantiate the mod's variant of DrawableHoldNoteTail with a new behavior. The time a judgement is awarded is changed from the end of its Perfect window to the time of the tail itself. --- .../Mods/TestSceneManiaModNoRelease.cs | 10 +-- .../Mods/ManiaModNoRelease.cs | 86 +++++++++++++++++-- .../Objects/Drawables/DrawableHoldNoteTail.cs | 24 +----- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 6 +- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs index 82534ee019..f6e79114de 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods /// /// -----[ ]-------------- - /// xo + /// xo /// [Test] - public void TestPressAndReleaseJustAfterTailWithCloseByHead() + public void TestPressAndReleaseAfterTailWithCloseByHead() { const int duration = 30; @@ -301,11 +301,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods performTest(new List { - new ManiaReplayFrame(time_head + duration + 20, ManiaAction.Key1), - new ManiaReplayFrame(time_head + duration + 30), + new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 70), }, beatmap); - assertHeadJudgement(HitResult.Good); + assertHeadJudgement(HitResult.Ok); assertTailJudgement(HitResult.Perfect); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index f370ef15bd..8cb2e821e6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -1,15 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using System.Threading; using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNoRelease : Mod, IApplicableToDrawableHitObject + public partial class ManiaModNoRelease : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset { public override string Name => "No Release"; @@ -21,14 +27,80 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.DifficultyReduction; - public void ApplyToDrawableHitObject(DrawableHitObject drawable) + public void ApplyToBeatmap(IBeatmap beatmap) { - if (drawable is DrawableHoldNote hold) + var maniaBeatmap = (ManiaBeatmap)beatmap; + var hitObjects = maniaBeatmap.HitObjects.Select(obj => { - hold.HitObjectApplied += dho => + if (obj is HoldNote hold) + return new NoReleaseHoldNote(hold); + + return obj; + }).ToList(); + + maniaBeatmap.HitObjects = hitObjects; + } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var maniaRuleset = (DrawableManiaRuleset)drawableRuleset; + + foreach (var stage in maniaRuleset.Playfield.Stages) + { + foreach (var column in stage.Columns) { - ((DrawableHoldNote)dho).Tail.LateReleaseResult = HitResult.Perfect; - }; + column.RegisterPool(10, 50); + } + } + } + + private partial class NoReleaseDrawableHoldNoteTail : DrawableHoldNoteTail + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + // apply perfect once the tail is reached + if (HoldNote.HoldStartTime != null && timeOffset >= 0) + ApplyResult(GetCappedResult(HitResult.Perfect)); + else + base.CheckForResult(userTriggered, timeOffset); + } + } + + private class NoReleaseTailNote : TailNote + { + } + + private class NoReleaseHoldNote : HoldNote + { + public NoReleaseHoldNote(HoldNote hold) + { + StartTime = hold.StartTime; + Duration = hold.Duration; + Column = hold.Column; + NodeSamples = hold.NodeSamples; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(Head = new HeadNote + { + StartTime = StartTime, + Column = Column, + Samples = GetNodeSamples(0), + }); + + AddNested(Tail = new NoReleaseTailNote + { + StartTime = EndTime, + Column = Column, + Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), + }); + + AddNested(Body = new HoldNoteBody + { + StartTime = StartTime, + Column = Column + }); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index eb1637b0ea..79002b3819 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -19,11 +18,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; - /// - /// The minimum uncapped result for a late release. - /// - public HitResult LateReleaseResult { get; set; } = HitResult.Miss; - public DrawableHoldNoteTail() : this(null) { @@ -38,23 +32,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - + protected override void CheckForResult(bool userTriggered, double timeOffset) => // Factor in the release lenience - double scaledTimeOffset = timeOffset / TailNote.RELEASE_WINDOW_LENIENCE; - - // Check for late release - if (HoldNote.HoldStartTime != null && scaledTimeOffset > HitObject.HitWindows.WindowFor(LateReleaseResult)) - { - ApplyResult(GetCappedResult(LateReleaseResult)); - } - else - { - base.CheckForResult(userTriggered, scaledTimeOffset); - } - } + base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); protected override HitResult GetCappedResult(HitResult result) { diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 3f930a310b..6be0ee2d6b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -72,18 +72,18 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The head note of the hold. /// - public HeadNote Head { get; private set; } + public HeadNote Head { get; protected set; } /// /// The tail note of the hold. /// - public TailNote Tail { get; private set; } + public TailNote Tail { get; protected set; } /// /// The body of the hold. /// This is an invisible and silent object that tracks the holding state of the . /// - public HoldNoteBody Body { get; private set; } + public HoldNoteBody Body { get; protected set; } public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; From 005af280f2ee8c05adb0bc0b5609b52eb40900fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:13:20 +0900 Subject: [PATCH 45/91] Isolate bindable breaks list to `EditorBeatmap` --- .../TestSceneEditorBeatmapProcessor.cs | 85 +++++++++++-------- osu.Game/Beatmaps/Beatmap.cs | 3 +- osu.Game/Beatmaps/BeatmapConverter.cs | 4 +- osu.Game/Beatmaps/IBeatmap.cs | 3 +- .../Difficulty/DifficultyCalculator.cs | 3 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 8 +- .../Screens/Edit/EditorBeatmapProcessor.cs | 6 +- 7 files changed, 66 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 1a3f0aa3df..251099c0e2 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -21,10 +21,11 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, - }; + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -38,14 +39,15 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -59,15 +61,16 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 2000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -81,14 +84,15 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, Duration = 10000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); beatmapProcessor.PreProcess(); @@ -102,16 +106,17 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, Duration = 10000 }, new Note { StartTime = 2000 }, new Note { StartTime = 12000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); beatmapProcessor.PreProcess(); @@ -125,15 +130,16 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 5000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -152,9 +158,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -165,7 +172,7 @@ namespace osu.Game.Tests.Editing new BreakPeriod(1200, 4000), new BreakPeriod(5200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -184,9 +191,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -197,7 +205,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -218,9 +226,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1100 }, @@ -230,7 +239,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -249,9 +258,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -262,7 +272,7 @@ namespace osu.Game.Tests.Editing new ManualBreakPeriod(1200, 4000), new ManualBreakPeriod(5200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -283,9 +293,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -296,7 +307,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -317,9 +328,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -329,7 +341,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(1200, 8800), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -348,9 +360,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -360,7 +373,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -374,9 +387,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -386,7 +400,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -400,9 +414,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, EndTime = 20000 }, @@ -412,7 +427,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -426,9 +441,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 10000 }, @@ -438,7 +454,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(0, 9000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -452,9 +468,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 10000 }, @@ -464,7 +481,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(0, 9000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 510410bc09..ae77e4adcf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps @@ -62,7 +61,7 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - public BindableList Breaks { get; set; } = new BindableList(); + public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 0ec8eab5d8..676eb1b159 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using osu.Framework.Bindables; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -52,7 +50,7 @@ namespace osu.Game.Beatmaps original.ControlPointInfo = original.ControlPointInfo.DeepClone(); // Used in osu!mania conversion. - original.Breaks = new BindableList(original.Breaks); + original.Breaks = original.Breaks.ToList(); return ConvertBeatmap(original, cancellationToken); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index d8a2560559..0d39c1f977 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; @@ -41,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - BindableList Breaks { get; set; } + List Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 97037302c6..722263c58e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio.Track; -using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -328,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public BindableList Breaks + public List Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 331da51888..1ebd2e6337 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -110,6 +110,9 @@ namespace osu.Game.Screens.Edit foreach (var obj in HitObjects) trackStartTime(obj); + Breaks = new BindableList(playableBeatmap.Breaks); + Breaks.BindCollectionChanged((_, _) => playableBeatmap.Breaks = Breaks.ToList()); + PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime); PreviewTime.BindValueChanged(s => { @@ -172,7 +175,9 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.ControlPointInfo = value; } - public BindableList Breaks + public readonly BindableList Breaks; + + List IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; @@ -191,6 +196,7 @@ namespace osu.Game.Screens.Edit public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + private IList mutableBreaks => (IList)PlayableBeatmap.Breaks; private readonly List batchPendingInserts = new List(); diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 99c8c3572b..377e978c4a 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -12,11 +12,13 @@ namespace osu.Game.Screens.Edit { public class EditorBeatmapProcessor : IBeatmapProcessor { - public IBeatmap Beatmap { get; } + public EditorBeatmap Beatmap { get; } + + IBeatmap IBeatmapProcessor.Beatmap => Beatmap; private readonly IBeatmapProcessor? rulesetBeatmapProcessor; - public EditorBeatmapProcessor(IBeatmap beatmap, Ruleset ruleset) + public EditorBeatmapProcessor(EditorBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap); From f694ae416eba180f03c55a0374018a2ccf53a593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:47:40 +0900 Subject: [PATCH 46/91] Fix typo in xmldoc --- osu.Game/Beatmaps/IBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 0d39c1f977..176738489a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps /// /// All lines from the [Events] section which aren't handled in the encoding process yet. - /// These lines shoule be written out to the beatmap file on save or export. + /// These lines should be written out to the beatmap file on save or export. /// List UnhandledEventLines { get; } From 2c3b411bb5e77696a29abf5fdeb873b1d72c47a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:59:24 +0900 Subject: [PATCH 47/91] Change breaks list to `IReadOnlyList` --- .../Mods/TestSceneManiaModInvert.cs | 7 +++++-- osu.Game/Beatmaps/Beatmap.cs | 6 ++++++ osu.Game/Beatmaps/BeatmapConverter.cs | 7 +++---- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +++++- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 95fe73db50..576b07265b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; @@ -24,8 +25,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestBreaksPreservedOnOriginalBeatmap() { var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); - beatmap.Breaks.Clear(); - beatmap.Breaks.Add(new BreakPeriod(0, 1000)); + var breaks = (List)beatmap.Breaks; + + breaks.Clear(); + breaks.Add(new BreakPeriod(0, 1000)); var workingBeatmap = new FlatWorkingBeatmap(beatmap); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ae77e4adcf..b185c1cd5a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -61,6 +61,12 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); + IReadOnlyList IBeatmap.Breaks + { + get => Breaks; + set => Breaks = new List(value); + } + public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 676eb1b159..e62de3e69b 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -49,9 +50,6 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); - // Used in osu!mania conversion. - original.Breaks = original.Breaks.ToList(); - return ConvertBeatmap(original, cancellationToken); } @@ -68,7 +66,8 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - beatmap.Breaks = original.Breaks; + // Used in osu!mania conversion. + beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 176738489a..151edc9ad8 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - List Breaks { get; set; } + IReadOnlyList Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 17c2c8c88d..62f6f5d30d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -64,8 +65,11 @@ namespace osu.Game.Database foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); + var breaks = new List(playableBeatmap.Breaks.Count); for (int i = 0; i < playableBeatmap.Breaks.Count; i++) - playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime)); + breaks.Add(new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime))); + + playableBeatmap.Breaks = breaks; foreach (var hitObject in playableBeatmap.HitObjects) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 722263c58e..7262b9d1a8 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public List Breaks + public IReadOnlyList Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 1ebd2e6337..cc1d820427 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Edit public readonly BindableList Breaks; - List IBeatmap.Breaks + IReadOnlyList IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; From f69bc40a4b465a6f6902d2038fcf824fb7b638bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 12:07:13 +0900 Subject: [PATCH 48/91] Move break cloning back to non-virtual method --- osu.Game/Beatmaps/BeatmapConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index e62de3e69b..5fd20d5aff 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -49,6 +49,8 @@ namespace osu.Game.Beatmaps // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = new List(original.Breaks); return ConvertBeatmap(original, cancellationToken); } @@ -66,8 +68,6 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - // Used in osu!mania conversion. - beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; From db847112149ae8e27d5960f2a7909acb5c91f480 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:16:10 +0900 Subject: [PATCH 49/91] Revert "Move break cloning back to non-virtual method" This reverts commit f69bc40a4b465a6f6902d2038fcf824fb7b638bb. --- osu.Game/Beatmaps/BeatmapConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 5fd20d5aff..e62de3e69b 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -49,8 +49,6 @@ namespace osu.Game.Beatmaps // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); - // Used in osu!mania conversion. - original.Breaks = new List(original.Breaks); return ConvertBeatmap(original, cancellationToken); } @@ -68,6 +66,8 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); + // Used in osu!mania conversion. + beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; From 04da1209f7d4caff3bc5689bea9c53d4eb2e1c0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:16:11 +0900 Subject: [PATCH 50/91] Revert "Change breaks list to `IReadOnlyList`" This reverts commit 2c3b411bb5e77696a29abf5fdeb873b1d72c47a7. --- .../Mods/TestSceneManiaModInvert.cs | 7 ++----- osu.Game/Beatmaps/Beatmap.cs | 6 ------ osu.Game/Beatmaps/BeatmapConverter.cs | 7 ++++--- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +----- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 7 files changed, 10 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 576b07265b..95fe73db50 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; @@ -25,10 +24,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestBreaksPreservedOnOriginalBeatmap() { var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); - var breaks = (List)beatmap.Breaks; - - breaks.Clear(); - breaks.Add(new BreakPeriod(0, 1000)); + beatmap.Breaks.Clear(); + beatmap.Breaks.Add(new BreakPeriod(0, 1000)); var workingBeatmap = new FlatWorkingBeatmap(beatmap); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index b185c1cd5a..ae77e4adcf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -61,12 +61,6 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - IReadOnlyList IBeatmap.Breaks - { - get => Breaks; - set => Breaks = new List(value); - } - public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index e62de3e69b..676eb1b159 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -50,6 +49,9 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = original.Breaks.ToList(); + return ConvertBeatmap(original, cancellationToken); } @@ -66,8 +68,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - // Used in osu!mania conversion. - beatmap.Breaks = new List(original.Breaks); + beatmap.Breaks = original.Breaks; beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 151edc9ad8..176738489a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - IReadOnlyList Breaks { get; set; } + List Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 62f6f5d30d..17c2c8c88d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -65,11 +64,8 @@ namespace osu.Game.Database foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); - var breaks = new List(playableBeatmap.Breaks.Count); for (int i = 0; i < playableBeatmap.Breaks.Count; i++) - breaks.Add(new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime))); - - playableBeatmap.Breaks = breaks; + playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime)); foreach (var hitObject in playableBeatmap.HitObjects) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 7262b9d1a8..722263c58e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public IReadOnlyList Breaks + public List Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index cc1d820427..1ebd2e6337 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Edit public readonly BindableList Breaks; - IReadOnlyList IBeatmap.Breaks + List IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; From 31edca866c6d921cc0f628f4074aece1a63f8c75 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:21:24 +0900 Subject: [PATCH 51/91] Remove unused code --- osu.Game/Screens/Edit/EditorBeatmap.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 1ebd2e6337..c8592b5bea 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -196,7 +196,6 @@ namespace osu.Game.Screens.Edit public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; - private IList mutableBreaks => (IList)PlayableBeatmap.Breaks; private readonly List batchPendingInserts = new List(); From d4a8f6c8b085e6bf8f3ed17c038c8d13b1ab76b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 09:12:34 +0200 Subject: [PATCH 52/91] Do not add extra sample control point after end of `IHasRepeats` objects --- .../per-slider-node-sample-settings.osu | 19 +++++++------------ .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu index 2f56465d90..8b10f21f52 100644 --- a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu +++ b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu @@ -1,4 +1,4 @@ -osu file format v128 +osu file format v128 [General] SampleSet: Normal @@ -7,18 +7,13 @@ SampleSet: Normal 15,1000,4,1,0,100,1,0 2271,-100,4,1,0,5,0,0 6021,-100,4,1,0,100,0,0 -9515,-100,4,1,0,5,0,0 -9521,-100,4,1,0,100,0,0 -10265,-100,4,1,0,5,0,0 -13765,-100,4,1,0,100,0,0 -13771,-100,4,1,0,5,0,0 +8515,-100,4,1,0,5,0,0 +12765,-100,4,1,0,100,0,0 +14764,-100,4,1,0,5,0,0 14770,-100,4,1,0,50,0,0 -18264,-100,4,1,0,100,0,0 -18270,-100,4,1,0,50,0,0 -21764,-100,4,1,0,5,0,0 -21770,-100,4,1,0,50,0,0 -25264,-100,4,1,0,100,0,0 -25270,-100,4,1,0,50,0,0 +17264,-100,4,1,0,5,0,0 +17270,-100,4,1,0,50,0,0 +22264,-100,4,1,0,100,0,0 [HitObjects] 113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0: diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 54f23d8ecc..8a8964ccd4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -293,7 +293,7 @@ namespace osu.Game.Beatmaps.Formats if (hasNodeSamples.NodeSamples[i].Count > 0) yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); - if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0) + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0 && i < hasNodeSamples.NodeSamples.Count - 1) yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); } } From 1e4db77925ddcd7dadd899fc6c0db98058276569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 13:32:33 +0200 Subject: [PATCH 53/91] Implement autoplay toggle for editor test play Contains some hacks to fix weird behaviours like rewinding to the start on enabling autoplay, or gameplay cursor hiding. --- .../Input/Bindings/GlobalActionContainer.cs | 4 ++ .../GlobalActionKeyBindingStrings.cs | 5 ++ .../Screens/Edit/GameplayTest/EditorPlayer.cs | 47 ++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2452852f6f..6e2d0eb25e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -143,6 +143,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), }; private static IEnumerable inGameKeyBindings => new[] @@ -432,6 +433,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] EditorToggleScaleControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))] + EditorTestPlayToggleAutoplay, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 2e44b96625..735d82d9f5 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -374,6 +374,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); + /// + /// "Test play: Toggle autoplay" + /// + public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + /// /// "Increase mod speed" /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 69851d0d35..e70b0419ca 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -4,17 +4,21 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Play; using osu.Game.Users; namespace osu.Game.Screens.Edit.GameplayTest { - public partial class EditorPlayer : Player + public partial class EditorPlayer : Player, IKeyBindingHandler { private readonly Editor editor; private readonly EditorState editorState; @@ -133,6 +137,47 @@ namespace osu.Game.Screens.Edit.GameplayTest protected override bool CheckModsAllowFailure() => false; // never fail. + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.EditorTestPlayToggleAutoplay: + toggleAutoplay(); + return true; + + default: + return false; + } + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private void toggleAutoplay() + { + if (DrawableRuleset.ReplayScore == null) + { + var autoplay = Ruleset.Value.CreateInstance().GetAutoplayMod(); + if (autoplay == null) + return; + + var score = autoplay.CreateScoreFromReplayData(GameplayState.Beatmap, [autoplay]); + + // remove past frames to prevent replay frame handler from seeking back to start in an attempt to play back the entirety of the replay. + score.Replay.Frames.RemoveAll(f => f.Time <= GameplayClockContainer.CurrentTime); + + DrawableRuleset.SetReplayScore(score); + // Without this schedule, the `GlobalCursorDisplay.Update()` machinery will fade the gameplay cursor out, but we still want it to show. + Schedule(() => DrawableRuleset.Cursor?.Show()); + } + else + DrawableRuleset.SetReplayScore(null); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From e28befb98def4411de3fe3e4d0fee09523b97571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:00:18 +0200 Subject: [PATCH 54/91] Implement quick pause toggle for editor test play --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++++- .../Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 12 ++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6e2d0eb25e..c8afacde67 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -100,7 +100,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), - new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), @@ -118,6 +117,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), }; private static IEnumerable editorKeyBindings => new[] @@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause), }; private static IEnumerable inGameKeyBindings => new[] @@ -436,6 +437,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))] EditorTestPlayToggleAutoplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))] + EditorTestPlayToggleQuickPause, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 735d82d9f5..c039c160d0 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -379,6 +379,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + /// + /// "Test play: Toggle quick pause" + /// + public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Test play: Toggle quick pause"); + /// /// "Increase mod speed" /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index e70b0419ca..7bcc3e01dd 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -148,6 +148,10 @@ namespace osu.Game.Screens.Edit.GameplayTest toggleAutoplay(); return true; + case GlobalAction.EditorTestPlayToggleQuickPause: + toggleQuickPause(); + return true; + default: return false; } @@ -178,6 +182,14 @@ namespace osu.Game.Screens.Edit.GameplayTest DrawableRuleset.SetReplayScore(null); } + private void toggleQuickPause() + { + if (GameplayClockContainer.IsPaused.Value) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From d85c467856b53fe7f7d66ef1d0fd9914ea92052a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:12:13 +0200 Subject: [PATCH 55/91] Implement quick exit hotkeys for editor test play --- .../Input/Bindings/GlobalActionContainer.cs | 19 ++++++++++++++++++- .../GlobalActionKeyBindingStrings.cs | 18 ++++++++++++++---- osu.Game/Localisation/InputSettingsStrings.cs | 5 +++++ .../Input/GlobalKeyBindingsSection.cs | 1 + .../Screens/Edit/GameplayTest/EditorPlayer.cs | 17 +++++++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c8afacde67..ef0c60cd20 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -34,6 +34,7 @@ namespace osu.Game.Input.Bindings /// public override IEnumerable DefaultKeyBindings => globalKeyBindings .Concat(editorKeyBindings) + .Concat(editorTestPlayKeyBindings) .Concat(inGameKeyBindings) .Concat(replayKeyBindings) .Concat(songSelectKeyBindings) @@ -68,6 +69,9 @@ namespace osu.Game.Input.Bindings case GlobalActionCategory.Overlays: return overlayKeyBindings; + case GlobalActionCategory.EditorTestPlay: + return editorTestPlayKeyBindings; + default: throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); } @@ -143,8 +147,14 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), + }; + + private static IEnumerable editorTestPlayKeyBindings => new[] + { new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause), + new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorTestPlayQuickExitToInitialTime), + new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorTestPlayQuickExitToCurrentTime), }; private static IEnumerable inGameKeyBindings => new[] @@ -440,6 +450,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))] EditorTestPlayToggleQuickPause, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToInitialTime))] + EditorTestPlayQuickExitToInitialTime, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))] + EditorTestPlayQuickExitToCurrentTime, } public enum GlobalActionCategory @@ -450,6 +466,7 @@ namespace osu.Game.Input.Bindings Replay, SongSelect, AudioControl, - Overlays + Overlays, + EditorTestPlay, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index c039c160d0..450585f79a 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -375,14 +375,24 @@ namespace osu.Game.Localisation public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); /// - /// "Test play: Toggle autoplay" + /// "Toggle autoplay" /// - public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Toggle autoplay"); /// - /// "Test play: Toggle quick pause" + /// "Toggle quick pause" /// - public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Test play: Toggle quick pause"); + public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Toggle quick pause"); + + /// + /// "Quick exit to initial time" + /// + public static LocalisableString EditorTestPlayQuickExitToInitialTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_initial_time"), @"Quick exit to initial time"); + + /// + /// "Quick exit to current time" + /// + public static LocalisableString EditorTestPlayQuickExitToCurrentTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_current_time"), @"Quick exit to current time"); /// /// "Increase mod speed" diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index fcfe48bedb..bc1a7e68ab 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -49,6 +49,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor"); + /// + /// "Editor: Test play" + /// + public static LocalisableString EditorTestPlaySection => new TranslatableString(getKey(@"editor_test_play_section"), @"Editor: Test play"); + /// /// "Reset all bindings in section" /// diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index 5a05d78905..e5bc6cbe8a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), + new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorTestPlaySection, GlobalActionCategory.EditorTestPlay), }); } } diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7bcc3e01dd..616d7a09b2 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -152,6 +152,14 @@ namespace osu.Game.Screens.Edit.GameplayTest toggleQuickPause(); return true; + case GlobalAction.EditorTestPlayQuickExitToInitialTime: + quickExit(false); + return true; + + case GlobalAction.EditorTestPlayQuickExitToCurrentTime: + quickExit(true); + return true; + default: return false; } @@ -190,6 +198,15 @@ namespace osu.Game.Screens.Edit.GameplayTest GameplayClockContainer.Stop(); } + private void quickExit(bool useCurrentTime) + { + if (useCurrentTime) + editorState.Time = GameplayClockContainer.CurrentTime; + + editor.RestoreState(editorState); + this.Exit(); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From 3d61a217ecee2dfb7b90b29f7772aadb1dcc09d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:21:05 +0200 Subject: [PATCH 56/91] Add test coverage --- .../Editing/TestSceneEditorTestGameplay.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 7dcb8766dd..23efb40d3f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; @@ -224,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); } + [Test] + public void TestAutoplayToggle() + { + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null); + AddStep("press Tab", () => InputManager.Key(Key.Tab)); + AddUntilStep("replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Not.Null); + AddStep("press Tab", () => InputManager.Key(Key.Tab)); + AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null); + AddStep("exit player", () => editorPlayer.Exit()); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + } + + [Test] + public void TestQuickPause() + { + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False); + AddStep("press Ctrl-P", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.P); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.True); + AddStep("press Ctrl-P", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.P); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False); + AddStep("exit player", () => editorPlayer.Exit()); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + } + + [Test] + public void TestQuickExitAtInitialPosition() + { + AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + + GameplayClockContainer gameplayClockContainer = null; + AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First()); + AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning); + // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor... + AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000); + + AddWaitStep("wait some", 5); + + AddStep("exit player", () => InputManager.PressKey(Key.F1)); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); + } + + [Test] + public void TestQuickExitAtCurrentPosition() + { + AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + + GameplayClockContainer gameplayClockContainer = null; + AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First()); + AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning); + // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor... + AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000); + + AddWaitStep("wait some", 5); + + AddStep("exit player", () => InputManager.PressKey(Key.F2)); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000)); + } + public override void TearDownSteps() { base.TearDownSteps(); From 9414aec8bfe19a72b52018e4de3418893a8a3335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:55:04 +0200 Subject: [PATCH 57/91] Add capability to remove breaks via context menu --- .../Components/Timeline/TimelineBreak.cs | 37 ++++++++++++++++++- .../Timeline/TimelineBreakDisplay.cs | 5 ++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 025eb8bede..7f64436267 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -9,20 +9,31 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public partial class TimelineBreak : CompositeDrawable + public partial class TimelineBreak : CompositeDrawable, IHasContextMenu { public Bindable Break { get; } = new Bindable(); + public Action? OnDeleted { get; init; } + + private Box background = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + public TimelineBreak(BreakPeriod b) { Break.Value = b; @@ -42,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 5 }, - Child = new Box + Child = background = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Gray5, @@ -77,6 +88,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint); + } + + public MenuItem[]? ContextMenuItems => new MenuItem[] + { + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)), + }; + private partial class DragHandle : FillFlowContainer { public Bindable Break { get; } = new Bindable(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index eaa31aea1e..d0f3a831f2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -71,7 +71,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!shouldBeVisible(breakPeriod)) continue; - Add(new TimelineBreak(breakPeriod)); + Add(new TimelineBreak(breakPeriod) + { + OnDeleted = b => breaks.Remove(b), + }); } } From 3f08605277a5b9cf0ddf14e213f2df4b6c93c124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 15:02:09 +0200 Subject: [PATCH 58/91] Add test coverage --- .../Editing/TestSceneTimelineSelection.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 9e147f5ff1..c6d284fae6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -403,6 +404,28 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2)); } + [Test] + public void TestBreakRemoval() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 5000 }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + AddAssert("beatmap has one break", () => EditorBeatmap.Breaks, () => Has.Count.EqualTo(1)); + + AddStep("move mouse to break", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + + AddStep("move mouse to delete menu item", () => InputManager.MoveMouseTo(this.ChildrenOfType().First().ChildrenOfType().First())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("beatmap has no breaks", () => EditorBeatmap.Breaks, () => Is.Empty); + AddAssert("break piece went away", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } From 6453522b34b3e9dd53c78a41a200fc3677a84c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 16:21:56 +0200 Subject: [PATCH 59/91] Add failing test coverage for changing banks/samples not working on node samples --- .../TestSceneHitObjectSampleAdjustments.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index f02d2a1bb1..9988c1cb59 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -402,6 +402,88 @@ namespace osu.Game.Tests.Visual.Editing void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); } + [Test] + public void TestHotkeysAffectNodeSamples() + { + AddStep("add slider", () => + { + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 1000, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = new List> + { + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM), + }, + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT), + }, + } + }); + }); + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + + AddStep("add clap addition", () => InputManager.Key(Key.R)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP); + + AddStep("remove clap addition", () => InputManager.Key(Key.R)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + AddStep("set drum bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.LShift); + }); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + } + private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => { var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); From a7b066f3ee59b9e9f13344ce3af4c5e7cf511e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 16:22:15 +0200 Subject: [PATCH 60/91] Include node samples when changing additions and banks --- .../Components/EditorSelectionHandler.cs | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 7c30b73122..70c91b16fd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components break; } - AddSampleBank(bankName); + SetSampleBank(bankName); } break; @@ -177,14 +177,27 @@ namespace osu.Game.Screens.Edit.Compose.Components { SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo); + var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray(); + foreach ((string sampleName, var bindable) in SelectionSampleStates) { - bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName)); + bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Name == sampleName)); } foreach ((string bankName, var bindable) in SelectionBankStates) { - bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName)); + bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Bank == bankName)); + } + + IEnumerable> enumerateAllSamples(HitObject hitObject) + { + yield return hitObject.Samples; + + if (hitObject is IHasRepeats withRepeats) + { + foreach (var node in withRepeats.NodeSamples) + yield return node; + } } } @@ -193,12 +206,25 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Ternary state changes /// - /// Adds a sample bank to all selected s. + /// Sets the sample bank for all selected s. /// /// The name of the sample bank. - public void AddSampleBank(string bankName) + public void SetSampleBank(string bankName) { - if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) + bool hasRelevantBank(HitObject hitObject) + { + bool result = hitObject.Samples.All(s => s.Bank == bankName); + + if (hitObject is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + result &= node.All(s => s.Bank == bankName); + } + + return result; + } + + if (SelectedItems.All(hasRelevantBank)) return; EditorBeatmap.PerformOnSelection(h => @@ -207,17 +233,37 @@ namespace osu.Game.Screens.Edit.Compose.Components return; h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList(); + + if (h is IHasRepeats hasRepeats) + { + for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.With(newBank: bankName)).ToList(); + } + EditorBeatmap.Update(h); }); } + private bool hasRelevantSample(HitObject hitObject, string sampleName) + { + bool result = hitObject.Samples.Any(s => s.Name == sampleName); + + if (hitObject is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + result &= node.Any(s => s.Name == sampleName); + } + + return result; + } + /// /// Adds a hit sample to all selected s. /// /// The name of the hit sample. public void AddHitSample(string sampleName) { - if (SelectedItems.All(h => h.Samples.Any(s => s.Name == sampleName))) + if (SelectedItems.All(h => hasRelevantSample(h, sampleName))) return; EditorBeatmap.PerformOnSelection(h => @@ -228,6 +274,23 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(h.CreateHitSampleInfo(sampleName)); + if (h is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + { + if (node.Any(s => s.Name == sampleName)) + continue; + + var hitSample = h.CreateHitSampleInfo(sampleName); + + string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank; + if (existingAdditionBank != null) + hitSample = hitSample.With(newBank: existingAdditionBank); + + node.Add(hitSample); + } + } + EditorBeatmap.Update(h); }); } @@ -238,12 +301,19 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - if (SelectedItems.All(h => h.Samples.All(s => s.Name != sampleName))) + if (SelectedItems.All(h => !hasRelevantSample(h, sampleName))) return; EditorBeatmap.PerformOnSelection(h => { h.SamplesBindable.RemoveAll(s => s.Name == sampleName); + + if (h is IHasRepeats hasRepeats) + { + for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Where(s => s.Name != sampleName).ToList(); + } + EditorBeatmap.Update(h); }); } From 9034f6186bb332424609ff3097f7ca04777b6cfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 00:15:34 +0900 Subject: [PATCH 61/91] 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 349829555d..9fd0df3036 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8e8bac53b9..48d9c2564a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 53509453405f8256fd66b05dabf84c65c5f2b0b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 00:19:04 +0900 Subject: [PATCH 62/91] Update `HasFlag` usages --- CodeAnalysis/BannedSymbols.txt | 1 - osu.Game.Rulesets.Catch/CatchRuleset.cs | 29 +++++----- .../Edit/CatchHitObjectComposer.cs | 3 +- .../TestSceneNotes.cs | 3 +- .../Legacy/HitObjectPatternGenerator.cs | 33 ++++++------ .../Legacy/PathObjectPatternGenerator.cs | 17 +++--- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 53 +++++++++---------- .../Edit/OsuHitObjectComposer.cs | 9 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 37 +++++++------ .../UI/Cursor/CursorTrail.cs | 9 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 29 +++++----- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 5 +- osu.Game/Database/LegacyImportManager.cs | 9 ++-- osu.Game/Database/RealmAccess.cs | 3 +- osu.Game/Graphics/ParticleSpewer.cs | 5 +- .../Graphics/UserInterface/TwoLayerButton.cs | 11 ++-- osu.Game/Overlays/Music/PlaylistOverlay.cs | 3 +- osu.Game/Overlays/SettingsToolboxGroup.cs | 3 +- .../SkinEditor/SkinSelectionHandler.cs | 5 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 3 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 5 +- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 11 ++-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 19 ++++--- .../Edit/Compose/Components/SelectionBox.cs | 5 +- .../SelectionBoxDragHandleContainer.cs | 5 +- .../Components/SelectionBoxRotationHandle.cs | 5 +- .../Components/SelectionBoxScaleHandle.cs | 3 +- osu.Game/Screens/Play/HUDOverlay.cs | 9 ++-- .../HitEventTimingDistributionGraph.cs | 3 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +- .../SerialisableDrawableExtensions.cs | 5 +- osu.Game/Skinning/SerialisedDrawableInfo.cs | 5 +- osu.Game/Storyboards/StoryboardExtensions.cs | 9 ++-- 34 files changed, 163 insertions(+), 197 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 03fd21829d..3c60b28765 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable ins M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. -M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 72d1a161dd..ad6dedaa8f 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -62,43 +61,43 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new CatchModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new CatchModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new CatchModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new CatchModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new CatchModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new CatchModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new CatchModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new CatchModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new CatchModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new CatchModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new CatchModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new CatchModRelax(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 4172720ada..83f48816f9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit result.ScreenSpacePosition.X = screenSpacePosition.X; - if (snapType.HasFlagFast(SnapType.RelativeGrids)) + if (snapType.HasFlag(SnapType.RelativeGrids)) { if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 31ff57395c..990f545ee4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests } private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) - => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); + => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index ad45a3fb21..9880369dfb 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy else convertType |= PatternType.LowProbability; - if (!convertType.HasFlagFast(PatternType.KeepSingle)) + if (!convertType.HasFlag(PatternType.KeepSingle)) { if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; @@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order for (int i = RandomStart; i < TotalColumns; i++) @@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 + if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 // If we convert to 7K + 1, let's not overload the special key && (TotalColumns != 8 || lastColumn != 0) // Make sure the last column was not the centre column @@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns for (int i = RandomStart; i < TotalColumns; i++) @@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (PreviousPattern.HitObjects.Count() == 1) { - if (convertType.HasFlagFast(PatternType.Stair)) + if (convertType.HasFlag(PatternType.Stair)) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" int targetColumn = lastColumn + 1; @@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.ReverseStair)) + if (convertType.HasFlag(PatternType.ReverseStair)) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" int targetColumn = lastColumn - 1; @@ -164,10 +163,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } } - if (convertType.HasFlagFast(PatternType.KeepSingle)) + if (convertType.HasFlag(PatternType.KeepSingle)) return generateRandomNotes(1); - if (convertType.HasFlagFast(PatternType.Mirror)) + if (convertType.HasFlag(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); @@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); @@ -187,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); @@ -195,7 +194,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); @@ -208,9 +207,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy foreach (var obj in p.HitObjects) { - if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) + if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; - if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) + if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } @@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { var pattern = new Pattern(); - bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); + bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack); if (!allowStacking) noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); @@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int getNextColumn(int last) { - if (convertType.HasFlagFast(PatternType.Gathered)) + if (convertType.HasFlag(PatternType.Gathered)) { last++; if (last == TotalColumns) @@ -297,7 +296,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { - if (convertType.HasFlagFast(PatternType.ForceNotStack)) + if (convertType.HasFlag(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index 6d593a75e7..c54da74424 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); @@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0); @@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0); @@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; @@ -371,7 +370,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; - bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); + bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) @@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int endTime = startTime + SegmentDuration * SpanCount; int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) @@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 667002533d..0dcbb36c77 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new ManiaModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new ManiaModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new ManiaModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new ManiaModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new ManiaModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new ManiaModEasy(); - if (mods.HasFlagFast(LegacyMods.FadeIn)) + if (mods.HasFlag(LegacyMods.FadeIn)) yield return new ManiaModFadeIn(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new ManiaModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new ManiaModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new ManiaModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new ManiaModHidden(); - if (mods.HasFlagFast(LegacyMods.Key1)) + if (mods.HasFlag(LegacyMods.Key1)) yield return new ManiaModKey1(); - if (mods.HasFlagFast(LegacyMods.Key2)) + if (mods.HasFlag(LegacyMods.Key2)) yield return new ManiaModKey2(); - if (mods.HasFlagFast(LegacyMods.Key3)) + if (mods.HasFlag(LegacyMods.Key3)) yield return new ManiaModKey3(); - if (mods.HasFlagFast(LegacyMods.Key4)) + if (mods.HasFlag(LegacyMods.Key4)) yield return new ManiaModKey4(); - if (mods.HasFlagFast(LegacyMods.Key5)) + if (mods.HasFlag(LegacyMods.Key5)) yield return new ManiaModKey5(); - if (mods.HasFlagFast(LegacyMods.Key6)) + if (mods.HasFlag(LegacyMods.Key6)) yield return new ManiaModKey6(); - if (mods.HasFlagFast(LegacyMods.Key7)) + if (mods.HasFlag(LegacyMods.Key7)) yield return new ManiaModKey7(); - if (mods.HasFlagFast(LegacyMods.Key8)) + if (mods.HasFlag(LegacyMods.Key8)) yield return new ManiaModKey8(); - if (mods.HasFlagFast(LegacyMods.Key9)) + if (mods.HasFlag(LegacyMods.Key9)) yield return new ManiaModKey9(); - if (mods.HasFlagFast(LegacyMods.KeyCoop)) + if (mods.HasFlag(LegacyMods.KeyCoop)) yield return new ManiaModDualStages(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new ManiaModNoFail(); - if (mods.HasFlagFast(LegacyMods.Random)) + if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); - if (mods.HasFlagFast(LegacyMods.Mirror)) + if (mods.HasFlag(LegacyMods.Mirror)) yield return new ManiaModMirror(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f93874481d..784132ec4c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -10,7 +10,6 @@ using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -194,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.Edit public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) { // In the case of snapping to nearby objects, a time value is not provided. // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap @@ -204,7 +203,7 @@ namespace osu.Game.Rulesets.Osu.Edit // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. - if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) @@ -216,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); - if (snapType.HasFlagFast(SnapType.RelativeGrids)) + if (snapType.HasFlag(SnapType.RelativeGrids)) { if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { @@ -227,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Edit } } - if (snapType.HasFlagFast(SnapType.GlobalGrids)) + if (snapType.HasFlag(SnapType.GlobalGrids)) { if (rectangularGridSnapToggle.Value == TernaryState.True) { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 6752712be1..73f9be3fdc 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -70,55 +69,55 @@ namespace osu.Game.Rulesets.Osu public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new OsuModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new OsuModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new OsuModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new OsuModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Autopilot)) + if (mods.HasFlag(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new OsuModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new OsuModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new OsuModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new OsuModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new OsuModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new OsuModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new OsuModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new OsuModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new OsuModRelax(); - if (mods.HasFlagFast(LegacyMods.SpunOut)) + if (mods.HasFlag(LegacyMods.SpunOut)) yield return new OsuModSpunOut(); - if (mods.HasFlagFast(LegacyMods.Target)) + if (mods.HasFlag(LegacyMods.Target)) yield return new OsuModTargetPractice(); - if (mods.HasFlagFast(LegacyMods.TouchDevice)) + if (mods.HasFlag(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 95a052dadb..30a77db5a1 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; @@ -243,14 +242,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor originPosition = Vector2.Zero; - if (Source.TrailOrigin.HasFlagFast(Anchor.x1)) + if (Source.TrailOrigin.HasFlag(Anchor.x1)) originPosition.X = 0.5f; - else if (Source.TrailOrigin.HasFlagFast(Anchor.x2)) + else if (Source.TrailOrigin.HasFlag(Anchor.x2)) originPosition.X = 1f; - if (Source.TrailOrigin.HasFlagFast(Anchor.y1)) + if (Source.TrailOrigin.HasFlag(Anchor.y1)) originPosition.Y = 0.5f; - else if (Source.TrailOrigin.HasFlagFast(Anchor.y2)) + else if (Source.TrailOrigin.HasFlag(Anchor.y2)) originPosition.Y = 1f; Source.parts.CopyTo(parts, 0); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 13501c6192..2053a11426 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -79,43 +78,43 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new TaikoModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new TaikoModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new TaikoModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new TaikoModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new TaikoModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new TaikoModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new TaikoModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new TaikoModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new TaikoModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new TaikoModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new TaikoModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new TaikoModRelax(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index c2f4097889..011aca5d9c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -528,8 +527,8 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine); + kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 7e1641d16f..5c2f220045 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -164,13 +163,13 @@ namespace osu.Game.Database var importTasks = new List(); Task beatmapImportTask = Task.CompletedTask; - if (content.HasFlagFast(StableContent.Beatmaps)) + if (content.HasFlag(StableContent.Beatmaps)) importTasks.Add(beatmapImportTask = new LegacyBeatmapImporter(beatmaps).ImportFromStableAsync(stableStorage)); - if (content.HasFlagFast(StableContent.Skins)) + if (content.HasFlag(StableContent.Skins)) importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); - if (content.HasFlagFast(StableContent.Collections)) + if (content.HasFlag(StableContent.Collections)) { importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess) { @@ -180,7 +179,7 @@ namespace osu.Game.Database }.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); } - if (content.HasFlagFast(StableContent.Scores)) + if (content.HasFlag(StableContent.Scores)) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1ece81be50..ff76142bcc 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,7 +15,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; @@ -1035,7 +1034,7 @@ namespace osu.Game.Database var legacyMods = (LegacyMods)sr.ReadInt32(); - if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + if (!legacyMods.HasFlag(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) return; score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 64c70095bf..51fbd134d5 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -6,7 +6,6 @@ using System; using System.Diagnostics; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; @@ -190,9 +189,9 @@ namespace osu.Game.Graphics float width = Texture.DisplayWidth * scale; float height = Texture.DisplayHeight * scale; - if (relativePositionAxes.HasFlagFast(Axes.X)) + if (relativePositionAxes.HasFlag(Axes.X)) position.X *= sourceSize.X; - if (relativePositionAxes.HasFlagFast(Axes.Y)) + if (relativePositionAxes.HasFlag(Axes.Y)) position.Y *= sourceSize.Y; return new RectangleF( diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 5532e5c6a7..6f61a14b75 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using System; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -57,15 +56,15 @@ namespace osu.Game.Graphics.UserInterface set { base.Origin = value; - c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; - c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; + c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; + c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; - X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; + X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; Remove(c1, false); Remove(c2, false); - c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; - c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; + c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1; + c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0; Add(c1); Add(c2); } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 6ecd0f51d3..2d03a4a26d 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -131,7 +130,7 @@ namespace osu.Game.Overlays.Music filter.Search.HoldFocus = true; Schedule(() => filter.Search.TakeFocus()); - this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); + this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlag(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); this.FadeIn(transition_duration, Easing.OutQuint); } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index de13bd96d4..53849fa53c 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -161,7 +160,7 @@ namespace osu.Game.Overlays protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) headerTextVisibilityCache.Invalidate(); return base.OnInvalidate(invalidation, source); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index a671e7a76e..722ffd6d07 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -145,9 +144,9 @@ namespace osu.Game.Overlays.SkinEditor var blueprintItem = ((Drawable)blueprint.Item); blueprintItem.Scale = Vector2.One; - if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X)) + if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.X)) blueprintItem.Width = 1; - if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y)) + if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.Y)) blueprintItem.Height = 1; } }); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 08df8df7e2..4bfa7fba81 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -51,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor CanScaleDiagonally.Value = true; } - private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); + private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlag(axis) == false); private Dictionary? objectsInScale; private Vector2? defaultOrigin; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 1da2e1b744..221282ef13 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -4,7 +4,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -150,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize - Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, + Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Origin = TooltipAnchor, - Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5), + Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), Alpha = 0, Children = new Drawable[] { diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index f345504ca1..b48fc44963 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -3,7 +3,6 @@ using MessagePack; using Newtonsoft.Json; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Replays; using osuTK; @@ -32,23 +31,23 @@ namespace osu.Game.Replays.Legacy [JsonIgnore] [IgnoreMember] - public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1); + public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); [JsonIgnore] [IgnoreMember] - public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1); + public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); [JsonIgnore] [IgnoreMember] - public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2); + public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); [JsonIgnore] [IgnoreMember] - public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); + public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); [JsonIgnore] [IgnoreMember] - public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke); + public bool Smoke => ButtonState.HasFlag(ReplayButtonState.Smoke); [Key(3)] public ReplayButtonState ButtonState; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 540e0440c6..5bd720cfba 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -10,7 +10,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -505,7 +504,7 @@ namespace osu.Game.Rulesets.Edit var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); double? targetTime = null; - if (snapType.HasFlagFast(SnapType.GlobalGrids)) + if (snapType.HasFlag(SnapType.GlobalGrids)) { if (playfield is ScrollingPlayfield scrollingPlayfield) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 66b3033f90..37a87462ca 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -12,7 +12,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo); + bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); type &= ~LegacyHitObjectType.NewCombo; var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); @@ -66,14 +65,14 @@ namespace osu.Game.Rulesets.Objects.Legacy HitObject result = null; - if (type.HasFlagFast(LegacyHitObjectType.Circle)) + if (type.HasFlag(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlagFast(LegacyHitObjectType.Slider)) + else if (type.HasFlag(LegacyHitObjectType.Slider)) { double? length = null; @@ -145,7 +144,7 @@ namespace osu.Game.Rulesets.Objects.Legacy result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } - else if (type.HasFlagFast(LegacyHitObjectType.Spinner)) + else if (type.HasFlag(LegacyHitObjectType.Spinner)) { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); @@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlagFast(LegacyHitObjectType.Hold)) + else if (type.HasFlag(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts @@ -472,7 +471,7 @@ namespace osu.Game.Rulesets.Objects.Legacy soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))); + type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal))); } else { @@ -480,13 +479,13 @@ namespace osu.Game.Rulesets.Objects.Legacy soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume)); } - if (type.HasFlagFast(LegacyHitSoundType.Finish)) + if (type.HasFlag(LegacyHitSoundType.Finish)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlagFast(LegacyHitSoundType.Whistle)) + if (type.HasFlag(LegacyHitSoundType.Whistle)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlagFast(LegacyHitSoundType.Clap)) + if (type.HasFlag(LegacyHitSoundType.Clap)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index fec3224fad..0cc8a8273f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -298,13 +297,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public void PerformFlipFromScaleHandles(Axes axes) { - if (axes.HasFlagFast(Axes.X)) + if (axes.HasFlag(Axes.X)) { dragHandles.FlipScaleHandles(Direction.Horizontal); OnFlip?.Invoke(Direction.Horizontal, false); } - if (axes.HasFlagFast(Axes.Y)) + if (axes.HasFlag(Axes.Y)) { dragHandles.FlipScaleHandles(Direction.Vertical); OnFlip?.Invoke(Direction.Vertical, false); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs index e7f69b7b37..e5ac05ca6a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -74,9 +73,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { foreach (var handle in scaleHandles) { - if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1)) + if (direction == Direction.Horizontal && !handle.Anchor.HasFlag(Anchor.x1)) handle.Anchor ^= Anchor.x0 | Anchor.x2; - if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1)) + if (direction == Direction.Vertical && !handle.Anchor.HasFlag(Anchor.y1)) handle.Anchor ^= Anchor.y0 | Anchor.y2; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index b9383f1bad..c62e0e0d41 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; @@ -46,8 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components Icon = FontAwesome.Solid.Redo, Scale = new Vector2 { - X = Anchor.HasFlagFast(Anchor.x0) ? 1f : -1f, - Y = Anchor.HasFlagFast(Anchor.y0) ? 1f : -1f + X = Anchor.HasFlag(Anchor.x0) ? 1f : -1f, + Y = Anchor.HasFlag(Anchor.y0) ? 1f : -1f } }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 3f4f2c2854..7b0943c1d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -128,6 +127,6 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1); + private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlag(Anchor.x1) && !anchor.HasFlag(Anchor.y1); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0c0941573c..ef3bb7c04a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -295,7 +294,7 @@ namespace osu.Game.Screens.Play Drawable drawable = (Drawable)element; // for now align some top components with the bottom-edge of the lowest top-anchored hud element. - if (drawable.Anchor.HasFlagFast(Anchor.y0)) + if (drawable.Anchor.HasFlag(Anchor.y0)) { // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. if (element is LegacyHealthDisplay) @@ -305,20 +304,20 @@ namespace osu.Game.Screens.Play bool isRelativeX = drawable.RelativeSizeAxes == Axes.X; - if (drawable.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX) + if (drawable.Anchor.HasFlag(Anchor.TopRight) || isRelativeX) { if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value) lowestTopScreenSpaceRight = bottom; } - if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX) + if (drawable.Anchor.HasFlag(Anchor.TopLeft) || isRelativeX) { if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value) lowestTopScreenSpaceLeft = bottom; } } // and align bottom-right components with the top-edge of the highest bottom-anchored hud element. - else if (drawable.Anchor.HasFlagFast(Anchor.BottomRight) || (drawable.Anchor.HasFlagFast(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) + else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) { var topLeft = element.ScreenSpaceDrawQuad.TopLeft; if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 47807a8346..207e19a716 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -282,7 +281,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) { if (lastDrawHeight != null && lastDrawHeight != DrawHeight) Scheduler.AddOnce(updateMetrics, false); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4f2325adbf..56e7c24985 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -828,7 +827,7 @@ namespace osu.Game.Screens.Select protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) itemsCache.Invalidate(); return base.OnInvalidate(invalidation, source); diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 97c4cc8f73..a0488492ae 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; @@ -19,9 +18,9 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; - if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true) component.Width = width; - if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true) component.Height = height; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index ac1aa80d29..b4be5745d1 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -68,10 +67,10 @@ namespace osu.Game.Skinning Rotation = component.Rotation; Scale = component.Scale; - if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true) Width = component.Width; - if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true) Height = component.Height; Anchor = component.Anchor; diff --git a/osu.Game/Storyboards/StoryboardExtensions.cs b/osu.Game/Storyboards/StoryboardExtensions.cs index 04c7196315..110af73cca 100644 --- a/osu.Game/Storyboards/StoryboardExtensions.cs +++ b/osu.Game/Storyboards/StoryboardExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osuTK; @@ -22,18 +21,18 @@ namespace osu.Game.Storyboards // Either flip horizontally or negative X scale, but not both. if (flipH ^ (vectorScale.X < 0)) { - if (origin.HasFlagFast(Anchor.x0)) + if (origin.HasFlag(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlagFast(Anchor.x2)) + else if (origin.HasFlag(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } // Either flip vertically or negative Y scale, but not both. if (flipV ^ (vectorScale.Y < 0)) { - if (origin.HasFlagFast(Anchor.y0)) + if (origin.HasFlag(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlagFast(Anchor.y2)) + else if (origin.HasFlag(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } From 7cb3d7445cdd42c32c4a6dc995d8383878c60e16 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 17:20:00 -0300 Subject: [PATCH 63/91] Add verify checks for title markers --- .../Editing/Checks/CheckTitleMarkersTest.cs | 235 ++++++++++++++++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 + .../Rulesets/Edit/Checks/CheckTitleMarkers.cs | 76 ++++++ 3 files changed, 314 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs new file mode 100644 index 0000000000..54d3136700 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs @@ -0,0 +1,235 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckTitleMarkersTest + { + private CheckTitleMarkers check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckTitleMarkers(); + + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = "Egao no Kanata", + TitleUnicode = "エガオノカナタ" + } + } + }; + } + + [Test] + public void TestNoTitleMarkers() + { + var issues = check.Run(getContext(beatmap)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestTVSizeMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (TV Size)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedTVSizeMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (tv size)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestGameVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedGameVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (game ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestShortVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedShortVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (short ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestSpedUpVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedSpedUpVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestNightcoreMixMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedNightcoreMixMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestSpedUpCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedSpedUpCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestNightcoreCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedNightcoreCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} \ No newline at end of file diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index a9681e13ba..642b878a7b 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Edit // Events new CheckBreaks(), + + // Metadata + new CheckTitleMarkers(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs new file mode 100644 index 0000000000..6753abde4d --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Text.RegularExpressions; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckTitleMarkers : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateIncorrectMarker(this), + }; + + public IEnumerable MarkerChecks => new MarkerCheck[] + { + new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), + new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), + new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), + new MarkerCheck("(Cut Ver.)", @"(?i)(? Run(BeatmapVerifierContext context) + { + string romanisedTitle = context.Beatmap.Metadata.Title; + string unicodeTitle = context.Beatmap.Metadata.TitleUnicode; + + foreach (var check in MarkerChecks) + { + bool hasRomanisedTitle = unicodeTitle != romanisedTitle; + + if (check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + { + yield return new IssueTemplateIncorrectMarker(this).Create(hasRomanisedTitle ? "Romanised title" : "Title", check.CorrectMarkerFormat); + } + + if (hasRomanisedTitle && check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) + { + yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); + } + } + } + + public class MarkerCheck + { + public string CorrectMarkerFormat; + public Regex ExactRegex; + public Regex AnyRegex; + + public MarkerCheck(string exact, string anyRegex) + { + CorrectMarkerFormat = exact; + ExactRegex = new Regex(Regex.Escape(exact)); + AnyRegex = new Regex(anyRegex); + } + } + + public class IssueTemplateIncorrectMarker : IssueTemplate + { + public IssueTemplateIncorrectMarker(ICheck check) + : base(check, IssueType.Problem, "{0} field has a incorrect format of marker {1}") + { + } + + public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat); + } + } +} \ No newline at end of file From 7cdad20119481ba3ff48001a08ee3f1abb58fc71 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 20:55:52 -0300 Subject: [PATCH 64/91] Fix explicit array type specification in MarkerChecks --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 6753abde4d..6e5faf3dae 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - public IEnumerable MarkerChecks => new MarkerCheck[] - { + public IEnumerable MarkerChecks = [ new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), @@ -26,7 +25,7 @@ namespace osu.Game.Rulesets.Edit.Checks new MarkerCheck("(Nightcore Mix)", @"(?i)(? Run(BeatmapVerifierContext context) { From 7143ff523fb9155ca7ed10e3f02d3ed38bef138b Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 21:09:49 -0300 Subject: [PATCH 65/91] Make `MarkerChecks` and `MarkerCheck` class private --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 6e5faf3dae..2471d175ae 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - public IEnumerable MarkerChecks = [ + private IEnumerable markerChecks = [ new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Edit.Checks string romanisedTitle = context.Beatmap.Metadata.Title; string unicodeTitle = context.Beatmap.Metadata.TitleUnicode; - foreach (var check in MarkerChecks) + foreach (var check in markerChecks) { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Edit.Checks } } - public class MarkerCheck + private class MarkerCheck { public string CorrectMarkerFormat; public Regex ExactRegex; From 21829e7ef46bc3dcc91ca5677ed837bbdb024d8a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 21:12:15 -0300 Subject: [PATCH 66/91] Fix test method names --- osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs index 54d3136700..a8f86a6d45 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestTVSizeMarker() + public void TestTvSizeMarker() { beatmap.BeatmapInfo.Metadata.Title += " (TV Size)"; beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)"; @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestMalformedTVSizeMarker() + public void TestMalformedTvSizeMarker() { beatmap.BeatmapInfo.Metadata.Title += " (tv size)"; beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)"; From abfdf90b541a94888e147554b863f24d96c2d277 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 3 Jul 2024 07:11:35 +0300 Subject: [PATCH 67/91] Remove unused using directive --- osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index c6d284fae6..229cb995d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Humanizer; using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; From b6dc483fc11ea322ffee2a54b00983d8823bd3e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 14:23:31 +0900 Subject: [PATCH 68/91] Add missing change handler to ensure undo/redo works for break removal --- .../Components/Timeline/TimelineBreakDisplay.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index d0f3a831f2..b9a66266bb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private Timeline timeline { get; set; } = null!; + [Resolved] + private IEditorChangeHandler editorChangeHandler { get; set; } = null!; + /// /// The visible time/position range of the timeline. /// @@ -73,7 +76,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Add(new TimelineBreak(breakPeriod) { - OnDeleted = b => breaks.Remove(b), + OnDeleted = b => + { + editorChangeHandler.BeginChange(); + breaks.Remove(b); + editorChangeHandler.EndChange(); + }, }); } } From abfcac746692671e3750af5b0cfcdd113cf40d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 15:17:00 +0900 Subject: [PATCH 69/91] Fix nullability --- .../Edit/Compose/Components/Timeline/TimelineBreak.cs | 2 +- .../Compose/Components/Timeline/TimelineBreakDisplay.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 7f64436267..29030099c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint); } - public MenuItem[]? ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)), }; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index b9a66266bb..eca44672f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Timeline timeline { get; set; } = null!; [Resolved] - private IEditorChangeHandler editorChangeHandler { get; set; } = null!; + private IEditorChangeHandler? editorChangeHandler { get; set; } /// /// The visible time/position range of the timeline. @@ -78,9 +78,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { OnDeleted = b => { - editorChangeHandler.BeginChange(); + editorChangeHandler?.BeginChange(); breaks.Remove(b); - editorChangeHandler.EndChange(); + editorChangeHandler?.EndChange(); }, }); } From 29c3ff06779a865df43f7ad6e79f1d6f35c6d8a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 09:33:48 +0200 Subject: [PATCH 70/91] Enable NRT in `RulesetInputManager` --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a08c3bab08..0fc39e6fcb 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Allocation; @@ -32,12 +30,12 @@ namespace osu.Game.Rulesets.UI public readonly KeyBindingContainer KeyBindingContainer; - [Resolved(CanBeNull = true)] - private ScoreProcessor scoreProcessor { get; set; } + [Resolved] + private ScoreProcessor? scoreProcessor { get; set; } - private ReplayRecorder recorder; + private ReplayRecorder? recorder; - public ReplayRecorder Recorder + public ReplayRecorder? Recorder { set { @@ -103,9 +101,9 @@ namespace osu.Game.Rulesets.UI #region IHasReplayHandler - private ReplayInputHandler replayInputHandler; + private ReplayInputHandler? replayInputHandler; - public ReplayInputHandler ReplayInputHandler + public ReplayInputHandler? ReplayInputHandler { get => replayInputHandler; set @@ -124,8 +122,8 @@ namespace osu.Game.Rulesets.UI #region Setting application (disables etc.) - private Bindable mouseDisabled; - private Bindable tapsDisabled; + private Bindable mouseDisabled = null!; + private Bindable tapsDisabled = null!; protected override bool Handle(UIEvent e) { @@ -227,9 +225,9 @@ namespace osu.Game.Rulesets.UI public class RulesetInputManagerInputState : InputState where T : struct { - public ReplayState LastReplayState; + public ReplayState? LastReplayState; - public RulesetInputManagerInputState(InputState state = null) + public RulesetInputManagerInputState(InputState state) : base(state) { } From 7f1d113454fcb63de64ffe470df929099fdcbd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:18:22 +0200 Subject: [PATCH 71/91] Add failing test coverage for replay detach --- .../TestSceneCatchReplayHandling.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs new file mode 100644 index 0000000000..361ecb7c4c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene + { + [Test] + public void TestReplayDetach() + { + DrawableCatchRuleset drawableRuleset = null!; + float catcherPosition = 0; + + AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), [])); + AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score())); + AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType().Single().X); + AddStep("hold down left", () => InputManager.PressKey(Key.Left)); + AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.EqualTo(catcherPosition)); + + AddStep("detach replay", () => drawableRuleset.SetReplayScore(null)); + AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.Not.EqualTo(catcherPosition)); + AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); + } + } +} From 294aa09c413b4e5425ca92280842823ac1d2678d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:18:45 +0200 Subject: [PATCH 72/91] Clear pressed keys and last replay frame when detaching replay from ruleset input manager --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 0fc39e6fcb..0bd90a6635 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.States; using osu.Game.Configuration; @@ -108,7 +109,11 @@ namespace osu.Game.Rulesets.UI get => replayInputHandler; set { - if (replayInputHandler != null) RemoveHandler(replayInputHandler); + if (replayInputHandler != null) + { + RemoveHandler(replayInputHandler); + new ReplayStateReset().Apply(CurrentState, this); + } replayInputHandler = value; UseParentInput = replayInputHandler == null; @@ -220,6 +225,19 @@ namespace osu.Game.Rulesets.UI RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); } } + + private class ReplayStateReset : IInput + { + public void Apply(InputState state, IInputStateChangeHandler handler) + { + if (!(state is RulesetInputManagerInputState inputState)) + throw new InvalidOperationException($"{nameof(ReplayState)} should only be applied to a {nameof(RulesetInputManagerInputState)}"); + + inputState.LastReplayState = null; + + handler.HandleInputStateChange(new ReplayStateChangeEvent(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], [])); + } + } } public class RulesetInputManagerInputState : InputState From 84c7d34b770ae8553c89f7c13722932b22cf7279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:32:08 +0200 Subject: [PATCH 73/91] Fix user-pressed keys remaining pressed whtn autoplay is turned on --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 0bd90a6635..a242896ff8 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -109,11 +109,21 @@ namespace osu.Game.Rulesets.UI get => replayInputHandler; set { + if (replayInputHandler == value) + return; + if (replayInputHandler != null) { RemoveHandler(replayInputHandler); + // ensures that all replay keys are released, and that the last replay state is correctly cleared new ReplayStateReset().Apply(CurrentState, this); } + else + { + // ensures that all user-pressed keys are released, so that the replay handler may trigger them itself + // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/45cd7c7c702c081334fce41e7771b9dc6481b28d/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) + SyncInputState(CreateInitialState()); + } replayInputHandler = value; UseParentInput = replayInputHandler == null; From 56cdd83451ac4fbc4132b92d4cdca23b90b5ce26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 20:42:12 +0900 Subject: [PATCH 74/91] Adjust padding and round corners of hover layer --- .../Screens/Edit/Components/TimeInfoContainer.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 9365402c1c..7c03198ec0 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -88,9 +88,18 @@ namespace osu.Game.Screens.Edit.Components Padding = new MarginPadding { Top = 5, - Horizontal = -5 + Horizontal = -2 + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box { RelativeSizeAxes = Axes.Both, }, + } }, - Child = new Box { RelativeSizeAxes = Axes.Both, }, Alpha = 0, }, trackTimer = new OsuSpriteText From 0ab13e44869f09415d1e96dc1d8631080a94f5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 14:37:07 +0200 Subject: [PATCH 75/91] Use alternative method of releasing user-pressed keys when activating autoplay --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a242896ff8..31c7c34572 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -20,6 +20,7 @@ using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; +using osuTK; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI @@ -113,17 +114,12 @@ namespace osu.Game.Rulesets.UI return; if (replayInputHandler != null) - { RemoveHandler(replayInputHandler); - // ensures that all replay keys are released, and that the last replay state is correctly cleared - new ReplayStateReset().Apply(CurrentState, this); - } - else - { - // ensures that all user-pressed keys are released, so that the replay handler may trigger them itself - // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/45cd7c7c702c081334fce41e7771b9dc6481b28d/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) - SyncInputState(CreateInitialState()); - } + + // ensures that all replay keys are released, that the last replay state is correctly cleared, + // and that all user-pressed keys are released, so that the replay handler may trigger them itself + // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/17d65f476d51cc5f2aaea818534f8fbac47e5fe6/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) + new ReplayStateReset().Apply(CurrentState, this); replayInputHandler = value; UseParentInput = replayInputHandler == null; @@ -243,9 +239,16 @@ namespace osu.Game.Rulesets.UI if (!(state is RulesetInputManagerInputState inputState)) throw new InvalidOperationException($"{nameof(ReplayState)} should only be applied to a {nameof(RulesetInputManagerInputState)}"); - inputState.LastReplayState = null; + new MouseButtonInput([], state.Mouse.Buttons).Apply(state, handler); + new KeyboardKeyInput([], state.Keyboard.Keys).Apply(state, handler); + new TouchInput(Enum.GetValues().Select(s => new Touch(s, Vector2.Zero)), false).Apply(state, handler); + new JoystickButtonInput([], state.Joystick.Buttons).Apply(state, handler); + new MidiKeyInput(new MidiState(), state.Midi).Apply(state, handler); + new TabletPenButtonInput([], state.Tablet.PenButtons).Apply(state, handler); + new TabletAuxiliaryButtonInput([], state.Tablet.AuxiliaryButtons).Apply(state, handler); handler.HandleInputStateChange(new ReplayStateChangeEvent(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], [])); + inputState.LastReplayState = null; } } } From 901fec65efd57b347948e388c31ce71c67fe0662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:49:17 +0200 Subject: [PATCH 76/91] Address code quality issues --- .../Rulesets/Edit/Checks/CheckTitleMarkers.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 2471d175ae..d6fd771e9c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,15 +16,16 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - private IEnumerable markerChecks = [ - new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), - new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), - new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), - new MarkerCheck("(Cut Ver.)", @"(?i)(? markerChecks = + [ + new MarkerCheck(@"(TV Size)", @"(?i)(tv (size|ver))"), + new MarkerCheck(@"(Game Ver.)", @"(?i)(game (size|ver))"), + new MarkerCheck(@"(Short Ver.)", @"(?i)(short (size|ver))"), + new MarkerCheck(@"(Cut Ver.)", @"(?i)(? Run(BeatmapVerifierContext context) @@ -50,9 +51,9 @@ namespace osu.Game.Rulesets.Edit.Checks private class MarkerCheck { - public string CorrectMarkerFormat; - public Regex ExactRegex; - public Regex AnyRegex; + public readonly string CorrectMarkerFormat; + public readonly Regex ExactRegex; + public readonly Regex AnyRegex; public MarkerCheck(string exact, string anyRegex) { @@ -72,4 +73,4 @@ namespace osu.Game.Rulesets.Edit.Checks public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat); } } -} \ No newline at end of file +} From 32b3d3d7dfefdffcef5698acd47df4e964ba6a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:49:21 +0200 Subject: [PATCH 77/91] Compile regexes for speed --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index d6fd771e9c..fb0203fe19 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Edit.Checks public MarkerCheck(string exact, string anyRegex) { CorrectMarkerFormat = exact; - ExactRegex = new Regex(Regex.Escape(exact)); - AnyRegex = new Regex(anyRegex); + ExactRegex = new Regex(Regex.Escape(exact), RegexOptions.Compiled); + AnyRegex = new Regex(anyRegex, RegexOptions.Compiled); } } From 5696e85b68d3a343ea89a4b56d6d13d54fd11c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:50:36 +0200 Subject: [PATCH 78/91] Adjust conditionals The fact that checking the unicode title was gated behind a `hasRomanisedTitle` guard was breaking my brain. --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index fb0203fe19..b6339851ef 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -37,15 +37,11 @@ namespace osu.Game.Rulesets.Edit.Checks { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; - if (check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) - { - yield return new IssueTemplateIncorrectMarker(this).Create(hasRomanisedTitle ? "Romanised title" : "Title", check.CorrectMarkerFormat); - } - - if (hasRomanisedTitle && check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) - { + if (check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); - } + + if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat); } } From bcb479d4f70d7f268dc54b4f6265ad2b40496507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 16:13:30 +0200 Subject: [PATCH 79/91] Use one less regex --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index b6339851ef..00482a72fc 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Text.RegularExpressions; using osu.Game.Rulesets.Edit.Checks.Components; @@ -37,10 +38,10 @@ namespace osu.Game.Rulesets.Edit.Checks { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; - if (check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) + if (check.AnyRegex.IsMatch(unicodeTitle) && !unicodeTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal)) yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); - if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !romanisedTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal)) yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat); } } @@ -48,13 +49,11 @@ namespace osu.Game.Rulesets.Edit.Checks private class MarkerCheck { public readonly string CorrectMarkerFormat; - public readonly Regex ExactRegex; public readonly Regex AnyRegex; public MarkerCheck(string exact, string anyRegex) { CorrectMarkerFormat = exact; - ExactRegex = new Regex(Regex.Escape(exact), RegexOptions.Compiled); AnyRegex = new Regex(anyRegex, RegexOptions.Compiled); } } From 00a0058fc709320e3dc1bea4053ee3fb30c94397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 16:13:47 +0200 Subject: [PATCH 80/91] Fix typo --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 00482a72fc..9c702ad58a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateIncorrectMarker : IssueTemplate { public IssueTemplateIncorrectMarker(ICheck check) - : base(check, IssueType.Problem, "{0} field has a incorrect format of marker {1}") + : base(check, IssueType.Problem, "{0} field has an incorrect format of marker {1}") { } From 42aff953d991246006700feea63accfad795651f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 23:59:29 +0900 Subject: [PATCH 81/91] Ensure menu items update when curve type changes --- .../Components/PathControlPointVisualiser.cs | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ddf6cd0f57..abe8be530a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu where T : OsuHitObject, IHasPath { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield. internal readonly Container> Pieces; @@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (allowSelection) d.RequestSelection = selectionRequested; + d.ControlPoint.Changed += controlPointChanged; d.DragStarted = DragStarted; d.DragInProgress = DragInProgress; d.DragEnded = DragEnded; @@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components foreach (var point in e.OldItems.Cast()) { + point.Changed -= controlPointChanged; + foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) piece.RemoveAndDisposeImmediately(); } @@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + private void controlPointChanged() => updateCurveMenuItems(); + protected override bool OnClick(ClickEvent e) { if (Pieces.Any(piece => piece.IsHovered)) @@ -318,6 +323,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + foreach (var p in Pieces) + p.ControlPoint.Changed -= controlPointChanged; + } + private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) @@ -328,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// /// Attempts to set all selected control point pieces to the given path type. - /// If that would fail, try to change the path such that it instead succeeds + /// If that fails, try to change the path such that it instead succeeds /// in a UX-friendly way. /// /// The path type we want to assign to the given control point piece. @@ -371,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private int draggedControlPointIndex; private HashSet selectedControlPoints; + private List curveTypeItems; + public void DragStarted(PathControlPoint controlPoint) { dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); @@ -467,7 +481,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components var splittablePieces = selectedPieces.Where(isSplittable).ToList(); int splittableCount = splittablePieces.Count; - List curveTypeItems = new List(); + curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) { @@ -505,25 +519,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components () => DeleteSelected()) ); + updateCurveMenuItems(); + return menuItems.ToArray(); + + CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)); } } - private MenuItem createMenuItemForPathType(PathType? type) + private void updateCurveMenuItems() { - int totalCount = Pieces.Count(p => p.IsSelected.Value); - int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); + foreach (var item in curveTypeItems.OfType()) + { + int totalCount = Pieces.Count(p => p.IsSelected.Value); + int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType); - var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => updatePathTypeOfSelectedPieces(type)); + if (countOfState == totalCount) + item.State.Value = TernaryState.True; + else if (countOfState > 0) + item.State.Value = TernaryState.Indeterminate; + else + item.State.Value = TernaryState.False; + } + } - if (countOfState == totalCount) - item.State.Value = TernaryState.True; - else if (countOfState > 0) - item.State.Value = TernaryState.Indeterminate; - else - item.State.Value = TernaryState.False; + private class CurveTypeMenuItem : TernaryStateRadioMenuItem + { + public readonly PathType? PathType; - return item; + public CurveTypeMenuItem(PathType? pathType, Action action) + : base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action) + { + PathType = pathType; + } } } } From 6abb728cd5dc5a97bc1ced545494b3aa5d2118b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 00:22:46 +0900 Subject: [PATCH 82/91] Change menu items to be in same order as hotkeys --- .../Components/PathControlPointVisualiser.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index abe8be530a..dfe334be0c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -483,20 +483,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems = new List(); - if (!selectedPieces.Contains(Pieces[0])) + // todo: hide/disable items which aren't valid for selected points + foreach (PathType? type in path_types) { - curveTypeItems.Add(createMenuItemForPathType(null)); - curveTypeItems.Add(new OsuMenuItemSpacer()); + // special inherit case + if (type == null) + { + if (selectedPieces.Contains(Pieces[0])) + continue; + + curveTypeItems.Add(new OsuMenuItemSpacer()); + } + + curveTypeItems.Add(createMenuItemForPathType(type)); } - // todo: hide/disable items which aren't valid for selected points - curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR)); - curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE)); - curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); - curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4))); - if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) + { + curveTypeItems.Add(new OsuMenuItemSpacer()); curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); + } var menuItems = new List { From f7339e3e8b9726078506baaf3f02969d34137fb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 00:26:00 +0900 Subject: [PATCH 83/91] Remove outdated(?) todo --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index dfe334be0c..f45dae8937 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -483,7 +483,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems = new List(); - // todo: hide/disable items which aren't valid for selected points foreach (PathType? type in path_types) { // special inherit case From e151454c4ec655d349002ceab292bac1f682fbc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 01:00:51 +0900 Subject: [PATCH 84/91] Add missing check for curve menu items not yet being created --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f45dae8937..6251d17d85 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -534,6 +534,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void updateCurveMenuItems() { + if (curveTypeItems == null) + return; + foreach (var item in curveTypeItems.OfType()) { int totalCount = Pieces.Count(p => p.IsSelected.Value); From e754668daa3dc8d57d7d4f62aa61735df3e4798e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 01:09:06 +0200 Subject: [PATCH 85/91] Always inherit the volume from the previous hit object --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 5cb9adfd72..84fe1584dd 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -163,6 +163,19 @@ namespace osu.Game.Rulesets.Edit if (lastHitNormal != null) HitObject.Samples[0] = lastHitNormal; } + else + { + // Only inherit the volume from the previous hit object + var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + + if (lastHitNormal != null) + { + for (int i = 0; i < HitObject.Samples.Count; i++) + { + HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); + } + } + } } /// From 371ca4cc4bb659c8d2765d9a8b38f431e1d1d049 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 05:43:43 +0300 Subject: [PATCH 86/91] Remove unnecessary null-conditional operators --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 32e4616a25..453b75ac84 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -192,12 +192,12 @@ namespace osu.Game.Rulesets.Mania.UI if (press) { - inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); + inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); } else { - inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); + inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); highlightOverlay.FadeTo(0, 400, Easing.OutQuint); } } From e4f90719ed15e7a17bb9be0360730eed9aa2f9a5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 06:22:53 +0300 Subject: [PATCH 87/91] Update test to match new behaviour --- osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs index 361ecb7c4c..1721703e48 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs @@ -25,8 +25,10 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType().Single().X); AddStep("hold down left", () => InputManager.PressKey(Key.Left)); AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.EqualTo(catcherPosition)); + AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); AddStep("detach replay", () => drawableRuleset.SetReplayScore(null)); + AddStep("hold down left", () => InputManager.PressKey(Key.Left)); AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.Not.EqualTo(catcherPosition)); AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); } From 7a0a5620e10ec5f046029311bdf7c561b0529204 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 14:06:18 +0300 Subject: [PATCH 88/91] Add failing test case --- .../Editing/TestSceneDifficultySwitching.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 76ed5063b0..457d4cee34 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -12,7 +12,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Storyboards; @@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("stack empty", () => Stack.CurrentScreen == null); } + [Test] + public void TestSwitchToDifficultyOfAnotherRuleset() + { + BeatmapInfo targetDifficulty = null; + + AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset); + + AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)); + switchToDifficulty(() => targetDifficulty); + confirmEditingBeatmap(() => targetDifficulty); + + AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset); + + AddStep("exit editor forcefully", () => Stack.Exit()); + // ensure editor loader didn't resume. + AddAssert("stack empty", () => Stack.CurrentScreen == null); + } + private void switchToDifficulty(Func difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke())); private void confirmEditingBeatmap(Func targetDifficulty) From 207ee8a2eea0d981efc07715559d9f8c212240b8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 14:06:36 +0300 Subject: [PATCH 89/91] Fix editor not updating ruleset when switching difficulty --- osu.Game/Screens/Edit/EditorLoader.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 8bcfa7b9f0..0e0fb9f795 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit scheduledDifficultySwitch = Schedule(() => { - Beatmap.Value = nextBeatmap.Invoke(); + var workingBeatmap = nextBeatmap.Invoke(); + + Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset; + Beatmap.Value = workingBeatmap; + state = editorState; // This screen is a weird exception to the rule that nothing after song select changes the global beatmap. From ea4e6cf1d7aac04a664e135a9b43251498ad6167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 14:39:11 +0200 Subject: [PATCH 90/91] Add test coverage --- .../Editing/TestScenePlacementBlueprint.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index a5681bea4a..c16533126b 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -5,11 +5,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; @@ -102,5 +104,56 @@ namespace osu.Game.Tests.Visual.Editing AddStep("change tool to circle", () => InputManager.Key(Key.Number2)); AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); } + + [Test] + public void TestAutomaticBankAssignment() + { + AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle + { + StartTime = 0, + Samples = + { + new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70), + new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, volume: 70), + } + })); + AddStep("seek to 500", () => EditorClock.Seek(500)); + AddStep("enable automatic bank assignment", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Q); + InputManager.ReleaseKey(Key.LShift); + }); + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + } + + [Test] + public void TestVolumeIsInheritedFromLastObject() + { + AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle + { + StartTime = 0, + Samples = + { + new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70), + } + })); + AddStep("seek to 500", () => EditorClock.Seek(500)); + AddStep("select drum bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.LShift); + }); + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + } } } From 72492a79cdedf0ca75da171125f90dce52dbad31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 14:40:40 +0200 Subject: [PATCH 91/91] Reduce duplication in new logic --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 84fe1584dd..63e38bf5de 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -156,24 +156,20 @@ namespace osu.Game.Rulesets.Edit comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); } - if (AutomaticBankAssignment) - { - // Take the hitnormal sample of the last hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) - HitObject.Samples[0] = lastHitNormal; - } - else - { - // Only inherit the volume from the previous hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) + if (lastHitNormal != null) + { + if (AutomaticBankAssignment) { + // Take the hitnormal sample of the last hit object + HitObject.Samples[0] = lastHitNormal; + } + else + { + // Only inherit the volume from the previous hit object for (int i = 0; i < HitObject.Samples.Count; i++) - { HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); - } } } }