From cda9440a296304bd710c1787436ea1a948f6c999 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Dec 2023 14:39:50 +0900 Subject: [PATCH 001/130] 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 002/130] 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 b74c3b1c5cbdd1d4bfc2b108bc9bd0a9b7b68e7f Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:19:38 +0800 Subject: [PATCH 003/130] Make all hit objects before the start time marked as hit. --- osu.Game/Screens/Edit/Editor.cs | 2 +- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 37 ++++++++++++++++++- .../Edit/GameplayTest/EditorPlayerLoader.cs | 5 ++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c1f6c02301..224823de70 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -452,7 +452,7 @@ namespace osu.Game.Screens.Edit pushEditorPlayer(); } - void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); + void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this, playableBeatmap)); } /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7dff05667d..47abcff476 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.Spectator; using osu.Game.Overlays; +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.Users; @@ -15,17 +21,19 @@ namespace osu.Game.Screens.Edit.GameplayTest { private readonly Editor editor; private readonly EditorState editorState; + private readonly IBeatmap playableBeatmap; protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] private MusicController musicController { get; set; } = null!; - public EditorPlayer(Editor editor) + public EditorPlayer(Editor editor, IBeatmap playableBeatmap) : base(new PlayerConfiguration { ShowResults = false }) { this.editor = editor; editorState = editor.GetState(); + this.playableBeatmap = playableBeatmap; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) @@ -43,6 +51,22 @@ namespace osu.Game.Screens.Edit.GameplayTest protected override void LoadComplete() { base.LoadComplete(); + + var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; + + foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) + { + var judgement = hitObject.CreateJudgement(); + + if (!frame.Header.Statistics.ContainsKey(judgement.MaxResult)) + frame.Header.Statistics.Add(judgement.MaxResult, 0); + + frame.Header.Statistics[judgement.MaxResult]++; + } + + HealthProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) @@ -54,6 +78,17 @@ namespace osu.Game.Screens.Edit.GameplayTest }, RESULTS_DISPLAY_DELAY); } }); + + static IEnumerable enumerateHitObjects(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + foreach (var nested in hitObject.NestedHitObjects) + yield return nested; + + yield return hitObject; + } + } } protected override void PrepareReplay() diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index bb151e4a45..c62b8cafb8 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -14,8 +15,8 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private OsuLogo osuLogo { get; set; } = null!; - public EditorPlayerLoader(Editor editor) - : base(() => new EditorPlayer(editor)) + public EditorPlayerLoader(Editor editor, IBeatmap playableBeatmap) + : base(() => new EditorPlayer(editor, playableBeatmap)) { } From 72e302dfac77c3b8a980e7f8026ace08eb95d191 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:27:41 +0800 Subject: [PATCH 004/130] Enumerate nested hit objects --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 47abcff476..e2b2b067e0 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { foreach (var hitObject in hitObjects) { - foreach (var nested in hitObject.NestedHitObjects) + foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects)) yield return nested; yield return hitObject; From aa83ac1896f0c54e15d967157f8ddd113a520342 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:53:54 +0800 Subject: [PATCH 005/130] add test case --- .../Editing/TestSceneEditorTestGameplay.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index bbd7123f20..ccc17dc3f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -126,6 +126,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); } + [Test] + public void TestGameplayTestAtEndOfBeatmap() + { + AddStep("seek to last 2 seconds", () => EditorClock.Seek(importedBeatmapSet.MaxLength - 2000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer); + + AddWaitStep("wait some", 5); + AddAssert("current screen is editor", () => Stack.CurrentScreen is Editor); + } + [Test] public void TestCancelGameplayTestWithUnsavedChanges() { From 6cd255f549f6a7499bc2b87f90ce9fef2180e5ac Mon Sep 17 00:00:00 2001 From: Nitrous Date: Thu, 11 Jan 2024 11:36:58 +0800 Subject: [PATCH 006/130] `Contains` + `Add` to `TryAdd` --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index e2b2b067e0..e1519f9a09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -57,10 +57,7 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) { var judgement = hitObject.CreateJudgement(); - - if (!frame.Header.Statistics.ContainsKey(judgement.MaxResult)) - frame.Header.Statistics.Add(judgement.MaxResult, 0); - + frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); frame.Header.Statistics[judgement.MaxResult]++; } From f807a3fd971527ba62938b674946989c44a4966c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 007/130] 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 008/130] 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 d2c86b0813d41ddd01ce4cbc00906f94271bfbf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 21:32:52 +0900 Subject: [PATCH 009/130] Avoid passing beatmap in from editor when it's already present --- osu.Game/Screens/Edit/Editor.cs | 2 +- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 32 +++++++++++-------- .../Edit/GameplayTest/EditorPlayerLoader.cs | 5 ++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2954d7dcaa..07c32983f5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -452,7 +452,7 @@ namespace osu.Game.Screens.Edit pushEditorPlayer(); } - void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this, playableBeatmap)); + void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); } /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 42eb57c253..c327ae185d 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -21,19 +21,17 @@ namespace osu.Game.Screens.Edit.GameplayTest { private readonly Editor editor; private readonly EditorState editorState; - private readonly IBeatmap playableBeatmap; protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo); [Resolved] private MusicController musicController { get; set; } = null!; - public EditorPlayer(Editor editor, IBeatmap playableBeatmap) + public EditorPlayer(Editor editor) : base(new PlayerConfiguration { ShowResults = false }) { this.editor = editor; editorState = editor.GetState(); - this.playableBeatmap = playableBeatmap; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) @@ -52,17 +50,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadComplete(); - var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - - foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) - { - var judgement = hitObject.CreateJudgement(); - frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); - frame.Header.Statistics[judgement.MaxResult]++; - } - - HealthProcessor.ResetFromReplayFrame(frame); - ScoreProcessor.ResetFromReplayFrame(frame); + markPreviousObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -75,6 +63,22 @@ namespace osu.Game.Screens.Edit.GameplayTest }, RESULTS_DISPLAY_DELAY); } }); + } + + private void markPreviousObjectsHit() + { + var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; + + foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects.Where(h => h.StartTime < editorState.Time))) + { + var judgement = hitObject.CreateJudgement(); + + frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); + frame.Header.Statistics[judgement.MaxResult]++; + } + + HealthProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.ResetFromReplayFrame(frame); static IEnumerable enumerateHitObjects(IEnumerable hitObjects) { diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index c62b8cafb8..bb151e4a45 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -15,8 +14,8 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private OsuLogo osuLogo { get; set; } = null!; - public EditorPlayerLoader(Editor editor, IBeatmap playableBeatmap) - : base(() => new EditorPlayer(editor, playableBeatmap)) + public EditorPlayerLoader(Editor editor) + : base(() => new EditorPlayer(editor)) { } From 3b5b7b2f880152dcd5d4bc2e99fc61e79830a6f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 22:57:17 +0900 Subject: [PATCH 010/130] Fix the majority of cases where gameplay stil doesn't end due to judgement count mismatch --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index c327ae185d..836f718f81 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -69,9 +69,9 @@ namespace osu.Game.Screens.Edit.GameplayTest { var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects.Where(h => h.StartTime < editorState.Time))) + foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { - var judgement = hitObject.CreateJudgement(); + var judgement = hitObject.Judgement; frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); frame.Header.Statistics[judgement.MaxResult]++; @@ -80,11 +80,11 @@ namespace osu.Game.Screens.Edit.GameplayTest HealthProcessor.ResetFromReplayFrame(frame); ScoreProcessor.ResetFromReplayFrame(frame); - static IEnumerable enumerateHitObjects(IEnumerable hitObjects) + static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { - foreach (var hitObject in hitObjects) + foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) { - foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects)) + foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects, cutoffTime)) yield return nested; yield return hitObject; From 126837fadd02156990f479d7b02f161cb1e053cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 23:28:37 +0900 Subject: [PATCH 011/130] Apply results rather than fake a replay frame --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 836f718f81..7d637fcb09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -6,12 +6,9 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Online.Spectator; using osu.Game.Overlays; +using osu.Game.Rulesets.Judgements; 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.Users; @@ -67,19 +64,14 @@ namespace osu.Game.Screens.Edit.GameplayTest private void markPreviousObjectsHit() { - var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; - frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); - frame.Header.Statistics[judgement.MaxResult]++; + HealthProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); + ScoreProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); } - HealthProcessor.ResetFromReplayFrame(frame); - ScoreProcessor.ResetFromReplayFrame(frame); - static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) From fdb47f8dfaa7c7f90624b853839111052b4beb29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 23:30:47 +0900 Subject: [PATCH 012/130] Fix incorrect handling of nested objects when inside parent object's duration --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7d637fcb09..2028094964 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -74,12 +74,16 @@ namespace osu.Game.Screens.Edit.GameplayTest static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { - foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) + foreach (var hitObject in hitObjects) { foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects, cutoffTime)) - yield return nested; + { + if (nested.GetEndTime() < cutoffTime) + yield return nested; + } - yield return hitObject; + if (hitObject.GetEndTime() < cutoffTime) + yield return hitObject; } } } From fe738a09517d7638781efffbd1efde61e05cb861 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 5 Jun 2024 18:12:02 +0200 Subject: [PATCH 013/130] 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 014/130] 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 b8e07045545fbfe3b46818b01fed515f12977fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:14:46 +0200 Subject: [PATCH 015/130] Implement singular change events for `ControlPoint` --- .../Beatmaps/ControlPoints/ControlPoint.cs | 22 ++++++++++++++++++- .../ControlPoints/DifficultyControlPoint.cs | 5 +++++ .../ControlPoints/EffectControlPoint.cs | 6 +++++ .../ControlPoints/SampleControlPoint.cs | 6 +++++ .../ControlPoints/TimingControlPoint.cs | 7 ++++++ 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index f46e4af332..f08a3d3f87 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -11,8 +11,28 @@ namespace osu.Game.Beatmaps.ControlPoints { public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint { + /// + /// Invoked when any of this 's properties have changed. + /// + public event Action? Changed; + + protected void RaiseChanged() => Changed?.Invoke(this); + + private double time; + [JsonIgnore] - public double Time { get; set; } + public double Time + { + get => time; + set + { + if (time == value) + return; + + time = value; + RaiseChanged(); + } + } public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time; diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 05230c85f4..9f8ed1b396 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -44,6 +44,11 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } + public DifficultyControlPoint() + { + SliderVelocityBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty && GenerateTicks == existingDifficulty.GenerateTicks diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 0138ac7569..d48ed957ee 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -50,6 +50,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } + public EffectControlPoint() + { + KiaiModeBindable.BindValueChanged(_ => RaiseChanged()); + ScrollSpeedBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index ae4bdafd6f..800d9f9abc 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -56,6 +56,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => SampleVolumeBindable.Value = value; } + public SampleControlPoint() + { + SampleBankBindable.BindValueChanged(_ => RaiseChanged()); + SampleVolumeBindable.BindValueChanged(_ => RaiseChanged()); + } + /// /// Create a SampleInfo based on the sample settings in this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 4e69486e2d..9ac361cffe 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -82,6 +82,13 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double BPM => 60000 / BeatLength; + public TimingControlPoint() + { + TimeSignatureBindable.BindValueChanged(_ => RaiseChanged()); + OmitFirstBarLineBindable.BindValueChanged(_ => RaiseChanged()); + BeatLengthBindable.BindValueChanged(_ => RaiseChanged()); + } + // Timing points are never redundant as they can change the time signature. public override bool IsRedundant(ControlPoint? existing) => false; From 694cfd0e259d2e5357a5692b4e05c8e680ddb49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:15:14 +0200 Subject: [PATCH 016/130] Expose singular coarse-grained change event on `ControlPointInfo` --- .../Beatmaps/ControlPoints/ControlPointGroup.cs | 5 +++++ .../Beatmaps/ControlPoints/ControlPointInfo.cs | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs index 1f34f3777d..c9c87dc85d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs @@ -10,8 +10,11 @@ namespace osu.Game.Beatmaps.ControlPoints public class ControlPointGroup : IComparable, IEquatable { public event Action? ItemAdded; + public event Action? ItemChanged; public event Action? ItemRemoved; + private void raiseItemChanged(ControlPoint controlPoint) => ItemChanged?.Invoke(controlPoint); + /// /// The time at which the control point takes effect. /// @@ -39,12 +42,14 @@ namespace osu.Game.Beatmaps.ControlPoints controlPoints.Add(point); ItemAdded?.Invoke(point); + point.Changed += raiseItemChanged; } public void Remove(ControlPoint point) { controlPoints.Remove(point); ItemRemoved?.Invoke(point); + point.Changed -= raiseItemChanged; } public sealed override bool Equals(object? obj) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 1a15db98e4..cb7515b66c 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -19,6 +19,14 @@ namespace osu.Game.Beatmaps.ControlPoints [Serializable] public class ControlPointInfo : IDeepCloneable { + /// + /// Invoked on any change to the set of control points. + /// + [CanBeNull] + public event Action ControlPointsChanged; + + private void raiseControlPointsChanged([CanBeNull] ControlPoint _ = null) => ControlPointsChanged?.Invoke(); + /// /// All control points grouped by time. /// @@ -116,6 +124,7 @@ namespace osu.Game.Beatmaps.ControlPoints if (addIfNotExisting) { newGroup.ItemAdded += GroupItemAdded; + newGroup.ItemChanged += raiseControlPointsChanged; newGroup.ItemRemoved += GroupItemRemoved; groups.Insert(~i, newGroup); @@ -131,6 +140,7 @@ namespace osu.Game.Beatmaps.ControlPoints group.Remove(item); group.ItemAdded -= GroupItemAdded; + group.ItemChanged -= raiseControlPointsChanged; group.ItemRemoved -= GroupItemRemoved; groups.Remove(group); @@ -287,6 +297,8 @@ namespace osu.Game.Beatmaps.ControlPoints default: throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}"); } + + raiseControlPointsChanged(); } protected virtual void GroupItemRemoved(ControlPoint controlPoint) @@ -301,6 +313,8 @@ namespace osu.Game.Beatmaps.ControlPoints effectPoints.Remove(typed); break; } + + raiseControlPointsChanged(); } public ControlPointInfo DeepClone() From 922837dd3a6c71bd04163f48270ecb25682b6551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:33:25 +0200 Subject: [PATCH 017/130] Reload scrolling composer on control point changes --- .../Rulesets/Edit/ScrollingHitObjectComposer.cs | 17 +++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index eb73cef01a..223b770b48 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,6 +23,9 @@ namespace osu.Game.Rulesets.Edit public abstract partial class ScrollingHitObjectComposer : HitObjectComposer where TObject : HitObject { + [Resolved] + private Editor? editor { get; set; } + private readonly Bindable showSpeedChanges = new Bindable(); private Bindable configShowSpeedChanges = null!; @@ -72,6 +77,8 @@ namespace osu.Game.Rulesets.Edit if (beatSnapGrid != null) AddInternal(beatSnapGrid); + + EditorBeatmap.ControlPointInfo.ControlPointsChanged += expireComposeScreenOnControlPointChange; } protected override void UpdateAfterChildren() @@ -104,5 +111,15 @@ namespace osu.Game.Rulesets.Edit beatSnapGrid.SelectionTimeRange = null; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (EditorBeatmap.IsNotNull()) + EditorBeatmap.ControlPointInfo.ControlPointsChanged -= expireComposeScreenOnControlPointChange; + } + + private void expireComposeScreenOnControlPointChange() => editor?.ReloadComposeScreen(); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3e3e772810..9703785856 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -996,6 +996,15 @@ namespace osu.Game.Screens.Edit } } + /// + /// Forces a reload of the compose screen after significant configuration changes. + /// + /// + /// This can be necessary for scrolling rulesets, as they do not easily support control points changing under them. + /// The reason that this works is that will re-instantiate the screen whenever it is requested next. + /// + public void ReloadComposeScreen() => screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately(); + [CanBeNull] private ScheduledDelegate playbackDisabledDebounce; From 8b6385f7d0e9ac575b1895b8118aeeeac13d6026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 09:08:25 +0200 Subject: [PATCH 018/130] Add failing test case demonstrating crash --- .../Editing/TestSceneTimelineSelection.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index d8219ff36e..9e147f5ff1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Tests.Beatmaps; @@ -357,6 +358,51 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count); } + [Test] + public void TestDragSelectionDuringPlacement() + { + var addedObjects = new[] + { + new Slider + { + StartTime = 300, + Path = new SliderPath([ + new PathControlPoint(), + new PathControlPoint(new Vector2(200)), + ]) + }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("seek to 700", () => EditorClock.Seek(700)); + AddStep("select spinner placement tool", () => + { + InputManager.Key(Key.Number4); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + }); + AddStep("begin spinner placement", () => InputManager.Click(MouseButton.Left)); + AddStep("seek to 1500", () => EditorClock.Seek(1500)); + + AddStep("start dragging", () => + { + var blueprintQuad = blueprintContainer.SelectionBlueprints[1].ScreenSpaceDrawQuad; + var dragStartPos = (blueprintQuad.TopLeft + blueprintQuad.BottomLeft) / 2 - new Vector2(30, 0); + InputManager.MoveMouseTo(dragStartPos); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("select entire object", () => + { + var blueprintQuad = blueprintContainer.SelectionBlueprints[1].ScreenSpaceDrawQuad; + var dragStartPos = (blueprintQuad.TopRight + blueprintQuad.BottomRight) / 2 + new Vector2(30, 0); + InputManager.MoveMouseTo(dragStartPos); + }); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddUntilStep("hitobject selected", () => EditorBeatmap.SelectedHitObjects, () => NUnit.Framework.Contains.Item(addedObjects[0])); + AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2)); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } From bdeea37a447c305c7c538e2965469c5c059ca1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 09:14:07 +0200 Subject: [PATCH 019/130] Commit active placement when starting drag selection via timeline This was reported in https://github.com/ppy/osu/pull/28474, albeit the code changes proposed there did not fix the issue at all. See 8b6385f7d0e9ac575b1895b8118aeeeac13d6026 for demonstration of the crash scenario. Basically what is happening there is: - The starting premise is that there is a spinner placement active. - At this time, a drag selection is started via the timeline. - Once the drag selection finds at least one suitable object to select, it mutates `SelectedItems`. - When selection changes for any reason, the `HitObjectComposer` decides to switch to the "select" tool, regardless of why the selection changed. - Changing the active tool causes the current placement - if any - to be committed, which mutates the beatmap. - Back at the drag box selection code, this causes a "collection modified when enumerating" exception. The proposed fix here is to eagerly commit active placement - if any - when drag selection is initiated via the timeline, which avoids this issue. This also appears to vaguely match stable behaviour and is sort of consistent with the logic of committing any outstanding changes upon switching to the selection tool. --- .../Editor/TestSceneManiaBeatSnapGrid.cs | 3 +++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 +++++-- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 ++-- .../Components/Timeline/TimelineBlueprintContainer.cs | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index fbc0ed1785..0df6b78bd1 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; using osuTK; @@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestHitObjectComposer : HitObjectComposer { public override Playfield Playfield { get; } + public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException(); public override IEnumerable HitObjects => Enumerable.Empty(); public override bool CursorInPlacementArea => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4d92a08bed..55295fa47b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -66,7 +66,8 @@ namespace osu.Game.Rulesets.Edit [Resolved] private OverlayColourProvider colourProvider { get; set; } - protected ComposeBlueprintContainer BlueprintContainer { get; private set; } + public override ComposeBlueprintContainer BlueprintContainer => blueprintContainer; + private ComposeBlueprintContainer blueprintContainer; protected ExpandingToolboxContainer LeftToolbox { get; private set; } @@ -137,7 +138,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer()) + .WithChild(blueprintContainer = CreateBlueprintContainer()) } }, new Container @@ -532,6 +533,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract Playfield Playfield { get; } + public abstract ComposeBlueprintContainer BlueprintContainer { get; } + /// /// All s in currently loaded beatmap. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4fba798a26..ecfaf1f72f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private void commitIfPlacementActive() + public void CommitIfPlacementActive() { CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); removePlacement(); @@ -402,7 +402,7 @@ namespace osu.Game.Screens.Edit.Compose.Components currentTool = value; // As per stable editor, when changing tools, we should forcefully commit any pending placement. - commitIfPlacementActive(); + CommitIfPlacementActive(); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 6ebd1961a2..9a8fdc3dac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -170,6 +170,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void UpdateSelectionFromDragBox() { + Composer.BlueprintContainer.CommitIfPlacementActive(); + var dragBox = (TimelineDragBox)DragBox; double minTime = dragBox.MinTime; double maxTime = dragBox.MaxTime; From 31a8bc75532a8a11da972e3a595246cd5baf43d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 14:12:55 +0200 Subject: [PATCH 020/130] Remove redundant qualifier --- .../Editor/TestSceneManiaBeatSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 0df6b78bd1..127beed83e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } } } 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 021/130] 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 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 022/130] 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 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 023/130] 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 024/130] 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 ec2b8f3bc3f9ab05afed177e21fbbd3147b7cfb3 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 16 Jun 2024 17:12:35 +0300 Subject: [PATCH 025/130] Changed timed difficulty attributes to be "per-HitObject" instead of "per-DifficultyHitObject" --- .../Difficulty/DifficultyCalculator.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d37cfc28b9..4644a457bf 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -109,26 +109,23 @@ namespace osu.Game.Rulesets.Difficulty var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); var difficultyObjects = getDifficultyHitObjects().ToArray(); - foreach (var obj in difficultyObjects) + int currentIndex = 0; + + foreach (var obj in Beatmap.HitObjects) { - // Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap. - // At the same time, we also need to consider the possibility DHOs may not be generated for any given object, - // so we'll add all remaining objects up to the current point in time to the progressive beatmap. - for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++) - { - if (obj != difficultyObjects[^1] && Beatmap.HitObjects[i].StartTime > obj.BaseObject.StartTime) - break; + progressiveBeatmap.HitObjects.Add(obj); - progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]); + while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime()) + { + foreach (var skill in skills) + { + cancellationToken.ThrowIfCancellationRequested(); + skill.Process(difficultyObjects[currentIndex]); + } + currentIndex++; } - foreach (var skill in skills) - { - cancellationToken.ThrowIfCancellationRequested(); - skill.Process(obj); - } - - attribs.Add(new TimedDifficultyAttributes(obj.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); + attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } return attribs; From 1ddfc8f0110e374063f2b0f8e5b9d062f1714e9b Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 18 Jun 2024 15:48:21 +0300 Subject: [PATCH 026/130] Updated the tests according to new logic and fixed one minor CI code quality thing --- .../TestSceneTimedDifficultyCalculation.cs | 45 ++++++------------- .../Difficulty/DifficultyCalculator.cs | 1 + 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index 1a75f735ef..f860cd097a 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.NonVisual public class TestSceneTimedDifficultyCalculation { [Test] - public void TestAttributesGeneratedForAllNonSkippedObjects() + public void TestAttributesGeneratedForEachObjectOnce() { var beatmap = new Beatmap { @@ -40,15 +40,14 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(4)); + Assert.That(attribs.Count, Is.EqualTo(3)); assertEquals(attribs[0], beatmap.HitObjects[0]); assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); - assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object. - assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] - public void TestAttributesNotGeneratedForSkippedObjects() + public void TestAttributesGeneratedForSkippedObjects() { var beatmap = new Beatmap { @@ -72,35 +71,14 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(1)); - assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); - } - - [Test] - public void TestNestedObjectOnlyAddsParentOnce() - { - var beatmap = new Beatmap - { - HitObjects = - { - new TestHitObject - { - StartTime = 1, - Skip = true, - Nested = 2 - }, - } - }; - - List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - - Assert.That(attribs.Count, Is.EqualTo(2)); + Assert.That(attribs.Count, Is.EqualTo(3)); assertEquals(attribs[0], beatmap.HitObjects[0]); - assertEquals(attribs[1], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] - public void TestSkippedLastObjectAddedInLastIteration() + public void TestAttributesGeneratedOnceForSkippedObjects() { var beatmap = new Beatmap { @@ -110,6 +88,7 @@ namespace osu.Game.Tests.NonVisual new TestHitObject { StartTime = 2, + Nested = 5, Skip = true }, new TestHitObject @@ -122,8 +101,10 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(1)); - assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + Assert.That(attribs.Count, Is.EqualTo(3)); + assertEquals(attribs[0], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 4644a457bf..8884166255 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -122,6 +122,7 @@ namespace osu.Game.Rulesets.Difficulty cancellationToken.ThrowIfCancellationRequested(); skill.Process(difficultyObjects[currentIndex]); } + currentIndex++; } 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 027/130] 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 028/130] 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 029/130] 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 030/130] 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 031/130] 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 032/130] 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 033/130] 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 ad2cd0ba8fb0b640155a704d2f39069eabec4985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 13:18:56 +0200 Subject: [PATCH 034/130] Adjust behaviour of hit animations toggle to match user expectations --- .../Components/HitCircleOverlapMarker.cs | 9 ------- .../HitCircles/HitCircleSelectionBlueprint.cs | 24 +++++++++++++++++++ .../Blueprints/Sliders/SliderCircleOverlay.cs | 14 +++++------ .../Sliders/SliderSelectionBlueprint.cs | 14 ++++++++++- .../Objects/Drawables/DrawableHitCircle.cs | 23 ++++++++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 18 ++++++++++++++ .../Objects/Drawables/DrawableSliderTail.cs | 24 +++++++++++++++++++ .../Objects/Drawables/DrawableHitObject.cs | 18 +++++++------- 8 files changed, 117 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index fe335a048d..8ed9d0476a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -7,7 +7,6 @@ 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.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Types; @@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Screens.Edit; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { @@ -48,13 +46,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, ring = new RingPiece { BorderThickness = 4, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index 0608f8c929..fd2bbe9916 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -16,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles protected readonly HitCirclePiece CirclePiece; private readonly HitCircleOverlapMarker marker; + private readonly Bindable showHitMarkers = new Bindable(); public HitCircleSelectionBlueprint(HitCircle circle) : base(circle) @@ -27,12 +31,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + showHitMarkers.BindValueChanged(_ => + { + if (!showHitMarkers.Value) + DrawableObject.RestoreHitAnimations(); + }); + } + protected override void Update() { base.Update(); CirclePiece.UpdateFrom(HitObject); marker.UpdateFrom(HitObject); + + if (showHitMarkers.Value) + DrawableObject.SuppressHitAnimations(); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index d47cf6bf23..bd3b4bbc54 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; @@ -14,18 +13,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly Slider slider; private readonly SliderPosition position; - private readonly HitCircleOverlapMarker marker; + private readonly HitCircleOverlapMarker? marker; public SliderCircleOverlay(Slider slider, SliderPosition position) { this.slider = slider; this.position = position; - InternalChildren = new Drawable[] - { - marker = new HitCircleOverlapMarker(), - CirclePiece = new HitCirclePiece(), - }; + if (position == SliderPosition.Start) + AddInternal(marker = new HitCircleOverlapMarker()); + + AddInternal(CirclePiece = new HitCirclePiece()); } protected override void Update() @@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle; CirclePiece.UpdateFrom(circle); - marker.UpdateFrom(circle); + marker?.UpdateFrom(circle); } public override void Hide() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 49fdf12d60..7ee6530099 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -59,6 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); private readonly BindableList selectedObjects = new BindableList(); + private readonly Bindable showHitMarkers = new Bindable(); public SliderSelectionBlueprint(Slider slider) : base(slider) @@ -66,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChildren = new Drawable[] { @@ -74,6 +76,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), }; + + config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers); } protected override void LoadComplete() @@ -90,6 +94,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (editorBeatmap != null) selectedObjects.BindTo(editorBeatmap.SelectedHitObjects); selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true); + showHitMarkers.BindValueChanged(_ => + { + if (!showHitMarkers.Value) + DrawableObject.RestoreHitAnimations(); + }); } public override bool HandleQuickDeletion() @@ -110,6 +119,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (IsSelected) BodyPiece.UpdateFrom(HitObject); + + if (showHitMarkers.Value) + DrawableObject.SuppressHitAnimations(); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c3ce6acce9..26e9773967 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -319,5 +320,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + UpdateComboColour(); + + using (BeginAbsoluteSequence(StateUpdateTime - 5)) + this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + + using (BeginAbsoluteSequence(HitStateUpdateTime)) + this.FadeOut(700).Expire(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit, force: true); + UpdateComboColour(); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e519e51562..7bae3cefcf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -370,5 +370,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private partial class DefaultSliderBody : PlaySliderBody { } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + HeadCircle.SuppressHitAnimations(); + TailCircle.SuppressHitAnimations(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit, force: true); + HeadCircle.RestoreHitAnimations(); + TailCircle.RestoreHitAnimations(); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index c4731118a1..21aa672d10 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -125,5 +127,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Slider != null) Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0); } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + UpdateComboColour(); + + using (BeginAbsoluteSequence(StateUpdateTime - 5)) + this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + + using (BeginAbsoluteSequence(HitStateUpdateTime)) + this.FadeOut(700).Expire(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit); + UpdateComboColour(); + } + + #endregion } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3ce6cc3cef..1f735576bc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private void updateStateFromResult() { if (Result.IsHit) - updateState(ArmedState.Hit, true); + UpdateState(ArmedState.Hit, true); else if (Result.HasResult) - updateState(ArmedState.Miss, true); + UpdateState(ArmedState.Miss, true); else - updateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle, true); } protected sealed override void OnFree(HitObjectLifetimeEntry entry) @@ -402,7 +402,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onRevertResult() { - updateState(ArmedState.Idle); + UpdateState(ArmedState.Idle); OnRevertResult?.Invoke(this, Result); } @@ -421,7 +421,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result is not null) { Result.TimeOffset = 0; - updateState(State.Value, true); + UpdateState(State.Value, true); } DefaultsApplied?.Invoke(this); @@ -461,7 +461,7 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException( $"Should never clear a {nameof(DrawableHitObject)} as the base implementation adds components. If attempting to use {nameof(InternalChild)} or {nameof(InternalChildren)}, using {nameof(AddInternal)} or {nameof(AddRangeInternal)} instead."); - private void updateState(ArmedState newState, bool force = false) + protected void UpdateState(ArmedState newState, bool force = false) { if (State.Value == newState && !force) return; @@ -506,7 +506,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Reapplies the current . /// - public void RefreshStateTransforms() => updateState(State.Value, true); + public void RefreshStateTransforms() => UpdateState(State.Value, true); /// /// Apply (generally fade-in) transforms leading into the start time. @@ -565,7 +565,7 @@ namespace osu.Game.Rulesets.Objects.Drawables ApplySkin(CurrentSkin, true); if (IsLoaded) - updateState(State.Value, true); + UpdateState(State.Value, true); } protected void UpdateComboColour() @@ -725,7 +725,7 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate; if (Result.HasResult) - updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); + UpdateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); OnNewResult?.Invoke(this, Result); } From 74399542d2e72c4bff4d7b6155202477b9bc5567 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 20 Jun 2024 17:27:15 +0200 Subject: [PATCH 035/130] 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 036/130] 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 89d3f67eb3486c422b9ce874e8fd1a000e320938 Mon Sep 17 00:00:00 2001 From: sometimes <76718358+ssz7-ch2@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:06:00 -0400 Subject: [PATCH 037/130] fix accuracyProcess typo --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 70d7f0fe37..0b20f1089a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -369,9 +369,9 @@ namespace osu.Game.Rulesets.Scoring MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; - double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; + double accuracyProgress = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; - TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion)); + TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProgress, currentBonusPortion)); TotalScore.Value = (long)Math.Round(TotalScoreWithoutMods.Value * scoreMultiplier); } From 80907acaa69e09b75865441646191feb1a3ee0be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jun 2024 22:01:05 +0800 Subject: [PATCH 038/130] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3f2fba4dc2..6114fd769e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From df43a1c6ccf42aaf8a08d659d84b718bef58b826 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jun 2024 03:31:40 +0800 Subject: [PATCH 039/130] Add note about every-frame-transforms --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 26e9773967..6f419073eb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -328,6 +328,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables UpdateState(ArmedState.Idle); UpdateComboColour(); + // This method is called every frame. If we need to, the following can likely be converted + // to code which doesn't use transforms at all. + + // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) using (BeginAbsoluteSequence(StateUpdateTime - 5)) this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); From 1dc9f102358ef72ff8a7ce41bfcbbbb66a041a1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jun 2024 09:46:23 +0800 Subject: [PATCH 040/130] Fix scale control key binding breaking previous defaults Oops from ppy/osu#28309. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2af564d8ba..2452852f6f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -412,9 +412,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] - EditorToggleScaleControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, @@ -432,6 +429,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))] DecreaseModSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] + EditorToggleScaleControl, } public enum GlobalActionCategory From 66b093b17e4166aec436ee4fd5f6a327155b144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jun 2024 14:29:15 +0200 Subject: [PATCH 041/130] Implement score breakdown display for daily challenge screen --- .../TestSceneDailyChallengeScoreBreakdown.cs | 61 ++++++ .../DailyChallengeScoreBreakdown.cs | 193 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs new file mode 100644 index 0000000000..5523d02694 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeScoreBreakdown : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicAppearance() + { + DailyChallengeScoreBreakdown breakdown = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + breakdown = new DailyChallengeScoreBreakdown + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (breakdown.IsNotNull()) + breakdown.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (breakdown.IsNotNull()) + breakdown.Height = height; + }); + + AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); + AddStep("add new score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + breakdown.AddNewScore(testScore); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs new file mode 100644 index 0000000000..b5c44e42a5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -0,0 +1,193 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeScoreBreakdown : CompositeDrawable + { + private FillFlowContainer barsContainer = null!; + + private const int bin_count = 13; + private long[] bins = new long[bin_count]; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new SectionHeader("Score breakdown"), + barsContainer = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Both, + Height = 0.9f, + Padding = new MarginPadding { Top = 35 }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + } + }; + + for (int i = 0; i < bin_count; ++i) + { + LocalisableString? label = null; + + switch (i) + { + case 2: + case 4: + case 6: + case 8: + label = @$"{100 * i}k"; + break; + + case 10: + label = @"1M"; + break; + } + + barsContainer.Add(new Bar(label) + { + Width = 1f / bin_count, + }); + } + } + + public void AddNewScore(IScoreInfo scoreInfo) + { + int targetBin = (int)Math.Clamp(Math.Floor((float)scoreInfo.TotalScore / 100000), 0, bin_count - 1); + bins[targetBin] += 1; + updateCounts(); + + var text = new OsuSpriteText + { + Text = scoreInfo.TotalScore.ToString(@"N0"), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Default.With(size: 30), + RelativePositionAxes = Axes.X, + X = (targetBin + 0.5f) / bin_count - 0.5f, + Alpha = 0, + }; + AddInternal(text); + + Scheduler.AddDelayed(() => + { + float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y; + text.FadeInFromZero() + .ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf) + .MoveToY(startY) + .MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint) + .FadeOut(2500, Easing.OutQuint) + .Expire(); + }, 150); + } + + public void SetInitialCounts(long[] counts) + { + if (counts.Length != bin_count) + throw new ArgumentException(@"Incorrect number of bins.", nameof(counts)); + + bins = counts; + updateCounts(); + } + + private void updateCounts() + { + long max = bins.Max(); + for (int i = 0; i < bin_count; ++i) + barsContainer[i].UpdateCounts(bins[i], max); + } + + private partial class Bar : CompositeDrawable + { + private readonly LocalisableString? label; + + private long count; + private long max; + + public Container CircularBar { get; private set; } = null!; + + public Bar(LocalisableString? label = null) + { + this.label = label; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + AddInternal(new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Bottom = 20, + Horizontal = 3, + }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Masking = true, + Child = CircularBar = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Height = 0.01f, + Masking = true, + CornerRadius = 10, + Colour = colourProvider.Highlight1, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }); + + if (label != null) + { + AddInternal(new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + Text = label.Value, + Colour = colourProvider.Content2, + }); + } + } + + protected override void Update() + { + base.Update(); + + CircularBar.CornerRadius = CircularBar.DrawWidth / 4; + } + + public void UpdateCounts(long newCount, long newMax) + { + bool isIncrement = newCount > count; + + count = newCount; + max = newMax; + + CircularBar.ResizeHeightTo(0.01f + 0.99f * count / max, 300, Easing.OutQuint); + if (isIncrement) + CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint); + } + } + } +} From 2de42854c3d5686f8725a7310f5f387c7a879a05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 15:43:52 +0900 Subject: [PATCH 042/130] Fix corner radius looking bad when graph bars are too short --- .../OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index b5c44e42a5..3d4f27c44b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { base.Update(); - CircularBar.CornerRadius = CircularBar.DrawWidth / 4; + CircularBar.CornerRadius = Math.Min(CircularBar.DrawHeight / 2, CircularBar.DrawWidth / 4); } public void UpdateCounts(long newCount, long newMax) From b4cefe0cc2fda0ab4b5af6138ee158bd32262f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 15:50:11 +0900 Subject: [PATCH 043/130] Update rider `vcsConfiguration` Updated in recent rider versions --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 4bb9f4d2a0..86cc6c63e5 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 2cb18820ea0a2052bde8fcf911f8e06571703116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 10:07:58 +0200 Subject: [PATCH 044/130] Fix incorrect slider judgement positions when classic mod is active Regressed in https://github.com/ppy/osu/pull/27977. Bit ad-hoc but don't see how to fix without just reverting the change. --- .../Objects/Drawables/DrawableOsuJudgement.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 6e252a53e2..0630ecfbb5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -48,10 +48,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { - Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); - Scale = new Vector2(osuObject.HitObject.Scale); + switch (osuObject) + { + case DrawableSlider slider: + Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!); + break; + + default: + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); + break; + } positionTransferred = true; + + Scale = new Vector2(osuObject.HitObject.Scale); } } From 1eac0c622add0e5b0cf3bfda45dcbf86a90393ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 10:27:25 +0200 Subject: [PATCH 045/130] Fix legacy skin hold note bodies not appearing when scrolling upwards - Closes https://github.com/ppy/osu/issues/28567. - Regressed in https://github.com/ppy/osu/pull/28466. Bit of a facepalm moment innit... --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 087b428801..6de0752671 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy // i dunno this looks about right?? // the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild. if (sprite.DrawHeight > 0) - bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight)); + bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight)); } break; From 0d2a47167c72d29b2fa9bb46ead50c2e8e968960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 11:28:10 +0200 Subject: [PATCH 046/130] Fix crash on calculating playlist duration when rate-changing mods are present Regressed in https://github.com/ppy/osu/pull/28399. To reproduce, enter a playlist that has an item with a rate-changing mod (rather than create it yourself). This is happening because `APIRuleset` has `CreateInstance()` unimplemented: https://github.com/ppy/osu/blob/b4cefe0cc2fda0ab4b5af6138ee158bd32262f9a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs#L159 and only triggers when the playlist items in question originate from web. This is why it is bad to have interface implementations throw outside of maybe mock implementations for tests. `CreateInstance()` is a scourge elsewhere in general, we need way less of it in the codebase (because while convenient, it's also problematic to implement in online contexts, and also expensive because reflection). --- osu.Game/Online/Rooms/PlaylistExtensions.cs | 5 +++-- .../OnlinePlay/Components/OverlinedPlaylistHeader.cs | 7 ++++++- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index e9a0519f3d..8591b5bb47 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; +using osu.Game.Rulesets; using osu.Game.Utils; namespace osu.Game.Online.Rooms @@ -42,14 +43,14 @@ namespace osu.Game.Online.Rooms /// /// Returns the total duration from the in playlist order from the supplied , /// - public static string GetTotalDuration(this BindableList playlist) => + public static string GetTotalDuration(this BindableList playlist, RulesetStore rulesetStore) => playlist.Select(p => { double rate = 1; if (p.RequiredMods.Length > 0) { - var ruleset = p.Beatmap.Ruleset.CreateInstance(); + var ruleset = rulesetStore.GetRuleset(p.RulesetID)!.CreateInstance(); rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset))); } diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index fc86cbbbdd..dd728e460b 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -1,12 +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 osu.Framework.Allocation; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { public partial class OverlinedPlaylistHeader : OverlinedHeader { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + public OverlinedPlaylistHeader() : base("Playlist") { @@ -16,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { base.LoadComplete(); - Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(), true); + Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(rulesets), true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 84e419d67a..9166cac9de 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; using osu.Game.Localisation; +using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -78,6 +79,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private IBindable localUser = null!; private readonly Room room; @@ -366,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public void SelectBeatmap() => editPlaylistButton.TriggerClick(); private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => - playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; + playlistLength.Text = $"Length: {Playlist.GetTotalDuration(rulesets)}"; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 && hasValidDuration; From 2de08525514ceac86c242368450beca37bb4e198 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:06:50 +0800 Subject: [PATCH 047/130] Fix transient rank value applied to bindable --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 0b20f1089a..44ddb8c187 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -381,9 +381,12 @@ namespace osu.Game.Rulesets.Scoring if (rank.Value == ScoreRank.F) return; - rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); + ScoreRank newRank = RankFromScore(Accuracy.Value, ScoreResultCounts); + foreach (var mod in Mods.Value.OfType()) - rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); + newRank = mod.AdjustRank(newRank, Accuracy.Value); + + rank.Value = newRank; } protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) From 03cdfd06602be30c6ff130b07154debf5f3625dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:25:37 +0200 Subject: [PATCH 048/130] Fix timeline break piece crashing on drag if there are no objects before start or after end This fixes the direct cause of https://github.com/ppy/osu/issues/28577. --- .../Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 608c2bdab1..025eb8bede 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -170,8 +170,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline changeHandler?.BeginChange(); updateState(); - double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime(); - double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime; + double min = beatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() <= Break.Value.StartTime)?.GetEndTime() ?? double.NegativeInfinity; + double max = beatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= Break.Value.EndTime)?.StartTime ?? double.PositiveInfinity; if (isStartHandle) max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION); From 18e2a925a8130f1a48228c706a1ce863e0e6da30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:34:37 +0200 Subject: [PATCH 049/130] Add failing test coverage for manual breaks at start/end of map not being culled --- .../TestSceneEditorBeatmapProcessor.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 02ce3815ec..50f37e2070 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -296,5 +296,109 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8800)); }); } + + [Test] + public void TestBreaksAtEndOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + }, + Breaks = + { + new BreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestManualBreaksAtEndOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + }, + Breaks = + { + new ManualBreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestBreaksAtStartOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 10000 }, + new HitCircle { StartTime = 11000 }, + }, + Breaks = + { + new BreakPeriod(0, 9000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestManualBreaksAtStartOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 10000 }, + new HitCircle { StartTime = 11000 }, + }, + Breaks = + { + new ManualBreakPeriod(0, 9000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } } } From fae6dcfffa5bac1e4dc23f62a212a57589c00e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:48:54 +0200 Subject: [PATCH 050/130] Remove manual breaks at the start/end of beatmap This is the secondary cause of https://github.com/ppy/osu/issues/28577, because you could do the following: - Have a break autogenerate itself - Adjust either end of it to make it mark itself as manually-adjusted - Remove all objects before or after said break to end up in a state wherein there are no objects before or after a break. The direct fix is still correct because it is still technically possible to end up in a state wherein a break is before or after all objects (obvious one is manual `.osu` editing), but this behaviour is also undesirable for the autogeneration logic. --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 37bf915cec..bcbee78280 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -40,8 +40,12 @@ namespace osu.Game.Screens.Edit foreach (var manualBreak in Beatmap.Breaks.ToList()) { - if (Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) + if (manualBreak.EndTime <= Beatmap.HitObjects.FirstOrDefault()?.StartTime + || manualBreak.StartTime >= Beatmap.HitObjects.LastOrDefault()?.GetEndTime() + || Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) + { Beatmap.Breaks.Remove(manualBreak); + } } for (int i = 1; i < Beatmap.HitObjects.Count; ++i) From 2fda45cad4f6632409a396368e472fcfc7b2e859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 15:01:43 +0200 Subject: [PATCH 051/130] Fix crashes when opening scale/rotation popovers during selection box operations --- .../Edit/OsuSelectionRotationHandler.cs | 12 ++++++++---- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++++---- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 6 ++++-- .../Compose/Components/SelectionBoxRotationHandle.cs | 3 +++ .../Compose/Components/SelectionBoxScaleHandle.cs | 3 +++ .../Compose/Components/SelectionRotationHandler.cs | 7 +++++++ .../Edit/Compose/Components/SelectionScaleHandler.cs | 7 +++++++ 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 7624b2f27e..62a39d3702 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -53,9 +53,11 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Begin() { - if (objectsInRotation != null) + if (OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + base.Begin(); + changeHandler?.BeginChange(); objectsInRotation = selectedMovableObjects.ToArray(); @@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Update(float rotation, Vector2? origin = null) { - if (objectsInRotation == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; @@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Commit() { - if (objectsInRotation == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); changeHandler?.EndChange(); + base.Commit(); + objectsInRotation = null; originalPositions = null; originalPathControlPointPositions = null; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index de00aa6ad3..f4fd48f183 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Begin() { - if (objectsInScale != null) + if (OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); + base.Begin(); + changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); @@ -86,10 +88,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { - if (objectsInScale == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); - Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); + Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; @@ -117,11 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Commit() { - if (objectsInScale == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); changeHandler?.EndChange(); + base.Commit(); + objectsInScale = null; OriginalSurroundingQuad = null; defaultOrigin = null; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 28d0f8320f..a3547d45e5 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -77,13 +77,15 @@ namespace osu.Game.Rulesets.Osu.Edit { case GlobalAction.EditorToggleRotateControl: { - rotateButton.TriggerClick(); + if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) + rotateButton.TriggerClick(); return true; } case GlobalAction.EditorToggleScaleControl: { - scaleButton.TriggerClick(); + if (!ScaleHandler.OperationInProgress.Value || scaleButton.Selected.Value) + scaleButton.TriggerClick(); return true; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 5270162189..b9383f1bad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -67,6 +67,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (rotationHandler == null) return false; + if (rotationHandler.OperationInProgress.Value) + return false; + rotationHandler.Begin(); return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index eca0c08ba1..3f4f2c2854 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (scaleHandler == null) return false; + if (scaleHandler.OperationInProgress.Value) + return false; + originalAnchor = Anchor; scaleHandler.Begin(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 787716a38c..532daaf7fa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -12,6 +12,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public partial class SelectionRotationHandler : Component { + /// + /// Whether there is any ongoing scale operation right now. + /// + public Bindable OperationInProgress { get; private set; } = new BindableBool(); + /// /// Whether rotation anchored by the selection origin can currently be performed. /// @@ -50,6 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Begin() { + OperationInProgress.Value = true; } /// @@ -85,6 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Commit() { + OperationInProgress.Value = false; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index a96f627e56..c91362219c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -13,6 +13,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public partial class SelectionScaleHandler : Component { + /// + /// Whether there is any ongoing scale operation right now. + /// + public Bindable OperationInProgress { get; private set; } = new BindableBool(); + /// /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// @@ -63,6 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Begin() { + OperationInProgress.Value = true; } /// @@ -99,6 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Commit() { + OperationInProgress.Value = false; } } } From 0a440226979db17af8b76a238371362be35099eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 23:03:37 +0900 Subject: [PATCH 052/130] 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 3c115d1371..0f1a14afd8 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 449e4b0032..6ed60b00b3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From d722be16e39683c59738f9c66976a9b2c92f83f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 23:41:43 +0900 Subject: [PATCH 053/130] Add missing `base` calls for safety --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 4 ++++ osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 79a808bbd2..28763051e3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -84,6 +84,8 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = getTargetContainer(); initialRotation = targetContainer!.Rotation; + + base.Begin(); } public override void Update(float rotation, Vector2? origin = null) @@ -102,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = null; initialRotation = null; + + base.Commit(); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 6a118a73a8..36b38543d1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -61,6 +61,8 @@ namespace osu.Game.Overlays.SkinEditor originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + + base.Begin(); } public override void Update(float rotation, Vector2? origin = null) @@ -99,6 +101,8 @@ namespace osu.Game.Overlays.SkinEditor originalPositions = null; originalRotations = null; defaultOrigin = null; + + base.Commit(); } } } From dc817b62ccd63ed6477d8f2082d980769e5f359b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 00:49:56 +0900 Subject: [PATCH 054/130] Fix editor performance dropping over time when hit markers are enabled There's probably a better solution but let's hotfix this for now. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 3 ++- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 6f419073eb..7d707dea6c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -325,13 +325,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); UpdateComboColour(); // This method is called every frame. If we need to, the following can likely be converted // to code which doesn't use transforms at all. // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + using (BeginAbsoluteSequence(StateUpdateTime - 5)) this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7bae3cefcf..02d0ebee83 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); HeadCircle.SuppressHitAnimations(); TailCircle.SuppressHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 21aa672d10..42abf41d6f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); UpdateComboColour(); using (BeginAbsoluteSequence(StateUpdateTime - 5)) From df64d7f37458515dabde77792aeda9090e9f103e Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 25 Jun 2024 23:06:42 +0500 Subject: [PATCH 055/130] Refactor out taiko Peaks skill --- .../Difficulty/Skills/Peaks.cs | 93 ------------------- .../Difficulty/TaikoDifficultyCalculator.cs | 70 ++++++++++++-- 2 files changed, 64 insertions(+), 99 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs deleted file mode 100644 index 91d8e93543..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Peaks : Skill - { - private const double rhythm_skill_multiplier = 0.2 * final_multiplier; - private const double colour_skill_multiplier = 0.375 * final_multiplier; - private const double stamina_skill_multiplier = 0.375 * final_multiplier; - - private const double final_multiplier = 0.0625; - - private readonly Rhythm rhythm; - private readonly Colour colour; - private readonly Stamina stamina; - - public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier; - public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; - public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; - - public Peaks(Mod[] mods) - : base(mods) - { - rhythm = new Rhythm(mods); - colour = new Colour(mods); - stamina = new Stamina(mods); - } - - /// - /// Returns the p-norm of an n-dimensional vector. - /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); - - public override void Process(DifficultyHitObject current) - { - rhythm.Process(current); - colour.Process(current); - stamina.Process(current); - } - - /// - /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. - /// - /// - /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. - /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). - /// - public override double DifficultyValue() - { - List peaks = new List(); - - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); - var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); - var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); - - for (int i = 0; i < colourPeaks.Count; i++) - { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; - - double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); - - // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). - // These sections will not contribute to the difficulty. - if (peak > 0) - peaks.Add(peak); - } - - double difficulty = 0; - double weight = 1; - - foreach (double strain in peaks.OrderDescending()) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b84c2d25ee..9b746d47ea 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { private const double difficulty_multiplier = 1.35; + private const double final_multiplier = 0.0625; + private const double rhythm_skill_multiplier = 0.2 * final_multiplier; + private const double colour_skill_multiplier = 0.375 * final_multiplier; + private const double stamina_skill_multiplier = 0.375 * final_multiplier; + public override int Version => 20221107; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { return new Skill[] { - new Peaks(mods) + new Rhythm(mods), + new Colour(mods), + new Stamina(mods) }; } @@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - var combined = (Peaks)skills[0]; + Colour colour = (Colour)skills.First(x => x is Colour); + Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Stamina stamina = (Stamina)skills.First(x => x is Stamina); - double colourRating = combined.ColourDifficultyValue * difficulty_multiplier; - double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier; - double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier; + double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier; - double combinedRating = combined.DifficultyValue() * difficulty_multiplier; + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier; double starRating = rescale(combinedRating * 1.4); HitWindows hitWindows = new TaikoHitWindows(); @@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 10.43 * Math.Log(sr / 8 + 1); } + + /// + /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. + /// + /// + /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. + /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). + /// + private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + { + List peaks = new List(); + + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); + var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); + + for (int i = 0; i < colourPeaks.Count; i++) + { + double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + + double peak = norm(1.5, colourPeak, staminaPeak); + peak = norm(2, peak, rhythmPeak); + + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + if (peak > 0) + peaks.Add(peak); + } + + double difficulty = 0; + double weight = 1; + + foreach (double strain in peaks.OrderDescending()) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + + /// + /// Returns the p-norm of an n-dimensional vector. + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } } From 5d4509150bf5b107f36c1123f9eaae81d4bc864f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 11:56:58 +0900 Subject: [PATCH 056/130] Adjust beatmap carousel's spacing to remove dead-space As discussed in https://github.com/ppy/osu/discussions/28599. I think this feels better overall, and would like to apply the change before other design changes to the carousel. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 49c23bdbbf..3aa980cec0 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1000,8 +1000,6 @@ namespace osu.Game.Screens.Select return set; } - private const float panel_padding = 5; - /// /// Computes the target Y positions for every item in the carousel. /// @@ -1023,10 +1021,18 @@ namespace osu.Game.Screens.Select { case CarouselBeatmapSet set: { + bool isSelected = item.State.Value == CarouselItemState.Selected; + + float padding = isSelected ? 5 : -5; + + if (isSelected) + // double padding because we want to cancel the negative padding from the last item. + currentY += padding * 2; + visibleItems.Add(set); set.CarouselYPosition = currentY; - if (item.State.Value == CarouselItemState.Selected) + if (isSelected) { // scroll position at currentY makes the set panel appear at the very top of the carousel's screen space // move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas) @@ -1048,7 +1054,7 @@ namespace osu.Game.Screens.Select } } - currentY += set.TotalHeight + panel_padding; + currentY += set.TotalHeight + padding; break; } } From e84daedbea9a5dadc6e47eb3a136e63333619eae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 12:01:38 +0900 Subject: [PATCH 057/130] Reduce length of fade-out when hiding beatmap panels --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 0c3de5848b..4c9ac57d9d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -144,9 +144,9 @@ namespace osu.Game.Screens.Select.Carousel } if (!Item.Visible) - this.FadeOut(300, Easing.OutQuint); + this.FadeOut(100, Easing.OutQuint); else - this.FadeIn(250); + this.FadeIn(400, Easing.OutQuint); } protected virtual void Selected() From 0379abd714782a9e3d01cae2980859bd0b88541e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 13:56:52 +0900 Subject: [PATCH 058/130] Prevent multiple invocations of failure procedure --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 9e4c06b783..2799cd4b36 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Scoring /// public void TriggerFailure() { + if (HasFailed) + return; + if (Failed?.Invoke() != false) HasFailed = true; } From fa46d8e6c91d721a88be7282a40edb8d35e24dff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jun 2024 08:39:55 +0300 Subject: [PATCH 059/130] Fix intermittent failure in online menu banner tests --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 240421b360..6b27de9996 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -3,6 +3,8 @@ using System.Linq; using NUnit.Framework; +using NUnit.Framework.Internal; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; @@ -20,6 +22,7 @@ namespace osu.Game.Tests.Visual.Menus { base.SetUpSteps(); AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false); + AddStep("disable return to top on idle", () => Game.ChildrenOfType().Single().ReturnToTopOnIdle = false); } [Test] From 2e03afb2ed4c1a779ef12f74dbecd32896dd310b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 14:47:58 +0900 Subject: [PATCH 060/130] Always log missing official build attribute --- osu.Game/OsuGame.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 667c3ecb99..cf32daab00 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Humanizer; @@ -871,6 +872,9 @@ namespace osu.Game { base.LoadComplete(); + if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) + Logger.Log(NotificationsStrings.NotOfficialBuild.ToString()); + var languages = Enum.GetValues(); var mappings = languages.Select(language => From 0c8279c5df28a9c816849c4bc3c226a6ba6bd088 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 14:50:53 +0900 Subject: [PATCH 061/130] Remove unused using statements --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 6b27de9996..57cff38ab0 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -3,8 +3,6 @@ using System.Linq; using NUnit.Framework; -using NUnit.Framework.Internal; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; From 006184ed2f220e37dca2c0d889e4215a5759a4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 13:16:30 +0200 Subject: [PATCH 062/130] Implement carousel container for daily challenge screen --- .../TestSceneDailyChallengeCarousel.cs | 176 +++++++++++++ .../DailyChallenge/DailyChallengeCarousel.cs | 234 ++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs new file mode 100644 index 0000000000..640a895751 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -0,0 +1,176 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeCarousel : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + private readonly Bindable room = new Bindable(new Room()); + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) + { + Model = { BindTarget = room } + }; + + [Test] + public void TestBasicAppearance() + { + DailyChallengeCarousel carousel = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + carousel = new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (carousel.IsNotNull()) + carousel.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (carousel.IsNotNull()) + carousel.Height = height; + }); + AddRepeatStep("add content", () => carousel.Add(new FakeContent()), 3); + } + + [Test] + public void TestIntegration() + { + GridContainer grid = null!; + DailyChallengeCarousel carousel = null!; + DailyChallengeEventFeed feed = null!; + DailyChallengeScoreBreakdown breakdown = null!; + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + grid = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RowDimensions = + [ + new Dimension(), + new Dimension() + ], + Content = new[] + { + new Drawable[] + { + carousel = new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new DailyChallengeTimeRemainingRing(), + breakdown = new DailyChallengeScoreBreakdown(), + } + } + }, + [ + feed = new DailyChallengeEventFeed + { + RelativeSizeAxes = Axes.Both, + } + ], + } + }, + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (grid.IsNotNull()) + grid.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (grid.IsNotNull()) + grid.Height = height; + }); + AddSliderStep("update time remaining", 0f, 1f, 0f, progress => + { + var startedTimeAgo = TimeSpan.FromHours(24) * progress; + room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo; + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddStep("add normal score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null)); + breakdown.AddNewScore(testScore); + }); + AddStep("add new user best", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000))); + breakdown.AddNewScore(testScore); + }); + } + + private partial class FakeContent : CompositeDrawable + { + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Fake Content " + (char)('A' + RNG.Next(26)), + }, + }; + + text.FadeOut(500, Easing.OutQuint) + .Then().FadeIn(500, Easing.OutQuint) + .Loop(); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs new file mode 100644 index 0000000000..2fd5253347 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -0,0 +1,234 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeCarousel : Container + { + private const int switch_interval = 20_500; + + private readonly Container content; + private readonly FillFlowContainer navigationFlow; + + protected override Container Content => content; + + private double clockStartTime; + private int lastDisplayed = -1; + + public DailyChallengeCarousel() + { + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = 40 }, + }, + navigationFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 15, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Spacing = new Vector2(10), + } + }; + } + + public override void Add(Drawable drawable) + { + drawable.RelativeSizeAxes = Axes.Both; + drawable.Size = Vector2.One; + drawable.AlwaysPresent = true; + drawable.Alpha = 0; + + base.Add(drawable); + + navigationFlow.Add(new NavigationDot { Clicked = onManualNavigation }); + } + + public override bool Remove(Drawable drawable, bool disposeImmediately) + { + int index = content.IndexOf(drawable); + + if (index > 0) + navigationFlow.Remove(navigationFlow[index], true); + + return base.Remove(drawable, disposeImmediately); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + clockStartTime = Clock.CurrentTime; + } + + protected override void Update() + { + base.Update(); + + if (content.Count == 0) + { + lastDisplayed = -1; + return; + } + + double elapsed = Clock.CurrentTime - clockStartTime; + + int currentDisplay = (int)(elapsed / switch_interval) % content.Count; + double displayProgress = (elapsed % switch_interval) / switch_interval; + + navigationFlow[currentDisplay].Active.Value = true; + + if (content.Count > 1) + navigationFlow[currentDisplay].Progress = (float)displayProgress; + + if (currentDisplay == lastDisplayed) + return; + + if (lastDisplayed >= 0) + { + content[lastDisplayed].FadeOutFromOne(250, Easing.OutQuint); + navigationFlow[lastDisplayed].Active.Value = false; + } + + content[currentDisplay].Delay(250).Then().FadeInFromZero(250, Easing.OutQuint); + + lastDisplayed = currentDisplay; + } + + private void onManualNavigation(NavigationDot obj) + { + int index = navigationFlow.IndexOf(obj); + + if (index < 0) + return; + + clockStartTime = Clock.CurrentTime - index * switch_interval; + } + + private partial class NavigationDot : CompositeDrawable + { + public Action? Clicked { get; set; } + + public BindableBool Active { get; set; } = new BindableBool(); + + private double progress; + + public float Progress + { + set + { + if (progress == value) + return; + + progress = value; + progressLayer.Width = value; + } + } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box background = null!; + private Box progressLayer = null!; + private Box hoverLayer = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(15); + + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Light4, + }, + progressLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = colourProvider.Highlight1, + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + } + } + }, + new HoverClickSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(val => + { + if (val.NewValue) + { + background.FadeColour(colourProvider.Highlight1, 250, Easing.OutQuint); + this.ResizeWidthTo(30, 250, Easing.OutQuint); + progressLayer.Width = 0; + progressLayer.Alpha = 0.5f; + } + else + { + background.FadeColour(colourProvider.Light4, 250, Easing.OutQuint); + this.ResizeWidthTo(15, 250, Easing.OutQuint); + progressLayer.FadeOut(250, Easing.OutQuint); + } + }, true); + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + hoverLayer.FadeTo(0.2f, 250, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverLayer.FadeOut(250, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + Clicked?.Invoke(this); + + hoverLayer.FadeTo(1) + .Then().FadeTo(IsHovered ? 0.2f : 0, 250, Easing.OutQuint); + + return true; + } + } + } +} From 276d8fe1582ca00e774c12be32b2abf47bd4ac8b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 16:20:54 +0900 Subject: [PATCH 063/130] Truncate break times for legacy beatmap export --- osu.Game/Database/LegacyBeatmapExporter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 69120ea885..17c2c8c88d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -8,6 +8,7 @@ using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Timing; using osu.Game.IO; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -59,9 +60,13 @@ namespace osu.Game.Database // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves + foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); + 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)); + foreach (var hitObject in playableBeatmap.HitObjects) { // Truncate end time before truncating start time because end time is dependent on start time From ac235cb5068f5577d0feb1fc2ca39b223cc746ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 16:22:25 +0900 Subject: [PATCH 064/130] Remove unused local variable --- .../Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs index 640a895751..b9143945c4 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -66,9 +66,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge public void TestIntegration() { GridContainer grid = null!; - DailyChallengeCarousel carousel = null!; DailyChallengeEventFeed feed = null!; DailyChallengeScoreBreakdown breakdown = null!; + AddStep("create content", () => Children = new Drawable[] { new Box @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { new Drawable[] { - carousel = new DailyChallengeCarousel + new DailyChallengeCarousel { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, From 015a71f3d531f5d45cf81a286aec4976400fab61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 16:49:22 +0900 Subject: [PATCH 065/130] Add projectSettingsUpdater.xml to .gitignore and remove from tracking --- .gitignore | 1 + .idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu/.idea/projectSettingsUpdater.xml | 6 ------ 5 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 .idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu/.idea/projectSettingsUpdater.xml diff --git a/.gitignore b/.gitignore index 11fee27f28..a51ad09d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -265,6 +265,7 @@ __pycache__/ .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf +.idea/*/.idea/projectSettingsUpdater.xml # Generated files .idea/**/contentModel.xml diff --git a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 86cc6c63e5..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file From 5c2d4467670c21a6e5b8c10b504bc9c3420366cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Jun 2024 10:54:40 +0200 Subject: [PATCH 066/130] Rewrite `ControlPointTable` to use virtualised list --- osu.Game/Screens/Edit/EditorTable.cs | 15 +- osu.Game/Screens/Edit/TableHeaderText.cs | 19 ++ .../Screens/Edit/Timing/ControlPointList.cs | 38 +-- .../Screens/Edit/Timing/ControlPointTable.cs | 296 +++++++++++++----- .../RowAttributes/AttributeProgressBar.cs | 1 + 5 files changed, 233 insertions(+), 136 deletions(-) create mode 100644 osu.Game/Screens/Edit/TableHeaderText.cs diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index 4d8393e829..cec9dd2a7d 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -4,15 +4,11 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK.Graphics; @@ -89,16 +85,7 @@ namespace osu.Game.Screens.Edit return BackgroundFlow[index].Item; } - protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default); - - private partial class HeaderText : OsuSpriteText - { - public HeaderText(LocalisableString text) - { - Text = text.ToUpper(); - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); - } - } + protected override Drawable CreateHeader(int index, TableColumn? column) => new TableHeaderText(column?.Header ?? default); public partial class RowBackground : OsuClickableContainer { diff --git a/osu.Game/Screens/Edit/TableHeaderText.cs b/osu.Game/Screens/Edit/TableHeaderText.cs new file mode 100644 index 0000000000..61301f86ed --- /dev/null +++ b/osu.Game/Screens/Edit/TableHeaderText.cs @@ -0,0 +1,19 @@ +// 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.LocalisationExtensions; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Edit +{ + public partial class TableHeaderText : OsuSpriteText + { + public TableHeaderText(LocalisableString text) + { + Text = text.ToUpper(); + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 4e4090ccd0..a0d833c908 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -7,10 +7,8 @@ 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.Input.Events; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; @@ -21,12 +19,8 @@ namespace osu.Game.Screens.Edit.Timing public partial class ControlPointList : CompositeDrawable { private OsuButton deleteButton = null!; - private ControlPointTable table = null!; - private OsuScrollContainer scroll = null!; private RoundedButton addButton = null!; - private readonly IBindableList controlPointGroups = new BindableList(); - [Resolved] private EditorClock clock { get; set; } = null!; @@ -36,9 +30,6 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; - [Resolved] - private IEditorChangeHandler? changeHandler { get; set; } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { @@ -47,21 +38,10 @@ namespace osu.Game.Screens.Edit.Timing const float margins = 10; InternalChildren = new Drawable[] { - new Box - { - Colour = colours.Background4, - RelativeSizeAxes = Axes.Both, - }, - new Box - { - Colour = colours.Background3, - RelativeSizeAxes = Axes.Y, - Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins, - }, - scroll = new OsuScrollContainer + new ControlPointTable { RelativeSizeAxes = Axes.Both, - Child = table = new ControlPointTable(), + Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, new FillFlowContainer { @@ -106,19 +86,7 @@ namespace osu.Game.Screens.Edit.Timing : "+ Add at current time"; }, true); - controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, _) => - { - // This callback can happen many times in a change operation. It gets expensive. - // We really should be handling the `CollectionChanged` event properly. - Scheduler.AddOnce(() => - { - table.ControlGroups = controlPointGroups; - changeHandler?.SaveState(); - }); - }, true); - - table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); + //table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 219575a380..3bb801f471 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -2,149 +2,270 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Screens.Edit.Timing.RowAttributes; using osuTK; namespace osu.Game.Screens.Edit.Timing { - public partial class ControlPointTable : EditorTable + public partial class ControlPointTable : CompositeDrawable { - [Resolved] - private Bindable selectedGroup { get; set; } = null!; + public BindableList Groups { get; } = new BindableList(); - [Resolved] - private EditorClock clock { get; set; } = null!; + private const float timing_column_width = 300; + private const float row_height = 25; + private const float row_horizontal_padding = 20; - public const float TIMING_COLUMN_WIDTH = 300; - - public IEnumerable ControlGroups + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) { - set + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] { - int selectedIndex = GetIndexForObject(selectedGroup.Value); - - Content = null; - BackgroundFlow.Clear(); - - if (!value.Any()) - return; - - foreach (var group in value) + new Box { - BackgroundFlow.Add(new RowBackground(group) + Colour = colours.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Colour = colours.Background3, + RelativeSizeAxes = Axes.Y, + Width = timing_column_width + 10, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = row_height, + Padding = new MarginPadding { Horizontal = row_horizontal_padding }, + Children = new Drawable[] { - // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. - Action = () => Schedule(() => + new TableHeaderText("Time") { - SetSelectedRow(group); - clock.SeekSmoothlyTo(group.Time); - }) - }); - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TableHeaderText("Attributes") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = ControlPointTable.timing_column_width } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = row_height }, + Child = new ControlPointRowList + { + RelativeSizeAxes = Axes.Both, + RowData = { BindTarget = Groups, }, + }, + }, + }; + } - Columns = createHeaders(); - Content = value.Select(createContent).ToArray().ToRectangular(); + private partial class ControlPointRowList : VirtualisedListContainer + { + [Resolved] + private Bindable selectedGroup { get; set; } = null!; - // Attempt to retain selection. - if (SetSelectedRow(selectedGroup.Value)) - return; + public ControlPointRowList() + : base(row_height, 50) + { + } - // Some operations completely obliterate references, so best-effort reselect based on index. - if (SetSelectedRow(GetObjectAtIndex(selectedIndex))) - return; + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); - // Selection could not be retained. - selectedGroup.Value = null; + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(val => + { + var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); + if (row != null) + Scroll.ScrollIntoView(row); + }); } } - protected override void LoadComplete() + public partial class DrawableControlGroup : PoolableDrawable, IHasCurrentValue { - base.LoadComplete(); - - // Handle external selections. - selectedGroup.BindValueChanged(g => SetSelectedRow(g.NewValue), true); - } - - protected override bool SetSelectedRow(object? item) - { - if (!base.SetSelectedRow(item)) - return false; - - selectedGroup.Value = item as ControlPointGroup; - return true; - } - - private TableColumn[] createHeaders() - { - var columns = new List + public Bindable Current { - new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, TIMING_COLUMN_WIDTH)), - new TableColumn("Attributes", Anchor.CentreLeft), - }; + get => current.Current; + set => current.Current = value; + } - return columns.ToArray(); - } + private readonly BindableWithCurrent current = new BindableWithCurrent(); - private Drawable[] createContent(ControlPointGroup group) - { - return new Drawable[] + private Box background = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private Bindable selectedGroup { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() { - new ControlGroupTiming(group), - new ControlGroupAttributes(group, c => c is not TimingControlPoint) - }; + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background1, + Alpha = 0, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = row_horizontal_padding, }, + Children = new Drawable[] + { + new ControlGroupTiming { Group = { BindTarget = current }, }, + new ControlGroupAttributes(point => point is not TimingControlPoint) + { + Group = { BindTarget = current }, + Margin = new MarginPadding { Left = timing_column_width } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. + var currentGroup = Current.Value; + Schedule(() => + { + selectedGroup.Value = currentGroup; + editorClock.SeekSmoothlyTo(currentGroup.Time); + }); + return true; + } + + private void updateState() + { + bool isSelected = selectedGroup.Value?.Equals(current.Value) == true; + + if (IsHovered || isSelected) + background.FadeIn(100, Easing.OutQuint); + else + background.FadeOut(100, Easing.OutQuint); + + background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; + } } private partial class ControlGroupTiming : FillFlowContainer { - public ControlGroupTiming(ControlPointGroup group) + public Bindable Group { get; } = new Bindable(); + + private OsuSpriteText timeText = null!; + + [BackgroundDependencyLoader] + private void load() { Name = @"ControlGroupTiming"; RelativeSizeAxes = Axes.Y; - Width = TIMING_COLUMN_WIDTH; + Width = timing_column_width; Spacing = new Vector2(5); Children = new Drawable[] { - new OsuSpriteText + timeText = new OsuSpriteText { - Text = group.Time.ToEditorFormattedString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Width = 70, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new ControlGroupAttributes(group, c => c is TimingControlPoint) + new ControlGroupAttributes(c => c is TimingControlPoint) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Group = { BindTarget = Group }, } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Group.BindValueChanged(_ => timeText.Text = Group.Value?.Time.ToEditorFormattedString() ?? default(LocalisableString), true); + } } private partial class ControlGroupAttributes : CompositeDrawable { + public Bindable Group { get; } = new Bindable(); + private BindableList controlPoints { get; } = new BindableList(); + private readonly Func matchFunction; - private readonly IBindableList controlPoints = new BindableList(); + private FillFlowContainer fill = null!; - private readonly FillFlowContainer fill; - - public ControlGroupAttributes(ControlPointGroup group, Func matchFunction) + public ControlGroupAttributes(Func matchFunction) { this.matchFunction = matchFunction; + } + [BackgroundDependencyLoader] + private void load() + { AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Name = @"ControlGroupAttributes"; @@ -156,20 +277,21 @@ namespace osu.Game.Screens.Edit.Timing Direction = FillDirection.Horizontal, Spacing = new Vector2(2) }; - - controlPoints.BindTo(group.ControlPoints); - } - - [BackgroundDependencyLoader] - private void load() - { - createChildren(); } protected override void LoadComplete() { base.LoadComplete(); - controlPoints.CollectionChanged += (_, _) => createChildren(); + + Group.BindValueChanged(_ => + { + controlPoints.UnbindBindings(); + controlPoints.Clear(); + if (Group.Value != null) + ((IBindableList)controlPoints).BindTo(Group.Value.ControlPoints); + }, true); + + controlPoints.BindCollectionChanged((_, _) => createChildren(), true); } private void createChildren() diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs index 4cae774078..0a89f196fa 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes BackgroundColour = overlayColours.Background6; FillColour = controlPoint.GetRepresentingColour(colours); + FinishTransforms(true); } } } From b12db8fbe28d96c8f965f7a1d3fb067e287ce1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jun 2024 14:44:02 +0200 Subject: [PATCH 067/130] Rewrite `IssueTable` to use virtualised list --- osu.Game/Screens/Edit/Verify/IssueList.cs | 11 +- osu.Game/Screens/Edit/Verify/IssueTable.cs | 309 ++++++++++++++------- 2 files changed, 213 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index d07190fca0..de7b760bcd 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -56,10 +55,9 @@ namespace osu.Game.Screens.Edit.Verify Colour = colours.Background3, RelativeSizeAxes = Axes.Both, }, - new OsuScrollContainer + table = new IssueTable { RelativeSizeAxes = Axes.Both, - Child = table = new IssueTable(), }, new FillFlowContainer { @@ -101,9 +99,10 @@ namespace osu.Game.Screens.Edit.Verify issues = filter(issues); - table.Issues = issues - .OrderBy(issue => issue.Template.Type) - .ThenBy(issue => issue.Check.Metadata.Category); + table.Issues.Clear(); + table.Issues.AddRange(issues + .OrderBy(issue => issue.Template.Type) + .ThenBy(issue => issue.Check.Metadata.Category)); } private IEnumerable filter(IEnumerable issues) diff --git a/osu.Game/Screens/Edit/Verify/IssueTable.cs b/osu.Game/Screens/Edit/Verify/IssueTable.cs index 8fb30fb726..fbe789d452 100644 --- a/osu.Game/Screens/Edit/Verify/IssueTable.cs +++ b/osu.Game/Screens/Edit/Verify/IssueTable.cs @@ -1,132 +1,239 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Screens.Edit.Verify { - public partial class IssueTable : EditorTable + public partial class IssueTable : CompositeDrawable { - private Bindable selectedIssue = null!; + public BindableList Issues { get; } = new BindableList(); - [Resolved] - private VerifyScreen verify { get; set; } = null!; + public const float COLUMN_WIDTH = 70; + public const float COLUMN_GAP = 10; + public const float ROW_HEIGHT = 25; + public const float ROW_HORIZONTAL_PADDING = 20; + public const int TEXT_SIZE = 14; - [Resolved] - private EditorClock clock { get; set; } = null!; - - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; - - [Resolved] - private Editor editor { get; set; } = null!; - - public IEnumerable Issues + [BackgroundDependencyLoader] + private void load() { - set + InternalChildren = new Drawable[] { - Content = null; - BackgroundFlow.Clear(); - - if (!value.Any()) - return; - - foreach (var issue in value) + new Container { - BackgroundFlow.Add(new RowBackground(issue) + RelativeSizeAxes = Axes.X, + Height = ROW_HEIGHT, + Padding = new MarginPadding { Horizontal = ROW_HORIZONTAL_PADDING, }, + Children = new[] { - Action = () => + new TableHeaderText("Type") { - selectedIssue.Value = issue; - - if (issue.Time != null) - { - clock.Seek(issue.Time.Value); - editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode)); - } - - if (!issue.HitObjects.Any()) - return; - - editorBeatmap.SelectedHitObjects.Clear(); - editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects); + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - }); + new TableHeaderText("Time") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP }, + }, + new TableHeaderText("Message") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 2 * (COLUMN_WIDTH + COLUMN_GAP) }, + }, + new TableHeaderText("Category") + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = ROW_HEIGHT, }, + Child = new IssueRowList + { + RelativeSizeAxes = Axes.Both, + RowData = { BindTarget = Issues } + } + } + }; + } + + private partial class IssueRowList : VirtualisedListContainer + { + public IssueRowList() + : base(ROW_HEIGHT, 50) + { + } + + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + } + + public partial class DrawableIssue : PoolableDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private readonly Bindable selectedIssue = new Bindable(); + + private Box background = null!; + private OsuSpriteText issueTypeText = null!; + private OsuSpriteText issueTimestampText = null!; + private OsuSpriteText issueDetailText = null!; + private OsuSpriteText issueCategoryText = null!; + + [Resolved] + private EditorClock clock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private Editor editor { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + [BackgroundDependencyLoader] + private void load(VerifyScreen verify) + { + RelativeSizeAxes = Axes.X; + Height = ROW_HEIGHT; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 20, }, + Children = new Drawable[] + { + issueTypeText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + }, + issueTimestampText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = 2 * (COLUMN_GAP + COLUMN_WIDTH), + Right = COLUMN_GAP + COLUMN_WIDTH, + }, + Child = issueDetailText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium) + }, + }, + issueCategoryText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + } + } + } + }; + + selectedIssue.BindTo(verify.SelectedIssue); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedIssue.BindValueChanged(_ => updateState()); + Current.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + selectedIssue.Value = current.Value; + + if (current.Value.Time != null) + { + clock.Seek(current.Value.Time.Value); + editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode)); } - Columns = createHeaders(); - Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular(); + if (current.Value.HitObjects.Any()) + { + editorBeatmap.SelectedHitObjects.Clear(); + editorBeatmap.SelectedHitObjects.AddRange(current.Value.HitObjects); + } + + return true; + } + + private void updateState() + { + issueTypeText.Text = Current.Value.Template.Type.ToString(); + issueTypeText.Colour = Current.Value.Template.Colour; + issueTimestampText.Text = Current.Value.GetEditorTimestamp(); + issueDetailText.Text = Current.Value.ToString(); + issueCategoryText.Text = Current.Value.Check.Metadata.Category.ToString(); + + bool isSelected = selectedIssue.Value == current.Value; + + if (IsHovered || isSelected) + background.FadeIn(100, Easing.OutQuint); + else + background.FadeOut(100, Easing.OutQuint); + + background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; } } - - protected override void LoadComplete() - { - base.LoadComplete(); - - selectedIssue = verify.SelectedIssue.GetBoundCopy(); - selectedIssue.BindValueChanged(issue => - { - SetSelectedRow(issue.NewValue); - }, true); - } - - private TableColumn[] createHeaders() - { - var columns = new List - { - new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)), - new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)), - new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)), - new TableColumn("Message", Anchor.CentreLeft), - new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)), - }; - - return columns.ToArray(); - } - - private Drawable[] createContent(int index, Issue issue) => new Drawable[] - { - new OsuSpriteText - { - Text = $"#{index + 1}", - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium), - Margin = new MarginPadding { Right = 10 } - }, - new OsuSpriteText - { - Text = issue.Template.Type.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding { Right = 10 }, - Colour = issue.Template.Colour - }, - new OsuSpriteText - { - Text = issue.GetEditorTimestamp(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding { Right = 10 }, - }, - new OsuSpriteText - { - Text = issue.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium) - }, - new OsuSpriteText - { - Text = issue.Check.Metadata.Category.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding(10) - } - }; } } From 9dfd6cf9ef17c3660c5d5d7f30006405ad31ad9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 09:55:07 +0200 Subject: [PATCH 068/130] Remove `EditorTable` Begone, foul beast. --- osu.Game/Screens/Edit/EditorTable.cs | 176 --------------------------- 1 file changed, 176 deletions(-) delete mode 100644 osu.Game/Screens/Edit/EditorTable.cs diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs deleted file mode 100644 index cec9dd2a7d..0000000000 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Overlays; -using osuTK.Graphics; - -namespace osu.Game.Screens.Edit -{ - public abstract partial class EditorTable : TableContainer - { - public event Action? OnRowSelected; - - private const float horizontal_inset = 20; - - protected const float ROW_HEIGHT = 25; - - public const int TEXT_SIZE = 14; - - protected readonly FillFlowContainer BackgroundFlow; - - // We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow - // and no items in the underlying table are clickable. - protected override bool ShouldBeConsideredForInput(Drawable child) => child == BackgroundFlow && base.ShouldBeConsideredForInput(child); - - protected EditorTable() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Padding = new MarginPadding { Horizontal = horizontal_inset }; - RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT); - - AddInternal(BackgroundFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Depth = 1f, - Padding = new MarginPadding { Horizontal = -horizontal_inset }, - Margin = new MarginPadding { Top = ROW_HEIGHT } - }); - } - - protected int GetIndexForObject(object? item) - { - for (int i = 0; i < BackgroundFlow.Count; i++) - { - if (BackgroundFlow[i].Item == item) - return i; - } - - return -1; - } - - protected virtual bool SetSelectedRow(object? item) - { - bool foundSelection = false; - - foreach (var b in BackgroundFlow) - { - b.Selected = ReferenceEquals(b.Item, item); - - if (b.Selected) - { - Debug.Assert(!foundSelection); - OnRowSelected?.Invoke(b); - foundSelection = true; - } - } - - return foundSelection; - } - - protected object? GetObjectAtIndex(int index) - { - if (index < 0 || index > BackgroundFlow.Count - 1) - return null; - - return BackgroundFlow[index].Item; - } - - protected override Drawable CreateHeader(int index, TableColumn? column) => new TableHeaderText(column?.Header ?? default); - - public partial class RowBackground : OsuClickableContainer - { - public readonly object Item; - - private const int fade_duration = 100; - - private readonly Box hoveredBackground; - - public RowBackground(object item) - { - Item = item; - - RelativeSizeAxes = Axes.X; - Height = 25; - - AlwaysPresent = true; - - CornerRadius = 3; - Masking = true; - - Children = new Drawable[] - { - hoveredBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - }; - } - - private Color4 colourHover; - private Color4 colourSelected; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) - { - colourHover = colours.Background1; - colourSelected = colours.Colour3; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateState(); - FinishTransforms(true); - } - - private bool selected; - - public bool Selected - { - get => selected; - set - { - if (value == selected) - return; - - selected = value; - updateState(); - } - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint); - - if (selected || IsHovered) - hoveredBackground.FadeIn(fade_duration, Easing.OutQuint); - else - hoveredBackground.FadeOut(fade_duration, Easing.OutQuint); - } - } - } -} From 6beae91d53330ed58cb5dc2d0cc3fe5fc955a19a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:10:29 +0900 Subject: [PATCH 069/130] Ensure carousel panel depth is consistent based on vertical position I thought this was already being handled, but it turns out that changing sort mode (and potentially other operations) could break the depth of display of panels due to pooling and what not. This ensures consistency and also employs @bdach's suggestion of reversing the depth above and below the current selection for a better visual effect. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3aa980cec0..4f2325adbf 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -895,7 +895,6 @@ namespace osu.Game.Screens.Select { var panel = setPool.Get(p => p.Item = item); - panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; Scroll.Add(panel); @@ -915,6 +914,8 @@ namespace osu.Game.Screens.Select { bool isSelected = item.Item.State.Value == CarouselItemState.Selected; + bool hasPassedSelection = item.Item.CarouselYPosition < selectedBeatmapSet?.CarouselYPosition; + // Cheap way of doing animations when entering / exiting song select. const double half_time = 50; const float panel_x_offset_when_inactive = 200; @@ -929,6 +930,8 @@ namespace osu.Game.Screens.Select item.Alpha = (float)Interpolation.DampContinuously(item.Alpha, 0, half_time, Clock.ElapsedFrameTime); item.X = (float)Interpolation.DampContinuously(item.X, panel_x_offset_when_inactive, half_time, Clock.ElapsedFrameTime); } + + Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } if (item is DrawableCarouselBeatmapSet set) From fd91210c1c1fb4b5a34dc6ae3acd86bd27b115f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:47:33 +0900 Subject: [PATCH 070/130] Remove unnecessary setter on bindable --- .../Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 2fd5253347..5f58316b25 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public Action? Clicked { get; set; } - public BindableBool Active { get; set; } = new BindableBool(); + public BindableBool Active { get; } = new BindableBool(); private double progress; From 2a839b3697259ed90691affb263ad36a74bad0b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:50:06 +0900 Subject: [PATCH 071/130] Make action `required init` --- .../OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 5f58316b25..a9f9a5cd78 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -109,9 +109,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge lastDisplayed = currentDisplay; } - private void onManualNavigation(NavigationDot obj) + private void onManualNavigation(NavigationDot dot) { - int index = navigationFlow.IndexOf(obj); + int index = navigationFlow.IndexOf(dot); if (index < 0) return; @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private partial class NavigationDot : CompositeDrawable { - public Action? Clicked { get; set; } + public required Action Clicked { get; init; } public BindableBool Active { get; } = new BindableBool(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override bool OnClick(ClickEvent e) { - Clicked?.Invoke(this); + Clicked(this); hoverLayer.FadeTo(1) .Then().FadeTo(IsHovered ? 0.2f : 0, 250, Easing.OutQuint); From 936a8d800d235e0576d81d29289a536a7744507a Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 26 Jun 2024 21:39:14 +0800 Subject: [PATCH 072/130] Swap the low and high multiplier color --- osu.Game/Screens/Select/FooterButtonMods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 5685910c0a..a15d315f1b 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -59,8 +59,8 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - lowMultiplierColour = colours.Red; - highMultiplierColour = colours.Green; + lowMultiplierColour = colours.Green; + highMultiplierColour = colours.Red; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; 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 073/130] 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 074/130] 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 1998742e425ef68886911646aa1b054a7c6bcbb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 22:56:43 +0900 Subject: [PATCH 075/130] 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 0f1a14afd8..e2b7fe479a 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 6ed60b00b3..ddde2a3cec 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From b339d6a00cd3068d3c28a104004897c459f8ad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 16:26:32 +0200 Subject: [PATCH 076/130] Fix editor performance regression with hitmarkers active --- .../Objects/Drawables/DrawableHitCircle.cs | 19 ++++++++++--------- .../Objects/Drawables/DrawableSlider.cs | 4 ++-- .../Objects/Drawables/DrawableSliderTail.cs | 17 +++++++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 7d707dea6c..101c34b725 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -325,19 +326,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); UpdateComboColour(); - // This method is called every frame. If we need to, the following can likely be converted - // to code which doesn't use transforms at all. + // This method is called every frame in editor contexts, thus the lack of need for transforms. - // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + if (Time.Current >= HitStateUpdateTime) + { + // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + AccentColour.Value = Color4.White; + Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + } - using (BeginAbsoluteSequence(StateUpdateTime - 5)) - this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); - - using (BeginAbsoluteSequence(HitStateUpdateTime)) - this.FadeOut(700).Expire(); + LifetimeEnd = HitStateUpdateTime + 700; } internal void RestoreHitAnimations() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 02d0ebee83..eacd2b3e75 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -375,14 +375,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); HeadCircle.SuppressHitAnimations(); TailCircle.SuppressHitAnimations(); } internal void RestoreHitAnimations() { - UpdateState(ArmedState.Hit, force: true); + UpdateState(ArmedState.Hit); HeadCircle.RestoreHitAnimations(); TailCircle.RestoreHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 42abf41d6f..8bb1b0aebc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,12 +3,12 @@ #nullable disable -using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; @@ -132,14 +132,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); UpdateComboColour(); - using (BeginAbsoluteSequence(StateUpdateTime - 5)) - this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + // This method is called every frame in editor contexts, thus the lack of need for transforms. - using (BeginAbsoluteSequence(HitStateUpdateTime)) - this.FadeOut(700).Expire(); + if (Time.Current >= HitStateUpdateTime) + { + // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + AccentColour.Value = Color4.White; + Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + } + + LifetimeEnd = HitStateUpdateTime + 700; } internal void RestoreHitAnimations() 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 077/130] 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 bbacfc8d23622d87114afb2cdd982235bd6f3b87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 12:10:10 +0900 Subject: [PATCH 078/130] Add failing test coverage of osu!mania automated break creation scenarios --- .../TestSceneEditorBeatmapProcessor.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 50f37e2070..3ec61cbf80 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -5,6 +5,8 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -74,6 +76,50 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks, Is.Empty); } + [Test] + public void TestHoldNote() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, Duration = 10000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Has.Count.EqualTo(0)); + } + + [Test] + public void TestHoldNoteWithOverlappingNote() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, Duration = 10000 }, + new Note { StartTime = 2000 }, + new Note { StartTime = 12000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Has.Count.EqualTo(0)); + } + [Test] public void TestTwoObjectsFarApart() { From 7ef7e5f1638dec9d798dd869ce429142bde27876 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 12:10:26 +0900 Subject: [PATCH 079/130] Fix break generation not accounting for concurrent hitobjects correctly --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index bcbee78280..c3cb79c217 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -48,15 +48,20 @@ namespace osu.Game.Screens.Edit } } + double currentMaxEndTime = double.MinValue; + for (int i = 1; i < Beatmap.HitObjects.Count; ++i) { - double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime(); + // Keep track of the maximum end time encountered thus far. + // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. + currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); + double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; - if (nextObjectStartTime - previousObjectEndTime < BreakPeriod.MIN_GAP_DURATION) + if (nextObjectStartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) continue; - double breakStartTime = previousObjectEndTime + BreakPeriod.GAP_BEFORE_BREAK; + double breakStartTime = currentMaxEndTime + BreakPeriod.GAP_BEFORE_BREAK; double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) From e4335a543eba89a4ceb75c854257acab40c341fd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jun 2024 06:41:39 +0300 Subject: [PATCH 080/130] Add failing test case Includes a refactor of `ThemeComparisonTestScene` to allow accessing a manual input manager. --- .../Visual/Settings/TestSceneFileSelector.cs | 2 +- .../UserInterface/TestSceneOsuDropdown.cs | 69 ++++++++++--------- .../UserInterface/TestSceneRoundedButton.cs | 4 +- .../UserInterface/ThemeComparisonTestScene.cs | 41 +++++++---- 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index e8f74a2f1b..c70277987e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Settings { AddStep("create", () => { - Cell(0, 0).Children = new Drawable[] + ContentContainer.Children = new Drawable[] { new Box { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs index 63f7a2f2cc..2f855c8744 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -6,23 +6,51 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Input.States; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneOsuDropdown : ThemeComparisonTestScene { - protected override Drawable CreateContent() => - new OsuEnumDropdown - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Width = 150 - }; + protected override Drawable CreateContent() => new OsuEnumDropdown + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Width = 150 + }; + + [Test] + public void TestBackAction() + { + AddStep("open", () => dropdownMenu.Open()); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("closed", () => dropdownMenu.State == MenuState.Closed); + + AddStep("open", () => dropdownMenu.Open()); + AddStep("type something", () => dropdownSearchBar.SearchTerm.Value = "something"); + AddAssert("search bar visible", () => dropdownSearchBar.State.Value == Visibility.Visible); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("text clear", () => dropdownSearchBar.SearchTerm.Value == string.Empty); + AddAssert("search bar hidden", () => dropdownSearchBar.State.Value == Visibility.Hidden); + AddAssert("still open", () => dropdownMenu.State == MenuState.Open); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("closed", () => dropdownMenu.State == MenuState.Closed); + } + + [Test] + public void TestSelectAction() + { + AddStep("open", () => dropdownMenu.Open()); + AddStep("press down", () => InputManager.Key(Key.Down)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("second selected", () => dropdown.Current.Value == TestEnum.ReallyLongOption); + } + + private OsuEnumDropdown dropdown => this.ChildrenOfType>().Last(); + private Menu dropdownMenu => dropdown.ChildrenOfType().Single(); + private DropdownSearchBar dropdownSearchBar => dropdown.ChildrenOfType().Single(); private enum TestEnum { @@ -32,26 +60,5 @@ namespace osu.Game.Tests.Visual.UserInterface [System.ComponentModel.Description("Really lonnnnnnng option")] ReallyLongOption, } - - [Test] - // todo: this can be written much better if ThemeComparisonTestScene has a manual input manager - public void TestBackAction() - { - AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); - - AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); - AddStep("type something", () => dropdown().ChildrenOfType().Single().SearchTerm.Value = "something"); - AddAssert("search bar visible", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Visible); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("text clear", () => dropdown().ChildrenOfType().Single().SearchTerm.Value == string.Empty); - AddAssert("search bar hidden", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Hidden); - AddAssert("still open", () => dropdown().ChildrenOfType().Single().State == MenuState.Open); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); - - OsuEnumDropdown dropdown() => this.ChildrenOfType>().First(); - } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs index 8c2651f71d..2d5c2c6d57 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs @@ -53,8 +53,8 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestBackgroundColour() { AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red)); - AddAssert("rounded button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); - AddAssert("settings button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); + AddAssert("rounded button has correct colour", () => ContentContainer.ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); + AddAssert("settings button has correct colour", () => ContentContainer.ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 3177695f44..4700ef72d9 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -6,18 +6,21 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { - public abstract partial class ThemeComparisonTestScene : OsuGridTestScene + public abstract partial class ThemeComparisonTestScene : OsuManualInputManagerTestScene { private readonly bool showWithoutColourProvider; + public Container ContentContainer { get; private set; } = null!; + protected ThemeComparisonTestScene(bool showWithoutColourProvider = true) - : base(1, showWithoutColourProvider ? 2 : 1) { this.showWithoutColourProvider = showWithoutColourProvider; } @@ -25,16 +28,32 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { + Child = ContentContainer = new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + }; + if (showWithoutColourProvider) { - Cell(0, 0).AddRange(new[] + ContentContainer.Size = new Vector2(0.5f, 1f); + + Add(new Container { - new Box + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1f), + Children = new[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam - }, - CreateContent() + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoam + }, + CreateContent() + } }); } } @@ -43,10 +62,8 @@ namespace osu.Game.Tests.Visual.UserInterface { var colourProvider = new OverlayColourProvider(colourScheme); - int col = showWithoutColourProvider ? 1 : 0; - - Cell(0, col).Clear(); - Cell(0, col).Add(new DependencyProvidingContainer + ContentContainer.Clear(); + ContentContainer.Add(new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] From 811621325f2746df0947e673381351377ee4bfe7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jun 2024 07:09:46 +0300 Subject: [PATCH 081/130] Fix osu! dropdown search text box having commits disabled I've also removed inheritance from `SearchTextBox` because it contains logic that might interfere with the internal implementation of dropdown search bars (focus logic and stuff). --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 38e90bf4ea..c8bb45b59d 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -418,16 +418,19 @@ namespace osu.Game.Graphics.UserInterface FontSize = OsuFont.Default.Size, }; - private partial class DropdownSearchTextBox : SearchTextBox + private partial class DropdownSearchTextBox : OsuTextBox { - public override bool OnPressed(KeyBindingPressEvent e) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) { - if (e.Action == GlobalAction.Back) - // this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager. - // to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here. - return false; + BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + } - return base.OnPressed(e); + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + BorderThickness = 0; } } } From 981340debec29090e7c172479ccffe17b7cf2983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:45:14 +0200 Subject: [PATCH 082/130] Add safety test coverage for removal of breaks at end of beatmap --- .../TestSceneEditorBeatmapProcessor.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 3ec61cbf80..1a3f0aa3df 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -395,6 +395,32 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks, Is.Empty); } + [Test] + public void TestManualBreaksAtEndOfBeatmapAreRemovedCorrectlyEvenWithConcurrentObjects() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, EndTime = 20000 }, + new HoldNote { StartTime = 2000, EndTime = 3000 }, + }, + Breaks = + { + new ManualBreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + [Test] public void TestBreaksAtStartOfBeatmapAreRemoved() { From ef952bcd65d527ac792c107a55b2cdcb0b904969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:48:05 +0200 Subject: [PATCH 083/130] Use `GetLastObjectTime()` for safety Due to other circumstances this has no real effect, but may as well. --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index c3cb79c217..87e5d92b25 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit foreach (var manualBreak in Beatmap.Breaks.ToList()) { if (manualBreak.EndTime <= Beatmap.HitObjects.FirstOrDefault()?.StartTime - || manualBreak.StartTime >= Beatmap.HitObjects.LastOrDefault()?.GetEndTime() + || manualBreak.StartTime >= Beatmap.GetLastObjectTime() || Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) { Beatmap.Breaks.Remove(manualBreak); From b1baa49459b388f95652e685dcf55e157f29d851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:56:57 +0200 Subject: [PATCH 084/130] Add note about implicit reliance on sort by start time --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 87e5d92b25..99c8c3572b 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.Edit { // Keep track of the maximum end time encountered thus far. // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. + // Note that we're relying on the implicit assumption that objects are sorted by start time, + // which is why similar tracking is not done for start time. currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; From 779d2e81723425353b5ce0ffe598d7becdea4c8b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Jun 2024 16:00:22 +0900 Subject: [PATCH 085/130] Support increased visibility for first object with traceable mod --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 75ad00e169..9091837034 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -6,7 +6,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; @@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { } From a5aedded167f53f55ecf195521b6703e7302484b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 09:44:55 +0200 Subject: [PATCH 086/130] Remove commented code --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index a0d833c908..b7367dddda 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -85,8 +85,6 @@ namespace osu.Game.Screens.Edit.Timing ? "+ Clone to current time" : "+ Add at current time"; }, true); - - //table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); } protected override bool OnClick(ClickEvent e) From 9384cbcdd80aa5a7126be2aadf480f065f20cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 09:46:35 +0200 Subject: [PATCH 087/130] Fix scroll-into-view on control point table not working as it is supposed to --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 3bb801f471..75b86650af 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -100,9 +100,20 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(val => { + // can't use `.ScrollIntoView()` here because of the list virtualisation not giving + // child items valid coordinates from the start, so ballpark something similar + // using estimated row height. var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); - if (row != null) - Scroll.ScrollIntoView(row); + if (row == null) + return; + + float minPos = Items.GetLayoutPosition(row) * row_height; + float maxPos = minPos + row_height; + + if (minPos < Scroll.Current) + Scroll.ScrollTo(minPos); + else if (maxPos > Scroll.Current + Scroll.DisplayableContent) + Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); }); } } From 9e07c8fff7dc921673689dfcc085b7ce49361afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 10:31:24 +0200 Subject: [PATCH 088/130] Update framework again --- 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 e2b7fe479a..349829555d 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 ddde2a3cec..8e8bac53b9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From d6e7781be13821b52baafd1a06cb003344b43341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 10:44:28 +0200 Subject: [PATCH 089/130] Add client/server models for allowing clients to receive realtime playlist updates --- osu.Game/Online/Metadata/IMetadataClient.cs | 6 +++ osu.Game/Online/Metadata/IMetadataServer.cs | 10 ++++ osu.Game/Online/Metadata/MetadataClient.cs | 18 +++++++ .../Metadata/MultiplayerPlaylistItemStats.cs | 29 +++++++++++ .../Metadata/MultiplayerRoomScoreSetEvent.cs | 50 +++++++++++++++++++ .../Online/Metadata/OnlineMetadataClient.cs | 19 +++++++ .../DailyChallengeScoreBreakdown.cs | 3 +- .../Visual/Metadata/TestMetadataClient.cs | 5 ++ 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs create mode 100644 osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index ee7a726bfc..97c1bbde5f 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -26,5 +26,11 @@ namespace osu.Game.Online.Metadata /// Null value means there is no "daily challenge" currently active. /// Task DailyChallengeUpdated(DailyChallengeInfo? info); + + /// + /// Delivers information that a multiplayer score was set in a watched room. + /// To receive these, the client must call for a given room first. + /// + Task MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent); } } diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs index 8bf3f8f56b..79ed8b5634 100644 --- a/osu.Game/Online/Metadata/IMetadataServer.cs +++ b/osu.Game/Online/Metadata/IMetadataServer.cs @@ -43,5 +43,15 @@ namespace osu.Game.Online.Metadata /// Signals to the server that the current user would like to stop receiving updates on other users' online presence. /// Task EndWatchingUserPresence(); + + /// + /// Signals to the server that the current user would like to begin receiving updates about the state of the multiplayer room with the given . + /// + Task BeginWatchingMultiplayerRoom(long id); + + /// + /// Signals to the server that the current user would like to stop receiving updates about the state of the multiplayer room with the given . + /// + Task EndWatchingMultiplayerRoom(long id); } } diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index b619970494..8a5fe1733e 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -68,6 +68,24 @@ namespace osu.Game.Online.Metadata #endregion + #region Multiplayer room watching + + public abstract Task BeginWatchingMultiplayerRoom(long id); + + public abstract Task EndWatchingMultiplayerRoom(long id); + + public event Action? MultiplayerRoomScoreSet; + + Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent) + { + if (MultiplayerRoomScoreSet != null) + Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent); + + return Task.CompletedTask; + } + + #endregion + #region Disconnection handling public event Action? Disconnecting; diff --git a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs new file mode 100644 index 0000000000..d13705bf5b --- /dev/null +++ b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs @@ -0,0 +1,29 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Metadata +{ + [MessagePackObject] + [Serializable] + public class MultiplayerPlaylistItemStats + { + public const int TOTAL_SCORE_DISTRIBUTION_BINS = 13; + + /// + /// The ID of the playlist item which these stats pertain to. + /// + [Key(0)] + public long PlaylistItemID { get; set; } + + /// + /// The count of scores with given total ranges in the room. + /// The ranges are bracketed into bins, each of 100,000 score width. + /// The last bin will contain count of all scores with total of 1,200,000 or larger. + /// + [Key(1)] + public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS]; + } +} diff --git a/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs b/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs new file mode 100644 index 0000000000..00bc5dc840 --- /dev/null +++ b/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; + +namespace osu.Game.Online.Metadata +{ + [Serializable] + [MessagePackObject] + public class MultiplayerRoomScoreSetEvent + { + /// + /// The ID of the room in which the score was set. + /// + [Key(0)] + public long RoomID { get; set; } + + /// + /// The ID of the playlist item on which the score was set. + /// + [Key(1)] + public long PlaylistItemID { get; set; } + + /// + /// The ID of the score set. + /// + [Key(2)] + public long ScoreID { get; set; } + + /// + /// The ID of the user who set the score. + /// + [Key(3)] + public int UserID { get; set; } + + /// + /// The total score set by the player. + /// + [Key(4)] + public long TotalScore { get; set; } + + /// + /// If the set score is the user's new best on a playlist item, this member will contain the user's new rank in the room overall. + /// Otherwise, it will contain . + /// + [Key(5)] + public int? NewRank { get; set; } + } +} diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index b94f26a71d..80fcf7571d 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -62,6 +62,7 @@ namespace osu.Game.Online.Metadata connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated); + connection.On(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); }; @@ -240,6 +241,24 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } + public override async Task BeginWatchingMultiplayerRoom(long id) + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + return await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false); + } + + public override async Task EndWatchingMultiplayerRoom(long id) + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom)).ConfigureAwait(false); + } + public override async Task DisconnectRequested() { await base.DisconnectRequested().ConfigureAwait(false); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 3d4f27c44b..d251a10f9a 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Scoring; using osuTK; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private FillFlowContainer barsContainer = null!; - private const int bin_count = 13; + private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS; private long[] bins = new long[bin_count]; [BackgroundDependencyLoader] diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index b589e66d8b..fa64a83352 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -86,5 +86,10 @@ namespace osu.Game.Tests.Visual.Metadata dailyChallengeInfo.Value = info; return Task.CompletedTask; } + + public override Task BeginWatchingMultiplayerRoom(long id) + => Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]); + + public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask; } } From 29412bb29b8e311b3ba825736f21729cc3f3c6e9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 27 Jun 2024 12:22:00 +0200 Subject: [PATCH 090/130] Fix editor setting arbitrary beat divisor --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e91777eab2..22fc6fccdc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -292,7 +292,7 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(changeHandler); } - beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor); beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); updateLastSavedHash(); From 772a68cb3e3e808317dae49cd443dc9b2803d4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 12:38:29 +0200 Subject: [PATCH 091/130] Add test coverage for correct beat divisor save --- .../Visual/Editing/TestSceneEditorSaving.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 64c48e74cf..b487fa3cec 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -193,5 +193,20 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save); } + + [Test] + public void TestBeatDivisor() + { + AddStep("Set custom beat divisor", () => Editor.Dependencies.Get().SetArbitraryDivisor(7)); + + SaveEditor(); + AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash)); + AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7)); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7)); + AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7)); + } } } From 1b741dada30dc3d68db3e85180f4327e596a792d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 14:46:57 +0200 Subject: [PATCH 092/130] Show distance in pixels to previous/next object in osu! hitobject inspector --- .../Edit/OsuHitObjectComposer.cs | 5 +++ .../Edit/OsuHitObjectInspector.cs | 42 +++++++++++++++++++ .../Compose/Components/EditorInspector.cs | 2 +- .../Compose/Components/HitObjectInspector.cs | 15 +++++-- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 41f6b41f82..cf867b4795 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,8 +101,13 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); + RightToolbox.Clear(); RightToolbox.AddRange(new EditorToolboxGroup[] { + new EditorToolboxGroup("inspector") + { + Child = new OsuHitObjectInspector(), + }, OsuGridToolboxGroup, new TransformToolboxGroup { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs new file mode 100644 index 0000000000..27e7d5497c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuHitObjectInspector : HitObjectInspector + { + protected override void AddInspectorValues() + { + base.AddInspectorValues(); + + if (EditorBeatmap.SelectedHitObjects.Count > 0) + { + var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!; + var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!; + + Debug.Assert(firstInSelection != null && lastInSelection != null); + + var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime); + var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime()); + + if (precedingObject != null && precedingObject is not Spinner) + { + AddHeader("To previous"); + AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px"); + } + + if (nextObject != null && nextObject is not Spinner) + { + AddHeader("To next"); + AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px"); + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs index 442454f97a..5837dd7946 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs @@ -10,7 +10,7 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Compose.Components { - internal partial class EditorInspector : CompositeDrawable + public partial class EditorInspector : CompositeDrawable { protected OsuTextFlowContainer InspectorText = null!; diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index ac339dc9d9..2f19888e9e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit.Compose.Components { - internal partial class HitObjectInspector : EditorInspector + public partial class HitObjectInspector : EditorInspector { protected override void LoadComplete() { @@ -29,6 +29,16 @@ namespace osu.Game.Screens.Edit.Compose.Components rollingTextUpdate?.Cancel(); rollingTextUpdate = null; + AddInspectorValues(); + + // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. + // This is a good middle-ground for the time being. + if (EditorBeatmap.SelectedHitObjects.Count > 0) + rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); + } + + protected virtual void AddInspectorValues() + { switch (EditorBeatmap.SelectedHitObjects.Count) { case 0: @@ -90,9 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components AddValue($"{duration.Duration:#,0.##}ms"); } - // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. - // This is a good middle-ground for the time being. - rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); break; default: From b293eb7930c7ae213167ce9b9e2cccd8aec2a2c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 22:19:06 +0900 Subject: [PATCH 093/130] Remove redundant array spec --- 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 cf867b4795..ecebf6d7b1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); RightToolbox.Clear(); - RightToolbox.AddRange(new EditorToolboxGroup[] + RightToolbox.AddRange(new[] { new EditorToolboxGroup("inspector") { From fd6b77ea9295041500bb1316cd3cedd87e32ed38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 23:54:48 +0900 Subject: [PATCH 094/130] Fix distance snap control being removed --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 9 +++------ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ecebf6d7b1..f93874481d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable rectangularGridSnapToggle = new Bindable(); + protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()) @@ -101,13 +103,8 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); - RightToolbox.Clear(); - RightToolbox.AddRange(new[] + RightToolbox.AddRange(new Drawable[] { - new EditorToolboxGroup("inspector") - { - Child = new OsuHitObjectInspector(), - }, OsuGridToolboxGroup, new TransformToolboxGroup { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7d593c03ec..540e0440c6 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Edit { Child = new EditorToolboxGroup("inspector") { - Child = new HitObjectInspector() + Child = CreateHitObjectInspector() }, } } @@ -329,6 +329,8 @@ namespace osu.Game.Rulesets.Edit /// protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); + /// /// Construct a drawable ruleset for the provided ruleset. /// From 28d52789350ffe240ffe23f8df49e7ad3a2a8bf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 13:00:30 +0900 Subject: [PATCH 095/130] Show preset description text in tooltip popup As proposed in https://github.com/ppy/osu/discussions/28610. --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 077bd14751..501b56e2fd 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -6,6 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Mods private const double transition_duration = 200; + private readonly OsuSpriteText descriptionText; + public ModPresetTooltip(OverlayColourProvider colourProvider) { Width = 250; @@ -37,7 +41,15 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(7), - Spacing = new Vector2(7) + Spacing = new Vector2(7), + Children = new[] + { + descriptionText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Colour = colourProvider.Content2, + }, + } } }; } @@ -49,8 +61,12 @@ namespace osu.Game.Overlays.Mods if (ReferenceEquals(preset, lastPreset)) return; + descriptionText.Text = preset.Description; + lastPreset = preset; - Content.ChildrenEnumerable = preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod)); + + Content.RemoveAll(d => d is ModPresetRow, true); + Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod))); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From d370f50cc1234bf8422fb925d1ffb6fde09145ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 13:16:47 +0900 Subject: [PATCH 096/130] Syncrhronise colours across mod and preset tooltips --- .../Mods/IncompatibilityDisplayingModPanel.cs | 5 ++++- .../Mods/IncompatibilityDisplayingTooltip.cs | 10 +++------ osu.Game/Overlays/Mods/ModButtonTooltip.cs | 21 ++++++------------- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 4 ++-- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index 26c5b2ac49..84336319b7 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -16,6 +16,9 @@ namespace osu.Game.Overlays.Mods { private readonly BindableBool incompatible = new BindableBool(); + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Mods #region IHasCustomTooltip - public ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(); + public ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(overlayColourProvider); public Mod TooltipContent => Mod; diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs index 2f82711162..3ac541eaa3 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs @@ -24,13 +24,15 @@ namespace osu.Game.Overlays.Mods [Resolved] private Bindable ruleset { get; set; } = null!; - public IncompatibilityDisplayingTooltip() + public IncompatibilityDisplayingTooltip(OverlayColourProvider colourProvider) + : base(colourProvider) { AddRange(new Drawable[] { incompatibleText = new OsuSpriteText { Margin = new MarginPadding { Top = 5 }, + Colour = colourProvider.Content2, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = "Incompatible with:" }, @@ -43,12 +45,6 @@ namespace osu.Game.Overlays.Mods }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - incompatibleText.Colour = colours.BlueLight; - } - protected override void UpdateDisplay(Mod mod) { base.UpdateDisplay(mod); diff --git a/osu.Game/Overlays/Mods/ModButtonTooltip.cs b/osu.Game/Overlays/Mods/ModButtonTooltip.cs index 52b27f1e00..061c3e3e3a 100644 --- a/osu.Game/Overlays/Mods/ModButtonTooltip.cs +++ b/osu.Game/Overlays/Mods/ModButtonTooltip.cs @@ -1,9 +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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -18,11 +15,10 @@ namespace osu.Game.Overlays.Mods public partial class ModButtonTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText descriptionText; - private readonly Box background; protected override Container Content { get; } - public ModButtonTooltip() + public ModButtonTooltip(OverlayColourProvider colourProvider) { AutoSizeAxes = Axes.Both; Masking = true; @@ -30,9 +26,10 @@ namespace osu.Game.Overlays.Mods InternalChildren = new Drawable[] { - background = new Box + new Box { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, }, Content = new FillFlowContainer { @@ -43,6 +40,7 @@ namespace osu.Game.Overlays.Mods { descriptionText = new OsuSpriteText { + Colour = colourProvider.Content1, Font = OsuFont.GetFont(weight: FontWeight.Regular), }, } @@ -50,17 +48,10 @@ namespace osu.Game.Overlays.Mods }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Gray3; - descriptionText.Colour = colours.BlueLighter; - } - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - private Mod lastMod; + private Mod? lastMod; public void SetContent(Mod mod) { diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 501b56e2fd..ec81aa7ceb 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -40,14 +40,14 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(7), + Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, Spacing = new Vector2(7), Children = new[] { descriptionText = new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.Regular), - Colour = colourProvider.Content2, + Colour = colourProvider.Content1, }, } } From c1bec7a7c374f38adc33b124f93d40ff267657f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 14:55:55 +0900 Subject: [PATCH 097/130] Simplify colour logic for beatmap overlay filter rows --- .../BeatmapSearchGeneralFilterRow.cs | 5 ++-- ...BeatmapSearchMultipleSelectionFilterRow.cs | 10 ++----- .../Overlays/BeatmapListing/FilterTabItem.cs | 29 ++++++++++++++----- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index a4a914db55..2d56c60de6 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -56,8 +56,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } - protected override Color4 GetStateColour() => colours.Orange1; - protected override void LoadComplete() { base.LoadComplete(); @@ -65,6 +63,9 @@ namespace osu.Game.Overlays.BeatmapListing disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce); } + protected override Color4 ColourNormal => colours.Orange1; + protected override Color4 ColourActive => colours.Orange2; + protected override bool OnClick(ClickEvent e) { if (!disclaimerShown.Value && dialogOverlay != null) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 4bd25f6561..9b2e1d57fe 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapListing { public new readonly BindableList Current = new BindableList(); - private MultipleSelectionFilter filter; + private MultipleSelectionFilter filter = null!; public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header) : base(header) @@ -42,7 +39,6 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Creates a filter control that can be used to simultaneously select multiple values of type . /// - [NotNull] protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter(); protected partial class MultipleSelectionFilter : FillFlowContainer @@ -69,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing Current.BindCollectionChanged(currentChanged, true); } - private void currentChanged(object sender, NotifyCollectionChangedEventArgs e) + private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e) { foreach (var c in Children) c.Active.Value = Current.Contains(c.Value); @@ -122,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapListing { base.UpdateState(); selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint); - selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint); + selectedUnderline.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index ee188d34ce..2896039a99 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -24,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } - private OsuSpriteText text; + protected OsuSpriteText Text; protected Sample SelectSample { get; private set; } = null!; @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing AutoSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] { - text = new OsuSpriteText + Text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Text = LabelFor(Value) @@ -86,14 +87,26 @@ namespace osu.Game.Overlays.BeatmapListing protected virtual bool HighlightOnHoverWhenActive => false; - protected virtual void UpdateState() - { - bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive); + protected virtual Color4 ColourActive => ColourProvider.Content1; + protected virtual Color4 ColourNormal => ColourProvider.Light2; - text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint); - text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); + protected Color4 ColourForCurrentState + { + get + { + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered && (!Active.Value || HighlightOnHoverWhenActive)) + colour = colour.Lighten(0.2f); + + return colour; + } } - protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2; + protected virtual void UpdateState() + { + Text.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); + } } } From 77b00dac7e4956a143acdf8cbd5d3100064f403f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jun 2024 15:20:55 +0900 Subject: [PATCH 098/130] Fix low pass filter sometimes not applied in dialog overlays --- .../Overlays/Dialog/PopupDialogDangerousButton.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index 19d7ea7a87..d16f49eab7 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -71,7 +72,7 @@ namespace osu.Game.Overlays.Dialog protected override void LoadComplete() { base.LoadComplete(); - Progress.BindValueChanged(progressChanged); + Progress.BindValueChanged(progressChanged, true); } protected override void AbortConfirm() @@ -122,11 +123,13 @@ namespace osu.Game.Overlays.Dialog private void progressChanged(ValueChangedEvent progress) { - if (progress.NewValue < progress.OldValue) return; + lowPassFilter.Cutoff = Math.Max(1, (int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5)); - if (Clock.CurrentTime - lastTickPlaybackTime < 30) return; + if (progress.NewValue < progress.OldValue) + return; - lowPassFilter.CutoffTo((int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5)); + if (Clock.CurrentTime - lastTickPlaybackTime < 30) + return; var channel = tickSample.GetChannel(); From fa1527f446163f546ae7f88a551e0d4ff9ff9caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 15:43:04 +0900 Subject: [PATCH 099/130] Update design for selected filters to better imply that they are selected --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 88 ++++++++++++++++--- .../Overlays/BeatmapListing/FilterTabItem.cs | 22 ++--- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 9b2e1d57fe..9cf3328149 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -7,12 +7,17 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; +using FontWeight = osu.Game.Graphics.FontWeight; namespace osu.Game.Overlays.BeatmapListing { @@ -95,30 +100,91 @@ namespace osu.Game.Overlays.BeatmapListing protected partial class MultipleSelectionFilterTabItem : FilterTabItem { - private readonly Box selectedUnderline; - - protected override bool HighlightOnHoverWhenActive => true; + private Drawable activeContent = null!; + private Circle background = null!; public MultipleSelectionFilterTabItem(T value) : base(value) { + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeDuration = 100; + AutoSizeEasing = Easing.OutQuint; + // This doesn't match any actual design, but should make it easier for the user to understand // that filters are applied until we settle on a final design. - AddInternal(selectedUnderline = new Box + AddInternal(activeContent = new Container { Depth = float.MaxValue, - RelativeSizeAxes = Axes.X, - Height = 1.5f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Padding = new MarginPadding + { + Left = -16, + Right = -4, + Vertical = -2 + }, + Children = new Drawable[] + { + background = new Circle + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Icon = FontAwesome.Solid.TimesCircle, + Size = new Vector2(10), + Colour = ColourProvider.Background4, + Position = new Vector2(3, 0.5f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } }); } + protected override Color4 ColourActive => ColourProvider.Light1; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + return Active.Value + ? background.ReceivePositionalInputAt(screenSpacePos) + : base.ReceivePositionalInputAt(screenSpacePos); + } + protected override void UpdateState() { - base.UpdateState(); - selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint); - selectedUnderline.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered) + colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f); + + if (Active.Value) + { + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; + + activeContent.FadeIn(200, Easing.OutQuint); + background.FadeColour(colour, 200, Easing.OutQuint); + + // flipping colours + Text.FadeColour(ColourProvider.Background4, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.SemiBold); + } + else + { + Padding = new MarginPadding(); + + activeContent.FadeOut(); + + background.FadeColour(colour, 200, Easing.OutQuint); + Text.FadeColour(colour, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.Regular); + } } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 2896039a99..8f4ecaa0f5 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -85,27 +85,17 @@ namespace osu.Game.Overlays.BeatmapListing /// protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString(); - protected virtual bool HighlightOnHoverWhenActive => false; - protected virtual Color4 ColourActive => ColourProvider.Content1; protected virtual Color4 ColourNormal => ColourProvider.Light2; - protected Color4 ColourForCurrentState - { - get - { - Color4 colour = Active.Value ? ColourActive : ColourNormal; - - if (IsHovered && (!Active.Value || HighlightOnHoverWhenActive)) - colour = colour.Lighten(0.2f); - - return colour; - } - } - protected virtual void UpdateState() { - Text.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered) + colour = colour.Lighten(0.2f); + + Text.FadeColour(colour, 200, Easing.OutQuint); Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); } } From 030bbf26414f075e55528ead9a18a518d539d237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:07:29 +0200 Subject: [PATCH 100/130] Fix vertical overlaps on multiselection filters when they wrap --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 9cf3328149..958297b559 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapListing { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Spacing = new Vector2(10, 0); + Spacing = new Vector2(10, 5); AddRange(GetValues().Select(CreateTabItem)); } From ace6427d406555b65be69482f7303f9da7735e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:30:28 +0200 Subject: [PATCH 101/130] Expand test coverage - Covers fail case that wasn't covered before - Removes arbitrary wait step that was inevitably going to cause intermittent test failures --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 4c81fc3fe6..7dcb8766dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -126,10 +126,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); } - [Test] - public void TestGameplayTestAtEndOfBeatmap() + [TestCase(2000)] // chosen to be after last object in the map + [TestCase(22000)] // chosen to be in the middle of the last spinner + public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd) { - AddStep("seek to last 2 seconds", () => EditorClock.Seek(importedBeatmapSet.MaxLength - 2000)); + AddStep($"seek to end minus {offsetFromEnd}ms", () => EditorClock.Seek(importedBeatmapSet.MaxLength - offsetFromEnd)); AddStep("click test gameplay button", () => { var button = Editor.ChildrenOfType().Single(); @@ -140,8 +141,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer); - AddWaitStep("wait some", 5); - AddAssert("current screen is editor", () => Stack.CurrentScreen is Editor); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); } [Test] From 7ac5bd4d37a4517d174c840ae32869d3e7029904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:37:10 +0200 Subject: [PATCH 102/130] Ensure past drawable objects also get their results populated in editor test play --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 2028094964..9a7c1822a3 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -48,6 +48,7 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadComplete(); markPreviousObjectsHit(); + markVisibleDrawableObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -67,9 +68,10 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; + var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }; - HealthProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); - ScoreProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); + HealthProcessor.ApplyResult(result); + ScoreProcessor.ApplyResult(result); } static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) @@ -88,6 +90,40 @@ namespace osu.Game.Screens.Edit.GameplayTest } } + private void markVisibleDrawableObjectsHit() + { + if (!DrawableRuleset.Playfield.IsLoaded) + { + Schedule(markVisibleDrawableObjectsHit); + return; + } + + foreach (var drawableObjectEntry in enumerateDrawableEntries( + DrawableRuleset.Playfield.AllHitObjects + .Select(ho => ho.Entry) + .Where(e => e != null) + .Cast(), editorState.Time)) + { + drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement) + { Type = drawableObjectEntry.HitObject.Judgement.MaxResult }; + } + + static IEnumerable enumerateDrawableEntries(IEnumerable entries, double cutoffTime) + { + foreach (var entry in entries) + { + foreach (var nested in enumerateDrawableEntries(entry.NestedEntries, cutoffTime)) + { + if (nested.HitObject.GetEndTime() < cutoffTime) + yield return nested; + } + + if (entry.HitObject.GetEndTime() < cutoffTime) + yield return entry; + } + } + } + protected override void PrepareReplay() { // don't record replays. From a3ea36d2b264659c99573edf4845e8ba2a0cbca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:45:45 +0200 Subject: [PATCH 103/130] Fix formatting --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 9a7c1822a3..69851d0d35 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -105,7 +105,9 @@ namespace osu.Game.Screens.Edit.GameplayTest .Cast(), editorState.Time)) { drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement) - { Type = drawableObjectEntry.HitObject.Judgement.MaxResult }; + { + Type = drawableObjectEntry.HitObject.Judgement.MaxResult + }; } static IEnumerable enumerateDrawableEntries(IEnumerable entries, double cutoffTime) From 55b80f70f68f1c4040fb6f3e4558de79907c9b27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 18:12:20 +0900 Subject: [PATCH 104/130] 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 105/130] 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 106/130] 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 107/130] 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 108/130] 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 109/130] 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 110/130] 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 111/130] 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 112/130] 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 113/130] 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 114/130] 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 115/130] 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 116/130] 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 117/130] 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 118/130] 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 119/130] 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 120/130] 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 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 121/130] 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 122/130] 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 123/130] 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 124/130] 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 125/130] 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 126/130] 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 abfdf90b541a94888e147554b863f24d96c2d277 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 3 Jul 2024 07:11:35 +0300 Subject: [PATCH 127/130] 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 128/130] 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 129/130] 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 56cdd83451ac4fbc4132b92d4cdca23b90b5ce26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 20:42:12 +0900 Subject: [PATCH 130/130] 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